├── tests ├── test_communication.py ├── test_exceptions.py ├── mushroom.png ├── test_knitlib_cli.py ├── test_src_knitlib_knitting_job.py ├── test_knitpat_module.py ├── test_ayab_image.py ├── test_knitting_plugin.py └── test_ayab_communication.py ├── docs ├── authors.rst ├── changelog.rst ├── contributing.rst ├── readme.rst ├── requirements.txt ├── reference │ ├── index.rst │ ├── knitlib_plugin_api.rst │ ├── knitpat.rst │ └── knitlib.rst ├── installation.rst ├── spelling_wordlist.txt ├── index.rst ├── usage.rst ├── writing_plugins.rst └── conf.py ├── requirements.txt ├── src └── knitlib │ ├── plugins │ ├── ayab_plugin │ │ ├── mushroom.png │ │ ├── __init__.py │ │ ├── ayab_communication.py │ │ ├── ayab_image.py │ │ └── ayab_control.py │ ├── pdd_plugin │ │ ├── splitfile2track.py │ │ ├── __init__.py │ │ ├── insertpattern.py │ │ ├── brother.py │ │ └── PDDemulate.py │ ├── __init__.py │ ├── dummy_plugin.py │ └── knitting_plugin.py │ ├── knitpat │ ├── validate.py │ ├── knitting_pattern_schema.json │ └── __init__.py │ ├── __init__.py │ ├── machine_handler.py │ ├── knitting_job.py │ ├── communication.py │ ├── __main__.py │ └── exceptions.py ├── CHANGELOG.rst ├── .coveragerc ├── AUTHORS.rst ├── MANIFEST.in ├── ci ├── templates │ ├── .travis.yml │ ├── appveyor.yml │ └── tox.ini ├── appveyor-with-compiler.cmd ├── bootstrap.py └── appveyor-bootstrap.ps1 ├── .gitignore ├── .travis.yml ├── setup.cfg ├── tox.ini ├── appveyor.yml ├── CONTRIBUTING.rst ├── setup.py ├── README.rst └── LICENSE /tests/test_communication.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../AUTHORS.rst 2 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | --index-url https://pypi.python.org/simple/ 2 | 3 | -e . -------------------------------------------------------------------------------- /tests/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knitlib/HEAD/tests/mushroom.png -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Overview 3 | ######## 4 | 5 | .. include:: ../README.rst 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinxcontrib-napoleon 3 | sphinx-py3doc-enhanced-theme 4 | -e . 5 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | knit* 8 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | At the command line:: 6 | 7 | pip install knitlib 8 | -------------------------------------------------------------------------------- /src/knitlib/plugins/ayab_plugin/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knitlib/HEAD/src/knitlib/plugins/ayab_plugin/mushroom.png -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 0.0.1 (2015-05-15 6 | ----------------------------------------- 7 | 8 | * First release on PyPI. 9 | -------------------------------------------------------------------------------- /docs/spelling_wordlist.txt: -------------------------------------------------------------------------------- 1 | builtin 2 | builtins 3 | classmethod 4 | staticmethod 5 | classmethods 6 | staticmethods 7 | args 8 | kwargs 9 | callstack 10 | Changelog 11 | Indices 12 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [paths] 2 | source = src 3 | 4 | [run] 5 | branch = True 6 | source = src 7 | parallel = true 8 | 9 | [report] 10 | show_missing = true 11 | precision = 2 12 | omit = *migrations* 13 | -------------------------------------------------------------------------------- /docs/reference/knitlib_plugin_api.rst: -------------------------------------------------------------------------------- 1 | Knitlib Plugin API 2 | ============================= 3 | 4 | 5 | Base Knitting Plugin 6 | -------------------- 7 | 8 | .. automodule:: knitlib.plugins.knitting_plugin 9 | :members: 10 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | 2 | Authors 3 | ======= 4 | 5 | * Sebastian Oliva - http://sebastianoliva.com 6 | * Shiluka Dharmasena - http://shiluka.blogspot.com/ 7 | 8 | AYAB Authors 9 | ------------ 10 | 11 | * Christian Obersteiner 12 | * Andreas Müller 13 | 14 | PDD Authors 15 | ----------- 16 | 17 | * Steve Conklin -------------------------------------------------------------------------------- /docs/reference/knitpat.rst: -------------------------------------------------------------------------------- 1 | Knitpat 2 | ============================= 3 | 4 | Knitpat is an standarized format for knitting machine files. It is based in JSON, validated via a JSON Schema and 5 | by each tool according to it's supported features. 6 | 7 | 8 | .. automodule:: knitlib.knitpat 9 | :members: 10 | -------------------------------------------------------------------------------- /src/knitlib/knitpat/validate.py: -------------------------------------------------------------------------------- 1 | import jsonschema 2 | import json 3 | 4 | 5 | def validate(): 6 | validate_file = file("knitting_pattern_sample.json", "rb") 7 | schema_file = file("knitting_pattern_schema.json", "rb") 8 | data = json.load(validate_file) 9 | schema = json.load(schema_file) 10 | jsonschema.validate(data, schema) 11 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to knitlib's documentation! 2 | ====================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | readme 10 | installation 11 | usage 12 | reference/index 13 | contributing 14 | authors 15 | changelog 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/reference/knitlib.rst: -------------------------------------------------------------------------------- 1 | Knitlib Overview 2 | ============================= 3 | 4 | 5 | Knitlib modules 6 | --------------- 7 | 8 | .. automodule:: knitlib 9 | :members: 10 | 11 | Knitlib machine handler 12 | ----------------------- 13 | 14 | .. automodule:: knitlib.machine_handler 15 | :members: 16 | 17 | Knitlib Knitting Jobs 18 | --------------------- 19 | 20 | .. automodule:: knitlib.knitting_job 21 | :members: -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs 2 | graft examples 3 | graft src 4 | graft ci 5 | graft tests 6 | 7 | include *.komodoproject 8 | include .bumpversion.cfg 9 | include .coveragerc 10 | include .isort.cfg 11 | include .pylintrc 12 | 13 | include AUTHORS.rst 14 | include CHANGELOG.rst 15 | include CONTRIBUTING.rst 16 | include LICENSE 17 | include README.rst 18 | 19 | include tox.ini .travis.yml appveyor.yml 20 | include requirements.txt 21 | 22 | include *.json 23 | 24 | global-exclude *.py[cod] __pycache__ *.so 25 | -------------------------------------------------------------------------------- /ci/templates/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 2.7 3 | sudo: false 4 | env: 5 | global: 6 | LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | matrix: 8 | - TOXENV=check 9 | {% for env, config in tox_environments|dictsort %} 10 | - TOXENV={{ env }}{% if config.cover %},extension-coveralls,coveralls{% endif %} 11 | 12 | {% endfor %} 13 | before_install: 14 | - python --version 15 | - virtualenv --version 16 | - pip --version 17 | - uname -a 18 | - lsb_release -a 19 | install: 20 | - pip install tox 21 | script: 22 | - tox -v 23 | notifications: 24 | email: 25 | on_success: never 26 | on_failure: always 27 | 28 | -------------------------------------------------------------------------------- /src/knitlib/plugins/pdd_plugin/splitfile2track.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | if len(sys.argv) != 2: 7 | print 'Usage: %s file.dat' % sys.argv[0] 8 | print 'Splits a 2K file.dat file into two 1K track files track0.dat and track1.dat' 9 | sys.exit() 10 | 11 | infile = open(sys.argv[1], 'rb') 12 | 13 | track0file = open("track0.dat", 'wb') 14 | track1file = open("track1.dat", 'wb') 15 | 16 | t0dat = infile.read(1024) 17 | t1dat = infile.read(1024) 18 | 19 | track0file.write(t0dat) 20 | track0file.close() 21 | 22 | track1file.write(t1dat) 23 | track1file.close() 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Compiled python modules 7 | *.pyc 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | venv*/ 24 | pyvenv*/ 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | .coverage.* 33 | nosetests.xml 34 | htmlcov 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | .idea 44 | *.iml 45 | *.komodoproject 46 | 47 | # Complexity 48 | output/*.html 49 | output/*/index.html 50 | 51 | # Sphinx 52 | docs/_build 53 | 54 | .DS_Store 55 | *~ 56 | .*.sw[po] 57 | .build 58 | .ve 59 | .bootstrap 60 | *.bak 61 | .pythoscope/ 62 | -------------------------------------------------------------------------------- /tests/test_knitlib_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from click.testing import CliRunner 3 | import mock 4 | from knitlib.__main__ import main 5 | 6 | 7 | def test_main_init_with_no_args(): 8 | runner = CliRunner() 9 | result = runner.invoke(main, []) 10 | 11 | assert result.output is not '()\n' 12 | assert result.exit_code is 0 # It should fail due to invalid configuration, 13 | 14 | 15 | @mock.patch("knitlib.__main__.logging.error") 16 | def test_invalid_plugin_fail(mock_logging): 17 | runner = CliRunner() 18 | result = runner.invoke(main, ["--plugin_name", "INVALID_PLUGIN"]) 19 | # assert mock_logging has been called 20 | assert result.output is not '()\n' 21 | #assert result.exit_code is not 0 22 | 23 | 24 | @mock.patch('knitlib.plugins.dummy_plugin.logging.debug') 25 | def test_dummy_knit(mock_logging): 26 | runner = CliRunner() 27 | result = runner.invoke(main, ["--plugin_name", "dummy"]) 28 | # assert mock_logging.called 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/knitlib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva 18 | 19 | __version__ = "0.0.1" 20 | 21 | import machine_handler 22 | machine_handler = machine_handler 23 | 24 | import knitpat 25 | knitpat = knitpat 26 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | To use knitlib in a project:: 6 | 7 | import knitlib 8 | 9 | # Search for a plugin via it's public name. 10 | Plugin = knitlib.machine_handler.get_machine_plugin_by_id(plugin_name) 11 | # Initalize the plugin instance 12 | machine_instance = Plugin() 13 | machine_instance.configure(conf={'options': 'go here'}) 14 | # Knit will block execution. If you need async execution check the callbacks configuration. 15 | machine_instance.knit() 16 | machine_instance.finish() 17 | 18 | Each machine has it's own configuration options, however the base functions are standardized in knitpat. 19 | Check the Knitpat section for more info. 20 | 21 | Knitlib API allows for setting callbacks for certain actions that the user needs to interact with the underlying 22 | hardware. This callbacks system includes messaging, messages with blocking operations (move knob, set dial, feed yarn, 23 | etc) and progress. Default callbacks are included for CLI operation, and projects should provide their own 24 | implementations for the environment in use (knitweb, desktop UIs, etc). 25 | -------------------------------------------------------------------------------- /src/knitlib/plugins/ayab_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. It is based on AYAB 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva, Christian Obersteiner, Andreas Müller 18 | 19 | __author__ = 'tian' 20 | 21 | import ayab_control 22 | 23 | # This adds ayab_control.AyabControl to the upper namespace of the module. 24 | AyabPluginControl = ayab_control.AyabPluginControl 25 | -------------------------------------------------------------------------------- /docs/writing_plugins.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Writing Plugins 3 | =============== 4 | 5 | One of the main objectives of knitlib is to be a common platform for knitting machine software and control. Knitlib is 6 | designed with a modular approach, that allows programmers to easily create plugins without worrying about Control, UI 7 | elements, file and port abstractions, knitting pattern specification and format, etc. 8 | 9 | Fundamental Abstractions: A Finite State Machine for Knitting Machines 10 | ====================================================================== 11 | 12 | Center to all abstractions in Knitlib is the Knitting Finite State Machine, defined at knitlib/plugins/knitting_plugin.py 13 | BaseKnittingPlugin is a simple plugin base that offers several commodities for the handling of the knitting flow. 14 | Function such as onconfigure, onknit and onfinish are used to setup the plugin and to operate it. 15 | 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | knitlib could always use more documentation, whether as part of the 21 | official knitlib docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 2.7 3 | sudo: false 4 | env: 5 | global: 6 | LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so 7 | matrix: 8 | - TOXENV=check 9 | # - TOXENV=2.6,extension-coveralls,coveralls 10 | # - TOXENV=2.6-nocover 11 | - TOXENV=2.7,extension-coveralls,coveralls 12 | - TOXENV=2.7-nocover 13 | # - TOXENV=3.3,extension-coveralls,coveralls 14 | # - TOXENV=3.3-nocover 15 | # - TOXENV=3.4,extension-coveralls,coveralls 16 | # - TOXENV=3.4-nocover 17 | #- TOXENV=pypy,extension-coveralls,coveralls 18 | #- TOXENV=pypy-nocover 19 | before_install: 20 | - python --version 21 | - virtualenv --version 22 | - pip --version 23 | - uname -a 24 | - lsb_release -a 25 | install: 26 | - pip install tox flake8 27 | script: 28 | - tox -v 29 | notifications: 30 | email: 31 | on_success: never 32 | on_failure: always 33 | webhooks: 34 | urls: 35 | - https://webhooks.gitter.im/e/5d1581aa60471c78e1da 36 | on_success: change # options: [always|never|change] default: always 37 | on_failure: always # options: [always|never|change] default: always 38 | on_start: false # default: false 39 | 40 | -------------------------------------------------------------------------------- /tests/test_src_knitlib_knitting_job.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestKnittingJob(unittest.TestCase): 5 | def test___init__(self): 6 | # knitting_job = KnittingJob(plugin_class, port, knitpat_file) 7 | assert True # TODO: implement your test here 8 | 9 | def test_configure_job(self): 10 | # knitting_job = KnittingJob(plugin_class, port, knitpat_file) 11 | # self.assertEqual(expected, knitting_job.configure_job()) 12 | assert True # TODO: implement your test here 13 | 14 | def test_get_plugin_name(self): 15 | # knitting_job = KnittingJob(plugin_class, port, knitpat_file) 16 | # self.assertEqual(expected, knitting_job.get_plugin_name()) 17 | assert True # TODO: implement your test here 18 | 19 | def test_knit_job(self): 20 | # knitting_job = KnittingJob(plugin_class, port, knitpat_file) 21 | # self.assertEqual(expected, knitting_job.knit_job()) 22 | assert True # TODO: implement your test here 23 | 24 | def test_start_job(self): 25 | # knitting_job = KnittingJob(plugin_class, port, knitpat_file) 26 | # self.assertEqual(expected, knitting_job.start_job()) 27 | assert True # TODO: implement your test here 28 | 29 | 30 | if __name__ == '__main__': 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | 6 | 7 | extensions = [ 8 | 'sphinx.ext.autodoc', 9 | 'sphinx.ext.autosummary', 10 | 'sphinx.ext.todo', 11 | 'sphinx.ext.coverage', 12 | 'sphinx.ext.ifconfig', 13 | 'sphinx.ext.viewcode', 14 | 'sphinx.ext.napoleon' 15 | ] 16 | if os.getenv('SPELLCHECK'): 17 | extensions += 'sphinxcontrib.spelling', 18 | spelling_show_suggestions = True 19 | spelling_lang = 'en_US' 20 | 21 | source_suffix = '.rst' 22 | master_doc = 'index' 23 | project = 'knitlib' 24 | year = '2015' 25 | author = 'Sebastian Oliva' 26 | copyright = '{0}, {1}'.format(year, author) 27 | version = release = '0.0.1' 28 | 29 | #import sphinx_py3doc_enhanced_theme 30 | #html_theme = "sphinx_py3doc_enhanced_theme" 31 | #html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] 32 | 33 | pygments_style = 'trac' 34 | templates_path = ['.'] 35 | html_use_smartypants = True 36 | html_last_updated_fmt = '%b %d, %Y' 37 | html_split_index = True 38 | html_sidebars = { 39 | '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], 40 | } 41 | html_short_title = '%s-%s' % (project, version) 42 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 43 | if on_rtd: 44 | html_theme_options = { 45 | 'githuburl': 'https://github.com/fashiontec/knitlib/' 46 | } 47 | -------------------------------------------------------------------------------- /src/knitlib/knitpat/knitting_pattern_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema":"http://json-schema.org/draft-04/schema#", 3 | "title":"KnittingPattern", 4 | "description":"A knitting pattern specification. Defines a file name and knitting specs.", 5 | "type":"object", 6 | "properties":{ 7 | "file_url":{ 8 | "description":"The name of the file that holds the pattern.", 9 | "type":"string" 10 | }, 11 | "pattern_name":{ 12 | "description":"Name of the pattern", 13 | "type":"string" 14 | }, 15 | "dimensions":{ 16 | "type":"object", 17 | "properties":{ 18 | "image_length":{ 19 | "type":"integer" 20 | }, 21 | "image_width":{ 22 | "type":"integer" 23 | }, 24 | "physical_length":{ 25 | "type":"number" 26 | }, 27 | "physical_width":{ 28 | "type":"number" 29 | }, 30 | "units":{ 31 | "type":"string" 32 | }, 33 | "yarn_strength":{ 34 | "type":"object", 35 | "properties":{ 36 | "units":{ 37 | "type":"string" 38 | }, 39 | "value":{ 40 | "type":"number" 41 | } 42 | } 43 | } 44 | } 45 | }, 46 | "colors":{ 47 | "type":"integer" 48 | } 49 | }, 50 | "required":[ 51 | "file_url", 52 | "colors" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/knitlib/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva 18 | 19 | from enum import Enum 20 | 21 | from dummy_plugin import DummyKnittingPlugin 22 | from ayab_plugin import AyabPluginControl 23 | from pdd_plugin import PDDEmulationKnittingPlugin 24 | 25 | 26 | PluginType = Enum('PluginType', 'serial network other') 27 | '''PluginTypes holds an enumeration of the type of machine plugins available for use.''' 28 | 29 | active_plugins = { 30 | PluginType.other: {DummyKnittingPlugin.__PLUGIN_NAME__: DummyKnittingPlugin}, 31 | PluginType.serial: {AyabPluginControl.__PLUGIN_NAME__: AyabPluginControl, 32 | PDDEmulationKnittingPlugin.__PLUGIN_NAME__: PDDEmulationKnittingPlugin, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/knitlib/machine_handler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import plugins 3 | import serial.tools.list_ports 4 | 5 | """Handles allocation of machines to plugins and resources. 6 | 7 | Machine Plugins are defined as subclasses of BaseKnittingPlugin. Each Machine 8 | plugin can be matched to a port or interface. Each port can only handle one 9 | machine at once. 10 | """ 11 | 12 | 13 | def get_available_ports(): 14 | """Returns a list tuples of available serial ports.""" 15 | # TODO: add other kinds of ports listing. 16 | return list(serial.tools.list_ports.comports()) 17 | 18 | 19 | def get_machines_by_type(machine_type): 20 | """Returns a list of the available plugins for a given PluginType or empty array if none found.""" 21 | return plugins.active_plugins.get(machine_type, {}) 22 | 23 | 24 | def get_active_machine_plugins_names(): 25 | """Returns a list of tuples of the available plugins and type.""" 26 | active_plugins = [] 27 | for plugin_type, plugins_by_type in plugins.active_plugins.items(): 28 | for plugin_name, plugin_class in plugins_by_type.items(): 29 | active_plugins.append(plugin_name) 30 | return active_plugins 31 | 32 | 33 | def get_machine_plugin_by_id(machine_id, if_not_found=None): 34 | """Returns a machine plugin given the machine_id class name.""" 35 | for k, v in plugins.active_plugins.items(): 36 | if machine_id in v: 37 | return v[machine_id] 38 | return if_not_found 39 | 40 | 41 | def get_machine_types(): 42 | """Returns the PluginType Enum.""" 43 | return plugins.PluginType 44 | -------------------------------------------------------------------------------- /src/knitlib/knitpat/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tian' 2 | 3 | import jsonschema 4 | import logging 5 | import json 6 | import pkg_resources 7 | 8 | 9 | __SCHEMA_DATA = pkg_resources.resource_string(__name__, "knitting_pattern_schema.json") 10 | # file("./knitting_pattern_schema.json", "rb") 11 | __SCHEMA_DICT = json.loads(__SCHEMA_DATA) 12 | 13 | 14 | def validate_dict(loaded_json_data): 15 | """Checks if a dict is a valid Knitpat format. 16 | 17 | Validates if a dict, loaded from a json file, is valid according to the Knitpat scheme. 18 | 19 | Returns: 20 | True if valid, False if not. 21 | """ 22 | try: 23 | jsonschema.validate(loaded_json_data, __SCHEMA_DICT) 24 | return True 25 | except Exception as e: 26 | logging.error(e) 27 | return False 28 | 29 | 30 | def parse_ustring(string_data): 31 | """Parses a string into a dict and validates it.""" 32 | loaded_json_data = json.loads(string_data) 33 | if validate_dict(loaded_json_data): 34 | return loaded_json_data 35 | else: 36 | raise ValueError("Invalid data string") 37 | 38 | 39 | def parse_dict_from_cli(cli_dict): 40 | """Parses from CLI dict into a valid Knitpat.""" 41 | # TODO add parsing for size and measurement. 42 | parsed_dict = {} 43 | for k, v in cli_dict.items(): 44 | if k in ["colors"]: 45 | parsed_dict[k] = int(v) 46 | else: 47 | parsed_dict[k] = v 48 | if validate_dict(parsed_dict): 49 | return parsed_dict 50 | else: 51 | return dict() 52 | -------------------------------------------------------------------------------- /ci/templates/appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | environment: 4 | global: 5 | WITH_COMPILER: "cmd /E:ON /V:ON /C .\\ci\\appveyor-with-compiler.cmd" 6 | matrix: 7 | - TOXENV: check 8 | PYTHON_HOME: "C:\\Python27" 9 | PYTHON_VERSION: "2.7" 10 | PYTHON_ARCH: "32" 11 | {% for env, config in tox_environments|dictsort %}{% if env.startswith('2.7') or env.startswith('3.4') or env.startswith('3.3') %} 12 | - TOXENV: "{{ env }}" 13 | TOXPYTHON: "C:\\Python{{ env[:3].replace('.', '') }}\\python.exe" 14 | WINDOWS_SDK_VERSION: "v7.{{ '1' if env[0] == '3' else '0' }}" 15 | PYTHON_HOME: "C:\\Python{{ env[:3].replace('.', '') }}" 16 | PYTHON_VERSION: "{{ env[:3] }}" 17 | PYTHON_ARCH: "32" 18 | - TOXENV: "{{ env }}" 19 | TOXPYTHON: "C:\\Python{{ env[:3].replace('.', '') }}-x64\\python.exe" 20 | WINDOWS_SDK_VERSION: "v7.{{ '1' if env[0] == '3' else '0' }}" 21 | PYTHON_HOME: "C:\\Python{{ env[:3].replace('.', '') }}-x64" 22 | PYTHON_VERSION: "{{ env[:3] }}" 23 | PYTHON_ARCH: "64" 24 | {% endif %}{% endfor %} 25 | init: 26 | - "ECHO %TOXENV%" 27 | - ps: "ls C:\\Python*" 28 | install: 29 | - "powershell ci\\appveyor-bootstrap.ps1" 30 | test_script: 31 | - "%PYTHON_HOME%\\Scripts\\tox --version" 32 | - "%PYTHON_HOME%\\Scripts\\virtualenv --version" 33 | - "%PYTHON_HOME%\\Scripts\\pip --version" 34 | - "%WITH_COMPILER% %PYTHON_HOME%\\Scripts\\tox" 35 | after_test: 36 | - "IF \"%TOXENV:~-8,8%\" == \"-nocover\" %WITH_COMPILER% %TOXPYTHON% setup.py bdist_wheel" 37 | artifacts: 38 | - path: dist\* 39 | 40 | -------------------------------------------------------------------------------- /ci/appveyor-with-compiler.cmd: -------------------------------------------------------------------------------- 1 | :: To build extensions for 64 bit Python 3, we need to configure environment 2 | :: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: 3 | :: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) 4 | :: 5 | :: To build extensions for 64 bit Python 2, we need to configure environment 6 | :: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: 7 | :: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) 8 | :: 9 | :: 32 bit builds do not require specific environment configurations. 10 | :: 11 | :: Note: this script needs to be run with the /E:ON and /V:ON flags for the 12 | :: cmd interpreter, at least for (SDK v7.0) 13 | :: 14 | :: More details at: 15 | :: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows 16 | :: http://stackoverflow.com/a/13751649/163740 17 | :: 18 | :: Author: Olivier Grisel 19 | :: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 20 | @ECHO OFF 21 | 22 | SET COMMAND_TO_RUN=%* 23 | SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows 24 | 25 | IF "%PYTHON_ARCH%"=="64" ( 26 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 27 | SET DISTUTILS_USE_SDK=1 28 | SET MSSdk=1 29 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% 30 | "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release 31 | ECHO Executing: %COMMAND_TO_RUN% 32 | call %COMMAND_TO_RUN% || EXIT 1 33 | ) ELSE ( 34 | ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% 35 | ECHO Executing: %COMMAND_TO_RUN% 36 | call %COMMAND_TO_RUN% || EXIT 1 37 | ) 38 | -------------------------------------------------------------------------------- /tests/test_knitpat_module.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tian' 2 | 3 | import json 4 | import logging 5 | from click.testing import CliRunner 6 | 7 | from knitlib import __main__ as main 8 | from knitlib.knitpat import validate_dict 9 | from knitlib.knitpat import parse_ustring 10 | 11 | 12 | def test_validate_simple_data_knitpat_method(): 13 | test_sample = ("{\n" 14 | " \"file_url\":\"mypat.png\",\n" 15 | " \"name\":\"a small sweater\",\n" 16 | " \"colors\": 2}") 17 | sample_dict = json.loads(test_sample) 18 | validation = validate_dict(sample_dict) 19 | assert validation is True 20 | 21 | 22 | def test_validate_invalid_data_knitpat_method(): 23 | test_sample = ("{\n" 24 | " \"file_location\":\"mypat.png\",\n" 25 | " \"colors\": 2}") 26 | sample_dict = json.loads(test_sample) 27 | validation = validate_dict(sample_dict) 28 | assert validation is False 29 | 30 | 31 | def test_parse(): 32 | test_sample = ("{\n" 33 | " \"file_url\":\"mypat.png\",\n" 34 | " \"name\":\"a small sweater\",\n" 35 | " \"colors\": 2}") 36 | test_dict = {u"file_url": u"mypat.png", 37 | u"name": u"a small sweater", 38 | u"colors": 2} 39 | parsed_sample = parse_ustring(test_sample) 40 | assert test_dict == parsed_sample 41 | 42 | 43 | def test_cli_parse_simple_pattern(): 44 | runner = CliRunner() 45 | 46 | result = runner.invoke(main, ["--plugin_name", "dummy", 47 | "--config", "file_url", "mypat.png", 48 | "--config", "pattern_name", "a small sweater", 49 | "--config", "colors", "2" 50 | ]) 51 | print result.exception 52 | assert result.exit_code is 0 # Should be 0 as it would mean successful execution. 53 | # TODO: assert that parse_dict_from_cli is properly working 54 | 55 | -------------------------------------------------------------------------------- /src/knitlib/plugins/dummy_plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva 18 | 19 | import logging 20 | import time 21 | import knitting_plugin 22 | 23 | 24 | class DummyKnittingPlugin(knitting_plugin.BaseKnittingPlugin): 25 | """Implements a sample knitting plugin that allows for simple operation emulation.""" 26 | 27 | __PLUGIN_NAME__ = u"dummy" 28 | 29 | def __init__(self): 30 | super(DummyKnittingPlugin, self).__init__() 31 | 32 | base_log_string = u"{} has been called on dummy knitting plugin." 33 | 34 | def onknit(self, e): 35 | logging.debug(DummyKnittingPlugin.base_log_string.format("onknit")) 36 | # In order to simulate blocking we make it sleep. 37 | total = 5 38 | for i in range(total): 39 | time.sleep(1) 40 | self.interactive_callbacks["progress"](i / float(total), i, total) 41 | self.finish() 42 | 43 | def onfinish(self, e): 44 | logging.debug(DummyKnittingPlugin.base_log_string.format("onfinish")) 45 | 46 | def onconfigure(self, e): 47 | logging.debug(DummyKnittingPlugin.base_log_string.format("onconfigure")) 48 | 49 | def set_port(self, *args, **kwargs): 50 | pass 51 | 52 | @staticmethod 53 | def supported_config_features(): 54 | return {"$schema": "http://json-schema.org/schema#", "type": "object"} 55 | -------------------------------------------------------------------------------- /src/knitlib/knitting_job.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tian' 2 | 3 | from knitlib.plugins.knitting_plugin import BaseKnittingPlugin 4 | import uuid 5 | 6 | 7 | class KnittingJob(object): 8 | """A Knitting job is composed of a Machine Plugin at a certain state, a port and a knitpat file.""" 9 | 10 | def __init__(self, plugin_class, port, callbacks_dict=None, knitpat_dict=None): 11 | assert issubclass(plugin_class, BaseKnittingPlugin) 12 | self.id = uuid.uuid4() 13 | self.__plugin_class = plugin_class 14 | self.__plugin = None 15 | self.__callback_dict = callbacks_dict 16 | self.__port = port 17 | self.__knitpat_dict = knitpat_dict 18 | 19 | def get_plugin_name(self): 20 | return self.__plugin.__PLUGIN_NAME__ 21 | 22 | def get_plugin_instance(self): 23 | return self.__plugin 24 | 25 | def get_status(self): 26 | return self.__plugin.current 27 | 28 | def get_job_public_dict(self): 29 | plugin_state = self.__plugin.current if self.__plugin else "none" 30 | return { 31 | "id": str(self.id), 32 | "plugin_type": self.__plugin_class.__PLUGIN_NAME__, 33 | "port": str(self.__port), 34 | "state": plugin_state, 35 | "knitpat_file": self.__knitpat_dict 36 | } 37 | 38 | def init_job(self): 39 | self.__plugin = self.__plugin_class() 40 | assert self.__plugin.current == "activated" 41 | self.__plugin.set_port(self.__port) 42 | if self.__callback_dict is not None: 43 | self.__plugin.register_interactive_callbacks(self.__callback_dict) 44 | 45 | def configure_job(self, knitpat_dict=None): 46 | if knitpat_dict is None: 47 | knitpat_dict = self.__knitpat_dict 48 | self.__plugin.configure(conf=knitpat_dict) 49 | 50 | def knit_job(self): 51 | """Blocking knitting job operation. 52 | 53 | Be sure to call knit_job in an async context, or as a greenlet spawn. 54 | """ 55 | # TODO: Add exception handling. 56 | self.__plugin.knit() 57 | -------------------------------------------------------------------------------- /tests/test_ayab_image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. It is based on AYAB. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Shiluka Dharmasena, Sebastian Oliva 18 | 19 | import pytest 20 | import unittest 21 | import os 22 | from knitlib.plugins.ayab_plugin.ayab_image import ayabImage 23 | from PIL import Image 24 | 25 | class TestImage(unittest.TestCase): 26 | 27 | def setUp(self): 28 | self.script_dir = os.path.dirname(os.path.abspath(__file__)) 29 | self.filename_text = u"mushroom.png" 30 | self.conf = {} 31 | self.conf["filename"] = self.filename_text 32 | self.pil_image = Image.open(os.path.join(self.script_dir, self.conf["filename"])) 33 | self.ayab_image = ayabImage(self.pil_image, 2) 34 | 35 | def test_knitStartNeedle(self): 36 | assert self.ayab_image.knitStartNeedle() == 0 37 | 38 | def test_knitStopNeedle(self): 39 | assert self.ayab_image.knitStopNeedle() == 199 40 | 41 | def test_imgPosition(self): 42 | assert self.ayab_image.imgPosition() == 'center' 43 | 44 | def test_startLine(self): 45 | assert self.ayab_image.startLine() == 0 46 | 47 | def test_numColors(self): 48 | assert self.ayab_image.numColors() == 2 49 | 50 | def test_setStartLine(self): 51 | self.startLine = 0 52 | self.ayab_image.setStartLine(self.startLine) 53 | assert self.ayab_image.startLine() == 0 54 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = register clean --all sdist 3 | 4 | [flake8] 5 | max-line-length = 140 6 | exclude = tests/*,*/migrations/*,*/south_migrations/* 7 | 8 | [bumpversion] 9 | current_version = 0.0.1 10 | files = setup.py docs/conf.py src/knitlib/__init__.py 11 | commit = True 12 | tag = True 13 | 14 | [pytest] 15 | norecursedirs = 16 | .git 17 | .tox 18 | dist 19 | build 20 | south_migrations 21 | migrations 22 | python_files = 23 | test_*.py 24 | *_test.py 25 | tests.py 26 | addopts = 27 | -rxEfs 28 | --strict 29 | --ignore=docs/conf.py 30 | --ignore=setup.py 31 | --ignore=ci 32 | --doctest-modules 33 | --doctest-glob=\*.rst 34 | --tb=short 35 | 36 | [isort] 37 | force_single_line=True 38 | line_length=120 39 | known_first_party=knitlib 40 | default_section=THIRDPARTY 41 | forced_separate=test_knitlib 42 | 43 | [matrix] 44 | # This is the configuration for the `./bootstrap.py` script. 45 | # It generates `.travis.yml`, `tox.ini` and `appveyor.yml`. 46 | # 47 | # Syntax: [alias:] value [!variable[glob]] [&variable[glob]] 48 | # 49 | # alias: 50 | # - is used to generate the tox environment 51 | # - it's optional 52 | # - if not present the alias will be computed from the `value` 53 | # value: 54 | # - a value of "-" means empty 55 | # !variable[glob]: 56 | # - exclude the combination of the current `value` with 57 | # any value matching the `glob` in `variable` 58 | # - can use as many you want 59 | # &variable[glob]: 60 | # - only include the combination of the current `value` 61 | # when there's a value matching `glob` in `variable` 62 | # - can use as many you want 63 | 64 | python_versions = 65 | #2.6 66 | 2.7 67 | #3.3 68 | #3.4 69 | #pypy 70 | 71 | dependencies = 72 | fysom 73 | pyserial 74 | # 1.4: Django==1.4.16 !python_versions[3.*] 75 | # 1.5: Django==1.5.11 76 | # 1.6: Django==1.6.8 77 | # 1.7: Django==1.7.1 !python_versions[2.6] 78 | # Deps commented above are provided as examples. That's what you would use in a Django project. 79 | 80 | 81 | coverage_flags = 82 | : true 83 | nocover: false 84 | 85 | environment_variables = 86 | - 87 | -------------------------------------------------------------------------------- /src/knitlib/communication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of AYAB. 3 | # 4 | # AYAB is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # AYAB is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with AYAB. If not, see . 16 | # 17 | # Copyright 2015 Shiluka Dharmasena, Sebastian Oliva, Christian Obersteiner, Andreas Müller 18 | 19 | import time 20 | import serial 21 | import logging 22 | 23 | 24 | class Communication(object): 25 | """Class Handling the serial communication protocol.""" 26 | 27 | def __init__(self, serial=None): 28 | """Creates an Communication object, with an optional serial-like object.""" 29 | logging.basicConfig(level=logging.DEBUG) 30 | self.__logger = logging.getLogger(__name__) 31 | self.__ser = serial 32 | 33 | def __del__(self): 34 | """Handles on delete behavior closing serial port object.""" 35 | self.close_serial() 36 | 37 | def open_serial(self, pPortname=None, baudrate=None): 38 | """Opens serial port communication with a portName and baudrate.""" 39 | if not self.__ser: 40 | self.__portname = pPortname 41 | self.__baudrate = baudrate 42 | try: 43 | self.__ser = serial.Serial(self.__portname, self.__baudrate) 44 | time.sleep(1) 45 | except: 46 | self.__logger.error("could not open serial port " + self.__portname + "with specific baudrate " + self.__baudrate) 47 | raise CommunicationException() 48 | return True 49 | 50 | def close_serial(self): 51 | """Closes serial port.""" 52 | try: 53 | self.__ser.close() 54 | del(self.__ser) 55 | self.__ser = None 56 | except: 57 | raise CommunicationException() 58 | 59 | 60 | class CommunicationException(Exception): 61 | pass 62 | -------------------------------------------------------------------------------- /ci/bootstrap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, print_function, unicode_literals 4 | 5 | import os 6 | import sys 7 | from os.path import exists 8 | from os.path import join 9 | from os.path import dirname 10 | from os.path import abspath 11 | 12 | 13 | if __name__ == "__main__": 14 | base_path = dirname(dirname(abspath(__file__))) 15 | print("Project path: {0}".format(base_path)) 16 | env_path = join(base_path, ".tox", "bootstrap") 17 | if sys.platform == "win32": 18 | bin_path = join(env_path, "Scripts") 19 | else: 20 | bin_path = join(env_path, "bin") 21 | if not exists(env_path): 22 | import subprocess 23 | print("Making bootstrap env in: {0} ...".format(env_path)) 24 | try: 25 | subprocess.check_call(["virtualenv", env_path]) 26 | except Exception: 27 | subprocess.check_call([sys.executable, "-m", "virtualenv", env_path]) 28 | print("Installing `jinja2` and `matrix` into bootstrap environment ...") 29 | subprocess.check_call([join(bin_path, "pip"), "install", "jinja2", "matrix"]) 30 | activate = join(bin_path, "activate_this.py") 31 | exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) 32 | 33 | import jinja2 34 | import matrix 35 | 36 | jinja = jinja2.Environment( 37 | loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), 38 | trim_blocks=True, 39 | lstrip_blocks=True, 40 | keep_trailing_newline=True 41 | ) 42 | tox_environments = {} 43 | for (alias, conf) in matrix.from_file(join(base_path, "setup.cfg")).items(): 44 | python = conf["python_versions"] 45 | deps = conf["dependencies"] 46 | if "coverage_flags" in conf: 47 | cover = {"false": False, "true": True}[conf["coverage_flags"].lower()] 48 | if "environment_variables" in conf: 49 | env_vars = conf["environment_variables"] 50 | 51 | tox_environments[alias] = { 52 | "python": "python" + python if "py" not in python else python, 53 | "deps": deps.split(), 54 | } 55 | if "coverage_flags" in conf: 56 | tox_environments[alias].update(cover=cover) 57 | if "environment_variables" in conf: 58 | tox_environments[alias].update(env_vars=env_vars.split()) 59 | 60 | for name in os.listdir(join("ci", "templates")): 61 | with open(join(base_path, name), "w") as fh: 62 | fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) 63 | print("Wrote {}".format(name)) 64 | print("DONE.") 65 | -------------------------------------------------------------------------------- /src/knitlib/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva 18 | 19 | import sys 20 | import click 21 | import knitlib 22 | import logging 23 | import knitpat 24 | import knitting_job 25 | # Why does this file exist, and why __main__? 26 | # For more info, read: 27 | # - https://www.python.org/dev/peps/pep-0338/ 28 | # - https://docs.python.org/2/using/cmdline.html#cmdoption-m 29 | # - https://docs.python.org/3/using/cmdline.html#cmdoption-m 30 | 31 | name = "knitlib" 32 | 33 | 34 | @click.command() 35 | @click.option('--plugin_name', default="dummy", # pPluginType prompt='Name of the Machine Plugin you want.', 36 | help='The name of the Machine Plugin you want.') 37 | @click.option('--config', multiple=True, nargs=2, type=click.Tuple([unicode, unicode]), help="Arguments tuples for the " 38 | "selected plugin") 39 | @click.option('--port', type=unicode, help="Serial Port") 40 | def main(plugin_name, config, port): 41 | logging.basicConfig(level=logging.DEBUG) 42 | logging.debug("Config got as object {0}".format(config)) 43 | logging.debug("Port set as {}".format(port)) 44 | config_dict = dict(config) 45 | logging.debug(config_dict) 46 | knitpat_dict = knitpat.parse_dict_from_cli(config_dict) 47 | logging.debug("Knitpat object parsed as object {0}".format(knitpat_dict)) 48 | # Getting the selected plugin from ID. 49 | plugin = knitlib.machine_handler.get_machine_plugin_by_id(plugin_name) 50 | logging.debug("Plugin Selected: {0}".format(plugin)) 51 | if plugin is None: 52 | print("The plugin selected is not available. Available plugins are: {}". 53 | format(knitlib.machine_handler.get_active_machine_plugins_names())) 54 | return -2 55 | knitting_job_instance = knitting_job.KnittingJob(plugin, port) 56 | knitting_job_instance.init_job() 57 | knitting_job_instance.configure_job(knitpat_dict) 58 | knitting_job_instance.knit_job() 59 | return 0 60 | 61 | if __name__ == "__main__": 62 | sys.exit(main()) 63 | -------------------------------------------------------------------------------- /ci/templates/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | clean, 4 | check, 5 | {% for env in tox_environments|sort %} 6 | {{ env }}, 7 | {% endfor %} 8 | report, 9 | docs 10 | 11 | [testenv] 12 | setenv = 13 | PYTHONPATH={toxinidir}/tests 14 | PYTHONUNBUFFERED=yes 15 | deps = 16 | pytest 17 | pytest-capturelog 18 | commands = 19 | python setup.py clean --all build_ext --force --inplace 20 | {posargs:py.test -vv --ignore=src} 21 | 22 | [testenv:spell] 23 | setenv = 24 | SPELLCHECK = 1 25 | commands = 26 | sphinx-build -b spelling docs dist/docs 27 | usedevelop = true 28 | deps = 29 | -r{toxinidir}/docs/requirements.txt 30 | sphinxcontrib-spelling 31 | pyenchant 32 | 33 | [testenv:docs] 34 | whitelist_externals = 35 | rm 36 | commands = 37 | sphinx-build {posargs:-E} -b html docs dist/docs 38 | sphinx-build -b linkcheck docs dist/docs 39 | usedevelop = true 40 | deps = 41 | -r{toxinidir}/docs/requirements.txt 42 | 43 | [testenv:configure] 44 | deps = 45 | jinja2 46 | matrix 47 | usedevelop = true 48 | commands = 49 | python bootstrap.py 50 | 51 | [testenv:check] 52 | basepython = python3.4 53 | deps = 54 | docutils 55 | check-manifest 56 | flake8 57 | readme 58 | pygments 59 | usedevelop = true 60 | commands = 61 | python setup.py check --strict --metadata --restructuredtext 62 | check-manifest {toxinidir} 63 | flake8 src 64 | 65 | [testenv:coveralls] 66 | deps = 67 | coveralls 68 | usedevelop = true 69 | commands = 70 | coverage combine 71 | coverage report 72 | coveralls --merge=extension-coveralls.json 73 | 74 | [testenv:extension-coveralls] 75 | deps = 76 | cpp-coveralls 77 | usedevelop = true 78 | commands = 79 | coveralls --build-root=. --include=src --dump=extension-coveralls.json 80 | 81 | [testenv:report] 82 | basepython = python3.4 83 | commands = 84 | coverage combine 85 | coverage report 86 | usedevelop = true 87 | deps = coverage 88 | 89 | [testenv:clean] 90 | commands = coverage erase 91 | usedevelop = true 92 | deps = coverage 93 | 94 | {% for env, config in tox_environments|dictsort %} 95 | [testenv:{{ env }}] 96 | basepython = {{ config.python }} 97 | {% if config.cover or config.env_vars %} 98 | setenv = 99 | {[testenv]setenv} 100 | {% endif %} 101 | {% for var in config.env_vars %} 102 | {{ var }} 103 | {% endfor %} 104 | {% if config.cover %} 105 | WITH_COVERAGE=yes 106 | CFLAGS=-coverage 107 | usedevelop = true 108 | commands = 109 | python setup.py clean --all build_ext --force --inplace 110 | {posargs:py.test --cov=src --cov-report=term-missing -vv} 111 | {% endif %} 112 | {% if config.cover or config.deps %} 113 | deps = 114 | {[testenv]deps} 115 | {% endif %} 116 | {% if config.cover %} 117 | pytest-cov{% endif %} 118 | {% for dep in config.deps %} 119 | {{ dep }} 120 | {% endfor %} 121 | 122 | {% endfor %} 123 | 124 | 125 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | ; a generative tox configuration, see: https://testrun.org/tox/latest/config.html#generative-envlist 2 | 3 | [tox] 4 | envlist = 5 | clean, 6 | check, 7 | 2.7, 8 | ; {2.6,2.7,3.3,3.4,pypy}, 9 | 2.7-nocover, 10 | ; {2.6,2.7,3.3,3.4,pypy}-nocover, 11 | report, 12 | docs 13 | 14 | [flake8] 15 | ignore = E111 16 | max-line-length = 160 17 | 18 | [testenv] 19 | basepython = 20 | {2.7,docs}: python2.7 21 | {clean,check,report,extension-coveralls,coveralls}: python2.7 22 | setenv = 23 | PYTHONPATH={toxinidir}/tests 24 | PYTHONUNBUFFERED=yes 25 | deps = 26 | pytest 27 | pytest-capturelog 28 | pytest-cov 29 | mock 30 | commands = 31 | python setup.py clean --all build_ext --force --inplace 32 | {posargs:py.test --cov=src --cov-report=term-missing -vv} 33 | usedevelop = true 34 | 35 | [testenv:spell] 36 | setenv = 37 | SPELLCHECK=1 38 | commands = 39 | sphinx-build -b spelling docs dist/docs 40 | usedevelop = true 41 | deps = 42 | -r{toxinidir}/docs/requirements.txt 43 | sphinxcontrib-spelling 44 | pyenchant 45 | 46 | [testenv:docs] 47 | whitelist_externals = 48 | rm 49 | commands = 50 | sphinx-build {posargs:-E} -b html docs dist/docs 51 | sphinx-build -b linkcheck docs dist/docs 52 | usedevelop = true 53 | deps = 54 | -r{toxinidir}/docs/requirements.txt 55 | 56 | [testenv:check] 57 | basepython = python2.7 58 | deps = 59 | docutils 60 | check-manifest 61 | flake8 62 | readme 63 | pygments 64 | usedevelop = true 65 | commands = 66 | python setup.py check --strict --metadata --restructuredtext 67 | check-manifest {toxinidir} 68 | flake8 src 69 | 70 | [testenv:coveralls] 71 | deps = 72 | coveralls 73 | usedevelop = true 74 | commands = 75 | coverage combine 76 | coverage report 77 | coveralls 78 | 79 | [testenv:extension-coveralls] 80 | deps = 81 | cpp-coveralls 82 | usedevelop = true 83 | commands = 84 | coveralls --build-root=. --include=src --dump=extension-coveralls.json 85 | [testenv:report] 86 | basepython = python2.7 87 | commands = 88 | coverage combine 89 | coverage report 90 | usedevelop = true 91 | deps = coverage 92 | 93 | [testenv:clean] 94 | commands = coverage erase 95 | usedevelop = true 96 | deps = coverage 97 | 98 | 99 | [testenv:2.7-nocover] 100 | commands = 101 | python setup.py clean --all build_ext --force 102 | {posargs:py.test -vv --ignore=src} 103 | usedevelop = false 104 | 105 | [testenv:3.3-nocover] 106 | commands = 107 | python setup.py clean --all build_ext --force 108 | {posargs:py.test -vv --ignore=src} 109 | usedevelop = false 110 | 111 | [testenv:3.4-nocover] 112 | commands = 113 | python setup.py clean --all build_ext --force 114 | {posargs:py.test -vv --ignore=src} 115 | usedevelop = false 116 | 117 | [testenv:pypy-nocover] 118 | commands = 119 | python setup.py clean --all build_ext --force 120 | {posargs:py.test -vv --ignore=src} 121 | usedevelop = false 122 | 123 | -------------------------------------------------------------------------------- /tests/test_knitting_plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva 18 | import pytest 19 | 20 | import knitlib 21 | from knitlib import machine_handler 22 | from knitlib.plugins import knitting_plugin 23 | from knitlib.plugins import dummy_plugin 24 | 25 | 26 | def test_methods_exception(): 27 | """Tests that abstract methods from BaseKnittingPlugin throw exceptions. 28 | 29 | All methods from BaseKnittingPlugin should fail. Methods from this class 30 | are not implemented, as this is a base, abstract class.""" 31 | 32 | knit_machine = knitting_plugin.BaseKnittingPlugin() 33 | with pytest.raises(NotImplementedError): 34 | knit_machine.configure() 35 | with pytest.raises(NotImplementedError): 36 | knit_machine.knit() 37 | with pytest.raises(NotImplementedError): 38 | knit_machine.finish() 39 | 40 | def test_dummy_plugin(): 41 | """Tests that dummy plugin flows as expected in ideal conditions.""" 42 | knit_machine = dummy_plugin.DummyKnittingPlugin() 43 | knit_machine.configure(None) 44 | knit_machine.knit() 45 | knit_machine.finish() 46 | 47 | def test_machine_handler_get_machines(): 48 | for machine_type in list(machine_handler.get_machine_types()): 49 | assert machine_handler.get_machines_by_type(machine_type) == knitlib.plugins.active_plugins.get(machine_type, {}) 50 | 51 | 52 | def test_machine_handler_get_machine_by_id(): 53 | machine = knitlib.machine_handler.get_machine_plugin_by_id(dummy_plugin.DummyKnittingPlugin.__PLUGIN_NAME__) 54 | assert machine.__name__ == "DummyKnittingPlugin" 55 | 56 | def test_dummy_machine(): 57 | 58 | mach_type = knitlib.machine_handler.get_machine_types().other 59 | other_type_dict = knitlib.machine_handler.get_machines_by_type(mach_type) 60 | assert type(other_type_dict) is dict 61 | dummy_type = other_type_dict["dummy"]() 62 | 63 | dummy_type.configure(None) 64 | dummy_type.knit() 65 | dummy_type.finish() 66 | 67 | 68 | def test_ayab_plugin(): 69 | pass 70 | # machine = knitlib.machine_handler.get_machine_plugin_by_id("AYAB")() 71 | 72 | #dummy_type.configure(None) 73 | #dummy_type.knit() # https://bitbucket.org/chris007de/ayab-apparat/wiki/english/Hardware#!nomachine-development-mode 74 | #if dummy_type.isstate("finished"): 75 | # dummy_type.finish()+ 76 | -------------------------------------------------------------------------------- /src/knitlib/plugins/pdd_plugin/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'tian' 2 | 3 | import os 4 | import shutil 5 | # import fs 6 | import sys 7 | import logging 8 | import tempfile 9 | import PDDemulate 10 | from PDDemulate import PDDemulator 11 | from knitlib.plugins.knitting_plugin import BaseKnittingPlugin 12 | import knitlib.exceptions 13 | 14 | 15 | class PDDEmulationKnittingPlugin(BaseKnittingPlugin): 16 | 17 | __PLUGIN_NAME__ = u"PDD" 18 | 19 | def __init__(self, callbacks_dict=None, interactive_callbacks=None): 20 | super(PDDEmulationKnittingPlugin, self).__init__(callbacks_dict, interactive_callbacks) 21 | logging.debug("Loaded PDD emulator version: {}".format(PDDemulate.version)) 22 | self.__port = "" 23 | self.__conf = None 24 | self.__main_temp_dir = tempfile.gettempdir() 25 | self.__folder = os.path.join(self.__main_temp_dir, "PDD_tmp_dir") 26 | self.__emu = None 27 | # TODO: init emulator 28 | 29 | def register_interactive_callbacks(self, callbacks=None): 30 | super(PDDEmulationKnittingPlugin, self).register_interactive_callbacks(callbacks) 31 | 32 | def set_port(self, portname): 33 | self.__port = portname 34 | 35 | def onconfigure(self, e): 36 | if hasattr(e, "conf") and e.conf is not None: 37 | self.__conf = e.conf 38 | else: 39 | raise knitlib.exceptions.InputException("Conf dict is missing") 40 | 41 | conf = self.__conf 42 | if self.__port is "": 43 | self.__port = conf.get("port", "/dev/ttyUSB0") 44 | 45 | try: 46 | os.mkdir(self.__folder) 47 | except OSError: 48 | # Exception thrown meaning the folder already exists 49 | logging.warning("PDD temp folder already exists") 50 | 51 | # Copying the image file to temp folder. 52 | try: 53 | shutil.copy(conf.get("file_url"), self.__folder) 54 | except IOError: 55 | logging.error("Error when copying file url") 56 | 57 | self.__emu = PDDemulator(self.__folder) 58 | 59 | def onknit(self, e): 60 | self.__emu.open(cport=self.__port) 61 | logging.info("PDD Emulator Ready") 62 | self.__emu.handleRequests() 63 | 64 | def validate_configuration(self, conf): 65 | # TODO validate formally 66 | return True 67 | 68 | def onfinish(self, e): 69 | # TODO: remove and cleanup dir at self.__folder 70 | self.__emu.close() 71 | 72 | @staticmethod 73 | def supported_config_features(): 74 | return {"$schema": "http://json-schema.org/schema#", "type": "object"} 75 | 76 | 77 | if __name__ == "__main__": 78 | if len(sys.argv) < 3: 79 | print 'Usage: %s basedir serialdevice' % sys.argv[0] 80 | sys.exit() 81 | 82 | print 'Preparing . . . Please Wait' 83 | emu = PDDemulator(sys.argv[1]) 84 | 85 | # TODO: fix usb port hardcoding 86 | emu.open(cport=sys.argv[2]) 87 | 88 | print 'Emulator Ready!' 89 | try: 90 | while 1: 91 | emu.handleRequests() 92 | except (KeyboardInterrupt): 93 | pass 94 | 95 | emu.close() 96 | -------------------------------------------------------------------------------- /ci/appveyor-bootstrap.ps1: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 2 | # Sample script to install Python and pip under Windows 3 | # Authors: Olivier Grisel and Kyle Kastner 4 | # License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ 5 | 6 | $BASE_URL = "https://www.python.org/ftp/python/" 7 | $GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" 8 | $GET_PIP_PATH = "C:\get-pip.py" 9 | 10 | 11 | function DownloadPython ($python_version, $platform_suffix) { 12 | $webclient = New-Object System.Net.WebClient 13 | $filename = "python-" + $python_version + $platform_suffix + ".msi" 14 | $url = $BASE_URL + $python_version + "/" + $filename 15 | 16 | $basedir = $pwd.Path + "\" 17 | $filepath = $basedir + $filename 18 | if (Test-Path $filename) { 19 | Write-Host "Reusing" $filepath 20 | return $filepath 21 | } 22 | 23 | # Download and retry up to 5 times in case of network transient errors. 24 | Write-Host "Downloading" $filename "from" $url 25 | $retry_attempts = 3 26 | for($i=0; $i -lt $retry_attempts; $i++){ 27 | try { 28 | $webclient.DownloadFile($url, $filepath) 29 | break 30 | } 31 | Catch [Exception]{ 32 | Start-Sleep 1 33 | } 34 | } 35 | Write-Host "File saved at" $filepath 36 | return $filepath 37 | } 38 | 39 | 40 | function InstallPython ($python_version, $architecture, $python_home) { 41 | Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home 42 | if (Test-Path $python_home) { 43 | Write-Host $python_home "already exists, skipping." 44 | return $false 45 | } 46 | if ($architecture -eq "32") { 47 | $platform_suffix = "" 48 | } else { 49 | $platform_suffix = ".amd64" 50 | } 51 | $filepath = DownloadPython $python_version $platform_suffix 52 | Write-Host "Installing" $filepath "to" $python_home 53 | $args = "/qn /i $filepath TARGETDIR=$python_home" 54 | Write-Host "msiexec.exe" $args 55 | Start-Process -FilePath "msiexec.exe" -ArgumentList $args -Wait -Passthru 56 | Write-Host "Python $python_version ($architecture) installation complete" 57 | return $true 58 | } 59 | 60 | 61 | function InstallPip ($python_home) { 62 | $pip_path = $python_home + "/Scripts/pip.exe" 63 | $python_path = $python_home + "/python.exe" 64 | if (-not(Test-Path $pip_path)) { 65 | Write-Host "Installing pip..." 66 | $webclient = New-Object System.Net.WebClient 67 | $webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH) 68 | Write-Host "Executing:" $python_path $GET_PIP_PATH 69 | Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru 70 | } else { 71 | Write-Host "pip already installed." 72 | } 73 | } 74 | 75 | function InstallPackage ($python_home, $pkg) { 76 | $pip_path = $python_home + "/Scripts/pip.exe" 77 | & $pip_path install $pkg 78 | } 79 | 80 | function main () { 81 | InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON_HOME 82 | InstallPip $env:PYTHON_HOME 83 | InstallPackage $env:PYTHON_HOME setuptools 84 | InstallPackage $env:PYTHON_HOME wheel 85 | InstallPackage $env:PYTHON_HOME tox 86 | } 87 | 88 | main 89 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{branch}-{build}' 2 | build: off 3 | environment: 4 | global: 5 | WITH_COMPILER: "cmd /E:ON /V:ON /C .\\ci\\appveyor-with-compiler.cmd" 6 | matrix: 7 | - TOXENV: check 8 | PYTHON_HOME: "C:\\Python27" 9 | PYTHON_VERSION: "2.7" 10 | PYTHON_ARCH: "32" 11 | - TOXENV: "2.7" 12 | TOXPYTHON: "C:\\Python27\\python.exe" 13 | WINDOWS_SDK_VERSION: "v7.0" 14 | PYTHON_HOME: "C:\\Python27" 15 | PYTHON_VERSION: "2.7" 16 | PYTHON_ARCH: "32" 17 | - TOXENV: "2.7" 18 | TOXPYTHON: "C:\\Python27-x64\\python.exe" 19 | WINDOWS_SDK_VERSION: "v7.0" 20 | PYTHON_HOME: "C:\\Python27-x64" 21 | PYTHON_VERSION: "2.7" 22 | PYTHON_ARCH: "64" 23 | - TOXENV: "2.7-nocover" 24 | TOXPYTHON: "C:\\Python27\\python.exe" 25 | WINDOWS_SDK_VERSION: "v7.0" 26 | PYTHON_HOME: "C:\\Python27" 27 | PYTHON_VERSION: "2.7" 28 | PYTHON_ARCH: "32" 29 | - TOXENV: "2.7-nocover" 30 | TOXPYTHON: "C:\\Python27-x64\\python.exe" 31 | WINDOWS_SDK_VERSION: "v7.0" 32 | PYTHON_HOME: "C:\\Python27-x64" 33 | PYTHON_VERSION: "2.7" 34 | PYTHON_ARCH: "64" 35 | - TOXENV: "3.3" 36 | TOXPYTHON: "C:\\Python33\\python.exe" 37 | WINDOWS_SDK_VERSION: "v7.1" 38 | PYTHON_HOME: "C:\\Python33" 39 | PYTHON_VERSION: "3.3" 40 | PYTHON_ARCH: "32" 41 | - TOXENV: "3.3" 42 | TOXPYTHON: "C:\\Python33-x64\\python.exe" 43 | WINDOWS_SDK_VERSION: "v7.1" 44 | PYTHON_HOME: "C:\\Python33-x64" 45 | PYTHON_VERSION: "3.3" 46 | PYTHON_ARCH: "64" 47 | - TOXENV: "3.3-nocover" 48 | TOXPYTHON: "C:\\Python33\\python.exe" 49 | WINDOWS_SDK_VERSION: "v7.1" 50 | PYTHON_HOME: "C:\\Python33" 51 | PYTHON_VERSION: "3.3" 52 | PYTHON_ARCH: "32" 53 | - TOXENV: "3.3-nocover" 54 | TOXPYTHON: "C:\\Python33-x64\\python.exe" 55 | WINDOWS_SDK_VERSION: "v7.1" 56 | PYTHON_HOME: "C:\\Python33-x64" 57 | PYTHON_VERSION: "3.3" 58 | PYTHON_ARCH: "64" 59 | - TOXENV: "3.4" 60 | TOXPYTHON: "C:\\Python34\\python.exe" 61 | WINDOWS_SDK_VERSION: "v7.1" 62 | PYTHON_HOME: "C:\\Python34" 63 | PYTHON_VERSION: "3.4" 64 | PYTHON_ARCH: "32" 65 | - TOXENV: "3.4" 66 | TOXPYTHON: "C:\\Python34-x64\\python.exe" 67 | WINDOWS_SDK_VERSION: "v7.1" 68 | PYTHON_HOME: "C:\\Python34-x64" 69 | PYTHON_VERSION: "3.4" 70 | PYTHON_ARCH: "64" 71 | - TOXENV: "3.4-nocover" 72 | TOXPYTHON: "C:\\Python34\\python.exe" 73 | WINDOWS_SDK_VERSION: "v7.1" 74 | PYTHON_HOME: "C:\\Python34" 75 | PYTHON_VERSION: "3.4" 76 | PYTHON_ARCH: "32" 77 | - TOXENV: "3.4-nocover" 78 | TOXPYTHON: "C:\\Python34-x64\\python.exe" 79 | WINDOWS_SDK_VERSION: "v7.1" 80 | PYTHON_HOME: "C:\\Python34-x64" 81 | PYTHON_VERSION: "3.4" 82 | PYTHON_ARCH: "64" 83 | init: 84 | - "ECHO %TOXENV%" 85 | - ps: "ls C:\\Python*" 86 | install: 87 | - "powershell ci\\appveyor-bootstrap.ps1" 88 | test_script: 89 | - "%PYTHON_HOME%\\Scripts\\tox --version" 90 | - "%PYTHON_HOME%\\Scripts\\virtualenv --version" 91 | - "%PYTHON_HOME%\\Scripts\\pip --version" 92 | - "%WITH_COMPILER% %PYTHON_HOME%\\Scripts\\tox" 93 | after_test: 94 | - "IF \"%TOXENV:~-8,8%\" == \"-nocover\" %WITH_COMPILER% %TOXPYTHON% setup.py bdist_wheel" 95 | artifacts: 96 | - path: dist\* 97 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | Bug reports 9 | =========== 10 | 11 | When `reporting a bug `_ please include: 12 | 13 | * Your operating system name and version. 14 | * Any details about your local setup that might be helpful in troubleshooting. 15 | * Detailed steps to reproduce the bug. 16 | 17 | Documentation improvements 18 | ========================== 19 | 20 | knitlib could always use more documentation, whether as part of the 21 | official knitlib docs, in docstrings, or even on the web in blog posts, 22 | articles, and such. 23 | 24 | Feature requests, bug reports, and feedback 25 | =========================================== 26 | 27 | The best way to send feedback is to file an issue at https://github.com/fashiontec/knitlib/issues. 28 | 29 | If you are proposing a feature: 30 | 31 | * Explain in detail how it would work. 32 | * Keep the scope as narrow as possible, to make it easier to implement. 33 | * Remember that this is a volunteer-driven project, and that contributions are welcome :) 34 | 35 | Development 36 | =========== 37 | 38 | To set up `knitlib` for local development: 39 | 40 | 1. `Fork knitlib on GitHub `_. 41 | 2. Clone your fork locally:: 42 | 43 | git clone git@github.com:your_name_here/knitlib.git 44 | 45 | 3. Create a branch for local development:: 46 | 47 | git checkout -b name-of-your-bugfix-or-feature 48 | 49 | Now you can make your changes locally. 50 | 51 | 4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: 52 | 53 | tox 54 | 55 | 5. Commit your changes and push your branch to GitHub:: 56 | 57 | git add . 58 | git commit -m "Your detailed description of your changes." 59 | git push origin name-of-your-bugfix-or-feature 60 | 61 | 6. Submit a pull request through the GitHub website. 62 | 7. Assign some one to review the code, somebody who is familiar with the architecture. Or some of the other students involved. 63 | 8. Handle the code changes required by the reviewer, discuss alternative ways etc. 64 | 9. Clean up, reduce commits for the entire stuff done via the PR (Squashing Commits - http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 65 | 11. Ensure it runs tests. 66 | 10. Once the PR (Pull Request) is green (reviewer says OK or LGTM), merge the PR and delete the branch. 67 | 68 | Pull Request Guidelines 69 | ----------------------- 70 | 71 | If you need some code review or feedback while you're developing the code just make the pull request. 72 | 73 | For merging, you should: 74 | 75 | 1. Include passing tests (run ``tox``) [1]_. 76 | 2. Update documentation when there's new API, functionality etc. 77 | 3. Add a note to ``CHANGELOG.rst`` about the changes. 78 | 4. Add yourself to ``AUTHORS.rst``. 79 | 80 | .. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will 81 | `run the tests `_ for each change you add in the pull request. 82 | 83 | It will be slower though ... 84 | 85 | Tips 86 | ---- 87 | 88 | * To run a subset of tests:: 89 | 90 | tox -e envname -- py.test -k test_myfeature 91 | 92 | * To run all the test environments in *parallel* (you need to ``pip install detox``):: 93 | 94 | detox 95 | 96 | * Please check the `Google Python coding guidelines `_, it has good advices in order to write properly structured code. 97 | -------------------------------------------------------------------------------- /tests/test_ayab_communication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of AYAB. 3 | # 4 | # AYAB is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # AYAB is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with AYAB. If not, see . 16 | # 17 | # Copyright 2015 Shiluka Dharmasena, Sebastian Oliva, Christian Obersteiner, Andreas Müller 18 | # https://bitbucket.org/chris007de/ayab-apparat/ 19 | 20 | import pytest 21 | import serial 22 | import unittest 23 | from knitlib.exceptions import CommunicationException 24 | from knitlib.plugins.ayab_plugin.ayab_communication import AyabCommunication 25 | from mock import patch 26 | 27 | class TestCommunication(unittest.TestCase): 28 | 29 | def setUp(self): 30 | self.dummy_serial = serial.serial_for_url("loop://logging=debug") 31 | self.comm_dummy = AyabCommunication(self.dummy_serial) 32 | 33 | def test_close_serial(self): 34 | """Test on closing serial port communication.""" 35 | before = self.dummy_serial.isOpen() 36 | assert before 37 | self.comm_dummy.close_serial() 38 | after = self.dummy_serial.isOpen() 39 | assert after == False 40 | 41 | def test_open_serial(self): 42 | """Test on opening serial port communication with a baudrate 115200.""" 43 | with patch.object(serial,'Serial') as mock_method: 44 | mock_method.return_value = object() 45 | self.ayabCom = AyabCommunication() 46 | openStatus = self.ayabCom.open_serial('dummyPortname') 47 | assert openStatus 48 | mock_method.assert_called_once_with('dummyPortname',115200) 49 | 50 | with patch.object(serial,'Serial') as mock_method: 51 | with pytest.raises(CommunicationException) as excinfo: 52 | mock_method.side_effect = serial.SerialException() 53 | self.ayabCom = AyabCommunication() 54 | openStatus = self.ayabCom.open_serial('dummyPortname') 55 | mock_method.assert_called_once_with('dummyPortname',115200) 56 | 57 | def test_req_start(self): 58 | """Test on sending a start message to the controller.""" 59 | start_val, end_val = 0, 10 60 | self.comm_dummy.req_start(start_val, end_val) 61 | byte_array = bytearray([0x01, start_val, end_val, 0x0a, 0x0d]) 62 | bytes_read = self.dummy_serial.read(len(byte_array)) 63 | self.assertEqual(bytes_read, byte_array) 64 | 65 | def test_req_info(self): 66 | """Test on Sending a request for information to controller.""" 67 | self.comm_dummy.req_info() 68 | byte_array = bytearray([0x03, 0x0a, 0x0d]) 69 | bytes_read = self.dummy_serial.read(len(byte_array)) 70 | assert bytes_read == byte_array 71 | 72 | def test_cnf_line(self): 73 | """Test on sending a line of data via the serial port.""" 74 | lineNumber = 13 75 | lineData = chr(0xAB) 76 | flags = 0x12 77 | crc8 = 0x57 78 | self.comm_dummy.cnf_line(lineNumber, lineData, flags, crc8) 79 | byte_array = bytearray([0x42, lineNumber, lineData, flags, crc8, 0x0a, 0x0d]) 80 | bytes_read = self.dummy_serial.read(len(byte_array)) 81 | assert bytes_read == byte_array 82 | 83 | def test_read_line(self): 84 | """Test on reading a line from serial communication.""" 85 | byte_start_val = 0x01 86 | byte_end_val = 0x12 87 | byte_array = bytearray([0xC1, byte_start_val, byte_end_val, 0x0a, 0x0d]) 88 | bytes_wrote = self.dummy_serial.write(byte_array) 89 | bytes_read = self.comm_dummy.read_line() 90 | assert bytes_read == byte_array -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | from __future__ import absolute_import, print_function 4 | 5 | import io 6 | import os 7 | import re 8 | from glob import glob 9 | from os.path import basename 10 | from os.path import dirname 11 | from os.path import join 12 | from os.path import relpath 13 | from os.path import splitext 14 | 15 | from setuptools import find_packages 16 | from setuptools import setup 17 | from setuptools.command.build_ext import build_ext 18 | from distutils.core import Extension 19 | from distutils.errors import CCompilerError 20 | from distutils.errors import CompileError 21 | from distutils.errors import DistutilsExecError 22 | from distutils.errors import DistutilsPlatformError 23 | 24 | def read(*names, **kwargs): 25 | return io.open( 26 | join(dirname(__file__), *names), 27 | encoding=kwargs.get('encoding', 'utf8') 28 | ).read() 29 | 30 | 31 | class optional_build_ext(build_ext): 32 | '''Allow the building of C extensions to fail.''' 33 | def run(self): 34 | try: 35 | build_ext.run(self) 36 | except DistutilsPlatformError as e: 37 | self._unavailable(e) 38 | self.extensions = [] # avoid copying missing files (it would fail). 39 | 40 | def build_extension(self, ext): 41 | try: 42 | build_ext.build_extension(self, ext) 43 | except (CCompilerError, CompileError, DistutilsExecError) as e: 44 | self._unavailable(e) 45 | self.extensions = [] # avoid copying missing files (it would fail). 46 | 47 | def _unavailable(self, e): 48 | print('*' * 80) 49 | print('''WARNING: 50 | 51 | An optional code optimization (C extension) could not be compiled. 52 | 53 | Optimizations for this package will not be available! 54 | ''') 55 | 56 | print('CAUSE:') 57 | print('') 58 | print(' ' + repr(e)) 59 | print('*' * 80) 60 | 61 | 62 | setup( 63 | name='knitlib', 64 | version='0.0.1', 65 | license='GPLv3', 66 | description='A library designed to support varied knitting machines.', 67 | long_description='%s\n%s' % (read('README.rst'), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst'))), 68 | author='Sebastian Oliva , Shiluka Dharmasena', 69 | author_email='code@sebastianoliva.com , shiluka@gmail.com', 70 | url='https://github.com/fashiontec/knitlib', 71 | packages=find_packages('src'), 72 | package_dir={'': 'src'}, 73 | py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], 74 | include_package_data=True, 75 | zip_safe=False, 76 | classifiers=[ 77 | # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 78 | 'Development Status :: 5 - Production/Stable', 79 | 'Intended Audience :: Developers', 80 | 'License :: OSI Approved :: BSD License', 81 | 'Operating System :: Unix', 82 | 'Operating System :: POSIX', 83 | 'Operating System :: Microsoft :: Windows', 84 | 'Programming Language :: Python', 85 | 'Programming Language :: Python :: 2.6', 86 | 'Programming Language :: Python :: 2.7', 87 | 'Programming Language :: Python :: 3', 88 | 'Programming Language :: Python :: 3.3', 89 | 'Programming Language :: Python :: 3.4', 90 | 'Programming Language :: Python :: Implementation :: CPython', 91 | 'Programming Language :: Python :: Implementation :: PyPy', 92 | 'Topic :: Utilities', 93 | ], 94 | keywords=[ 95 | # eg: 'keyword1', 'keyword2', 'keyword3', 96 | ], 97 | install_requires=[ 98 | 'click', 99 | 'fysom', 100 | 'pyserial', 101 | 'enum34', 102 | 'Pillow', 103 | 'jsonschema', 104 | 'fs' 105 | ], 106 | extras_require={ 107 | # eg: 'rst': ['docutils>=0.11'], 108 | }, 109 | entry_points={ 110 | 'console_scripts': [ 111 | 'knitlib = knitlib.__main__:main', 112 | ] 113 | }, 114 | cmdclass={'build_ext': optional_build_ext}, 115 | ext_modules=[ 116 | Extension( 117 | splitext(relpath(path, 'src').replace(os.sep, '.'))[0], 118 | sources=[path], 119 | include_dirs=[dirname(path)] 120 | ) 121 | for root, _, _ in os.walk('src') 122 | for path in glob(join(root, '*.c')) 123 | ] 124 | ) 125 | -------------------------------------------------------------------------------- /src/knitlib/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva, Shiluka Dharmasena 18 | 19 | 20 | class KnittingError(Exception): 21 | """Base class for exceptions in this module.""" 22 | def __init__(self, expression="", message=""): 23 | self.expression = expression 24 | self.message = message 25 | 26 | 27 | class InputException(KnittingError): 28 | """Exception raised for errors in the input. 29 | Attributes: 30 | expression -- input expression in which 31 | the error occurred 32 | message -- explanation of the error 33 | """ 34 | pass 35 | 36 | 37 | class CommunicationException(KnittingError): 38 | """Exception raised for errors in the communication. 39 | Attributes: 40 | expression -- communication expression in which 41 | the error occurred 42 | message -- explanation of the error 43 | """ 44 | pass 45 | 46 | 47 | class PatternNotFoundException(KnittingError): 48 | """Exception raised for errors in the pattern. 49 | Attributes: 50 | expression -- pattern expression in which 51 | the error occurred 52 | message -- explanation of the error 53 | """ 54 | pass 55 | 56 | 57 | class InsertPatternException(KnittingError): 58 | """Exception raised for errors in the insert of the pattern. 59 | Attributes: 60 | expression -- insert pattern expression in which 61 | the error occurred 62 | message -- explanation of the error 63 | """ 64 | pass 65 | 66 | 67 | class SendDataException(KnittingError): 68 | """Exception raised for errors in sending data to the serial port. 69 | Attributes: 70 | expression -- send data expression in which 71 | the error occurred 72 | message -- explanation of the error 73 | """ 74 | pass 75 | 76 | 77 | class OpenSerialException(KnittingError): 78 | """Exception raised for errors in opening the serial port. 79 | Attributes: 80 | expression -- open serial port expression in which 81 | the error occurred 82 | message -- explanation of the error 83 | """ 84 | pass 85 | 86 | 87 | class CloseSerialException(KnittingError): 88 | """Exception raised for errors in closing the serial port. 89 | Attributes: 90 | expression -- close serial port expression in which 91 | the error occurred 92 | message -- explanation of the error 93 | """ 94 | pass 95 | 96 | 97 | class ReadLineException(KnittingError): 98 | """Exception raised for errors in reading lines. 99 | Attributes: 100 | expression -- send data expression in which 101 | the error occurred 102 | message -- explanation of the error 103 | """ 104 | pass 105 | 106 | 107 | class WriteLineException(KnittingError): 108 | """Exception raised for errors in writing lines. 109 | Attributes: 110 | expression -- write lines expression in which 111 | the error occurred 112 | message -- explanation of the error 113 | """ 114 | pass 115 | 116 | 117 | class ReadByteException(KnittingError): 118 | """Exception raised for errors in reading bytes. 119 | Attributes: 120 | expression -- read data as bytes expression in which 121 | the error occurred 122 | message -- explanation of the error 123 | """ 124 | pass 125 | 126 | 127 | class WriteByteException(KnittingError): 128 | """Exception raised for errors in writing bytes. 129 | Attributes: 130 | expression -- write data as byte expression in which 131 | the error occurred 132 | message -- explanation of the error 133 | """ 134 | pass 135 | -------------------------------------------------------------------------------- /src/knitlib/plugins/ayab_plugin/ayab_communication.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of AYAB. 3 | # 4 | # AYAB is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # AYAB is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with AYAB. If not, see . 16 | # 17 | # Copyright 2013 Christian Obersteiner, Andreas Müller 18 | # https://bitbucket.org/chris007de/ayab-apparat/ 19 | 20 | """Handles the serial communication protocol. 21 | 22 | This module handles serial communication, currently works in a synchronous way. 23 | AyabCommunication uses an internal PySerial.Serial object to connect to the device. 24 | The initializer can also be overriden with a dummy serial object. 25 | """ 26 | 27 | import time 28 | import serial 29 | 30 | import logging 31 | 32 | from knitlib.exceptions import CommunicationException 33 | 34 | 35 | class AyabCommunication(object): 36 | """Class Handling the serial communication protocol.""" 37 | 38 | def __init__(self, serial=None): 39 | """Creates an AyabCommunication object, with an optional serial-like object.""" 40 | logging.basicConfig(level=logging.DEBUG) 41 | self.__logger = logging.getLogger(__name__) 42 | self.__ser = serial 43 | 44 | def __del__(self): 45 | """Handles on delete behaviour closing serial port object.""" 46 | self.close_serial() 47 | 48 | def open_serial(self, serial_port_name=None, serial_port_speed=115200): 49 | """Opens serial port communication with a portName.""" 50 | if not self.__ser: 51 | self.__portname = serial_port_name 52 | try: 53 | self.__ser = serial.Serial(self.__portname, serial_port_speed) 54 | time.sleep(1) 55 | except: 56 | self.__logger.error("could not open serial port " + self.__portname) 57 | raise CommunicationException() 58 | return True 59 | else: 60 | self.__logger.error("could not open serial port ") 61 | raise CommunicationException() 62 | 63 | def close_serial(self): 64 | """Closes serial port.""" 65 | try: 66 | self.__ser.close() 67 | del(self.__ser) 68 | self.__ser = None 69 | except: 70 | # TODO: add message for closing serial failure. 71 | raise CommunicationException() 72 | 73 | def read_line(self): 74 | """Reads a line from serial communication.""" 75 | line = bytes() 76 | if self.__ser: 77 | while self.__ser.inWaiting() > 0: 78 | line += self.__ser.read(1) 79 | return line 80 | 81 | def write_line(self, line): 82 | """Writes a line """ 83 | if self.__ser: 84 | while line.length > 0: 85 | self.__ser.write(line) 86 | return line 87 | 88 | def write_byte(self, byte): 89 | """Writes a byte""" 90 | if self.__ser: 91 | while byte.length > 0: 92 | self.__ser.write(byte) 93 | return byte 94 | 95 | def req_start(self, startNeedle, stopNeedle): 96 | """Sends a start message to the controller.""" 97 | 98 | msg = chr(0x01) # msg id 99 | msg += chr(int(startNeedle)) 100 | msg += chr(int(stopNeedle)) 101 | # print "< reqStart" 102 | self.__ser.write(msg + '\n\r') 103 | 104 | def req_info(self): 105 | """Sends a request for information to controller.""" 106 | # print "< reqInfo" 107 | self.__ser.write(chr(0x03) + '\n\r') 108 | 109 | def cnf_line(self, lineNumber, lineData, flags, crc8): 110 | """Sends a line of data via the serial port. 111 | 112 | Sends a line of data to the serial port, all arguments are mandatory. 113 | The data sent here is parsed by the Arduino controller which sets the 114 | knitting needles accordingly. 115 | 116 | Args: 117 | lineNumber (int): The line number to be sent. 118 | lineData (bytes): The bytearray to be sent to needles. 119 | flags (bytes): The flags sent to the controller. 120 | crc8 (bytes, optional): The CRC-8 checksum for transmission. 121 | 122 | """ 123 | 124 | msg = chr(0x42) # msg id 125 | msg += chr(lineNumber) # line number 126 | msg += lineData # line data 127 | msg += chr(flags) # flags 128 | msg += chr(crc8) # crc8 129 | # print "< cnfLine" 130 | # print lineData 131 | self.__ser.write(msg + '\n\r') 132 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | =============================== 3 | knitlib 4 | =============================== 5 | 6 | .. | |docs| |travis| |appveyor| |coveralls| |landscape| |scrutinizer| 7 | .. | |version| |downloads| |wheel| |supported-versions| |supported-implementations| 8 | 9 | | |travis| |docs| |downloads| |scrutinizer| 10 | 11 | 12 | .. image:: https://badge.waffle.io/fashiontec/knitlib.png?label=ready&title=Ready 13 | :target: https://waffle.io/fashiontec/knitlib 14 | :alt: 'Stories in Ready' 15 | 16 | .. |docs| image:: https://readthedocs.org/projects/knitlib/badge/?style=flat 17 | :target: https://readthedocs.org/projects/knitlib 18 | :alt: Documentation Status 19 | 20 | .. |travis| image:: http://img.shields.io/travis/fashiontec/knitlib/master.png?style=flat 21 | :alt: Travis-CI Build Status 22 | :target: https://travis-ci.org/fashiontec/knitlib 23 | 24 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/tian2992/knitlib?branch=master 25 | :alt: AppVeyor Build Status 26 | :target: https://ci.appveyor.com/project/tian2992/knitlib 27 | 28 | .. |coveralls| image:: http://img.shields.io/coveralls/tian2992/knitlib/master.png?style=flat 29 | :alt: Coverage Status 30 | :target: https://coveralls.io/r/tian2992/knitlib 31 | 32 | .. |landscape| image:: https://landscape.io/github/tian2992/knitlib/master/landscape.svg?style=flat 33 | :target: https://landscape.io/github/tian2992/knitlib/master 34 | :alt: Code Quality Status 35 | 36 | .. |version| image:: http://img.shields.io/pypi/v/knitlib.png?style=flat 37 | :alt: PyPI Package latest release 38 | :target: https://pypi.python.org/pypi/knitlib 39 | 40 | .. |downloads| image:: http://img.shields.io/pypi/dm/knitlib.png?style=flat 41 | :alt: PyPI Package monthly downloads 42 | :target: https://pypi.python.org/pypi/knitlib 43 | 44 | .. |wheel| image:: https://pypip.in/wheel/knitlib/badge.png?style=flat 45 | :alt: PyPI Wheel 46 | :target: https://pypi.python.org/pypi/knitlib 47 | 48 | .. |supported-versions| image:: https://pypip.in/py_versions/knitlib/badge.png?style=flat 49 | :alt: Supported versions 50 | :target: https://pypi.python.org/pypi/knitlib 51 | 52 | .. |supported-implementations| image:: https://pypip.in/implementation/knitlib/badge.png?style=flat 53 | :alt: Supported imlementations 54 | :target: https://pypi.python.org/pypi/knitlib 55 | 56 | .. |scrutinizer| image:: https://img.shields.io/scrutinizer/g/tian2992/knitlib/master.png?style=flat 57 | :alt: Scrutinizer Status 58 | :target: https://scrutinizer-ci.com/g/tian2992/knitlib/ 59 | 60 | 61 | ================== 62 | 1. What is Knitlib 63 | ================== 64 | Knitlib is a library designed to support the operation of varied knitting machines, mechanisms, and hacks. Knitlib is based on projects like AYAB, PDD, and KnitterStream to control knitting machines. Knitlib features a plugin system for knitting machines and implements an API to control machines' operation, knitting jobs and knitting patterns. The software is based on Python. There also is a Web API. Among the primary tasks is to develop plugins based on this solution to add support for more machines. 65 | 66 | 1.1 Idea behind Knitlib 67 | ----------------------- 68 | 69 | 1.2 Technical Background of Knitlib 70 | ----------------------------------- 71 | 72 | Knitlib is implemented as a Python Library and API. Each machine is supported via a Plugin, allowing for extensibility. Each of the plugins is based on a simple Finite State Machine, with states from machine initialization to operation and knitting process. Among the aplications using the Knitlib API is Knitlib-server, implementing a Webserver and REST / WebSocket endpoints. Clients can also implement message callbacks, errors, notifications and blocking messages in order to provide a good user experience. 73 | 74 | ========================== 75 | 2.Development Installation 76 | ========================== 77 | 78 | pip install -r requirements.txt 79 | pip install knitlib 80 | 81 | ======== 82 | 3. Usage 83 | ======== 84 | 85 | ============== 86 | 4. Development 87 | ============== 88 | 89 | To run the all tests run:: 90 | 91 | tox 92 | 93 | ============= 94 | 5. References 95 | ============= 96 | 97 | ================ 98 | 6. Documentation 99 | ================ 100 | 101 | .. https://knitlib.readthedocs.org/ 102 | 103 | =============== 104 | 7. Contributing 105 | =============== 106 | 107 | 7.1. Bug reports 108 | ---------------- 109 | 110 | Bugs can be reported via the Github issues tracker at https://github.com/fashiontec/knitlib/issues 111 | 112 | 7.2 Documentation improvements 113 | ------------------------------ 114 | 115 | 7.3 Feature requests, Issues, and Feedback 116 | ----------------------------------------------- 117 | Issues, feature requests and feedback can be reported via the Github issues tracker at https://github.com/fashiontec/knitlib/issues 118 | 119 | 120 | 121 | 7.4 Pull Request Guidelines 122 | --------------------------- 123 | 124 | 125 | 126 | ============================= 127 | 8. Applications using Knitlib 128 | ============================= 129 | 130 | ========== 131 | 9. License 132 | ========== 133 | 134 | Free software: LGPLv3+ license 135 | -------------------------------------------------------------------------------- /src/knitlib/plugins/knitting_plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of Knitlib. 3 | # 4 | # Knitlib is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # Knitlib is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with Knitlib. If not, see . 16 | # 17 | # Copyright 2015 Sebastian Oliva 18 | 19 | 20 | import abc 21 | import logging 22 | from fysom import Fysom 23 | 24 | 25 | class BaseKnittingPlugin(Fysom): 26 | """A generic plugin implementing a state machine for knitting. 27 | 28 | Subclasses inherit the basic State Machine defined in __init__. 29 | """ 30 | 31 | __NOT_IMPLEMENTED_ERROR = "Classes that inherit from KnittingPlugin should implement {0}" 32 | 33 | @abc.abstractmethod 34 | def onknit(self, e): 35 | """Callback when state machine executes knit(). 36 | 37 | Starts the knitting process, this is the only function call that can block indefinitely, as it is called from an instance 38 | of an individual Thread, allowing for processes that require timing and/or blocking behaviour. 39 | """ 40 | raise NotImplementedError( 41 | self.__NOT_IMPLEMENTED_ERROR.format("onknit. It is used for the main 'knitting loop'.")) 42 | 43 | @abc.abstractmethod 44 | def onfinish(self, e): 45 | """Callback when state machine executes finish(). 46 | 47 | When finish() gets called, the plugin is expected to be able to restore it's state back when configure() gets called. 48 | Finish should trigger a Process Completed notification so the user can operate accordingly. 49 | """ 50 | raise NotImplementedError( 51 | self.__NOT_IMPLEMENTED_ERROR.format("onfinish. It is a callback that is called when knitting is over.")) 52 | 53 | @abc.abstractmethod 54 | def onconfigure(self, e): 55 | """Callback when state machine executes configure(conf={}) 56 | 57 | This state gets called to configure the plugin for knitting. It can either 58 | be called when first configuring the plugin, when an error happened and a 59 | reset is necessary. 60 | 61 | Args: 62 | e: An event object holding a conf dict. 63 | """ 64 | raise NotImplementedError(self.__NOT_IMPLEMENTED_ERROR.format( 65 | "onconfigure. It is used to configure the knitting plugin before starting.")) 66 | 67 | @abc.abstractmethod 68 | def validate_configuration(self, conf): 69 | raise NotImplementedError(self.__NOT_IMPLEMENTED_ERROR.format( 70 | "validate_configuration must be defined. It verifies configurations are valid.")) 71 | 72 | @abc.abstractmethod 73 | def set_port(self, portname): 74 | """Sets a port name before configuration method.""" 75 | raise NotImplementedError(self.__NOT_IMPLEMENTED_ERROR.format( 76 | "set_port must be defined.")) 77 | 78 | def register_interactive_callbacks(self, callbacks=None): 79 | """Serves to register a dict of callbacks that require interaction by the User, 80 | 81 | Interactive callbacks serve to block operation until a human acts on them. Interactive callbacks can include 82 | physical operations (set needles, move knob, flip switch), decisions (yes/no or cancel), or simply human 83 | acknowledgement. 84 | 85 | Args: 86 | callbacks: keys can be info, warning, progress, error. 87 | 88 | """ 89 | if callbacks is None: 90 | callbacks = {} 91 | self.interactive_callbacks = callbacks 92 | 93 | @staticmethod 94 | def __cli_emit_message(message, level="info"): 95 | # TODO: use appropriate logging level for message. 96 | logging.info(message) 97 | 98 | @staticmethod 99 | def __cli_blocking_action(message, level="info"): 100 | """Capturing raw_input to block CLI action.""" 101 | # TODO: use appropriate logging level for message. 102 | logging.info(message) 103 | raw_input() 104 | 105 | @staticmethod 106 | def __cli_log_progress(percent, done, total): 107 | """Logs progress percentage and lines of current job.""" 108 | logging.info("Knitting at {}% . {} out of {}.".format(percent, done, total)) 109 | 110 | @staticmethod 111 | def supported_config_features(): 112 | """Returns a JSON schema dict with the available features for the given machine plugin.""" 113 | raise NotImplementedError(BaseKnittingPlugin.__NOT_IMPLEMENTED_ERROR.format( 114 | "supported_config_features must be defined. It returns a JSON Schema with available configuration options.")) 115 | 116 | def __init__(self, callbacks_dict=None, interactive_callbacks=None): 117 | """Interactive callbacks handle Plugin-Frontend interaction hooks.""" 118 | self.interactive_callbacks = {} 119 | 120 | # Are we running on CLI or knitlib web? 121 | # If no callbacks are registered, we set a CLI set as default. 122 | if interactive_callbacks is None: 123 | self.register_interactive_callbacks({ 124 | "blocking_user_action": BaseKnittingPlugin.__cli_blocking_action, 125 | "message": BaseKnittingPlugin.__cli_emit_message, 126 | "progress": BaseKnittingPlugin.__cli_log_progress 127 | }) 128 | else: 129 | self.register_interactive_callbacks(interactive_callbacks) 130 | 131 | # Fysom allows to set hooks before changing states, we set them here. 132 | if callbacks_dict is None: 133 | callbacks_dict = { 134 | 'onknit': self.onknit, 135 | 'onconfigure': self.onconfigure, 136 | 'onfinish': self.onfinish, 137 | } 138 | Fysom.__init__(self, { 139 | 'initial': 'activated', 140 | 'events': [ # TODO: add more states for handling error management. 141 | {'name': 'configure', 'src': 'activated', 'dst': 'configured'}, 142 | {'name': 'configure', 'src': 'configured', 'dst': 'configured'}, 143 | {'name': 'configure', 'src': 'finished', 'dst': 'configured'}, 144 | {'name': 'configure', 'src': 'error', 'dst': 'configured'}, 145 | {'name': 'knit', 'src': 'configured', 'dst': 'knitting'}, 146 | {'name': 'finish', 'src': 'knitting', 'dst': 'finished'}, 147 | {'name': 'finish', 'src': 'finished', 'dst': 'finished'}, 148 | {'name': 'fail', 'src': 'knitting', 'dst': 'error'}], 149 | 'callbacks': callbacks_dict 150 | }) 151 | -------------------------------------------------------------------------------- /src/knitlib/plugins/pdd_plugin/insertpattern.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2009 Steve Conklin 4 | # steve at conklinhouse dot com 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 19 | 20 | import sys 21 | import brother 22 | from PIL import Image 23 | # import array 24 | 25 | # import convenience functions from brother module 26 | # from brother import roundeven, roundfour, roundeight, nibblesPerRow, bytesPerPattern, bytesForMemo, methodWithPointers 27 | from brother import roundfour, bytesForMemo, methodWithPointers 28 | 29 | TheImage = None 30 | 31 | version = '1.0' 32 | 33 | 34 | class PatternInserter: 35 | def __init__(self): 36 | self.printInfoCallback = self.printInfo 37 | self.printErrorCallback = self.printError 38 | self.printPatternCallback = self.printPattern 39 | 40 | def printInfo(self, printMsg): 41 | print printMsg 42 | 43 | def printError(self, printMsg): 44 | print printMsg 45 | 46 | def printPattern(self, printMsg): 47 | sys.stdout.write(printMsg) 48 | 49 | def insertPattern(self, oldbrotherfile, pattnum, imgfile, newbrotherfile): 50 | 51 | bf = brother.brotherFile(oldbrotherfile) 52 | 53 | pats = bf.getPatterns() 54 | 55 | # ok got a bank, now lets figure out how big this thing we want to insert is 56 | TheImage = Image.open(imgfile) 57 | TheImage.load() 58 | 59 | im_size = TheImage.size 60 | width = im_size[0] 61 | self.printInfoCallback("width:" + str(width)) 62 | height = im_size[1] 63 | self.printInfoCallback("height:" + str(height)) 64 | 65 | # find the program entry 66 | thePattern = None 67 | 68 | for pat in pats: 69 | if (int(pat["number"]) == int(pattnum)): 70 | # print "found it!" 71 | thePattern = pat 72 | if (thePattern is None): 73 | raise PatternNotFoundException(pattnum) 74 | 75 | if (height != thePattern["rows"] or width != thePattern["stitches"]): 76 | raise InserterException("Pattern is the wrong size, the BMP is ", height, "x", width, "and the pattern is ", 77 | thePattern["rows"], "x", thePattern["stitches"]) 78 | 79 | # debugging stuff here 80 | x = 0 81 | y = 0 82 | 83 | x = width - 1 84 | for y in xrange(height): 85 | for x in xrange(width): 86 | value = TheImage.getpixel((x, y)) 87 | if value: 88 | self.printPattern('* ') 89 | else: 90 | self.printPattern(' ') 91 | print " " 92 | 93 | # debugging stuff done 94 | 95 | # now to make the actual, yknow memo+pattern data 96 | 97 | # the memo seems to be always blank. i have no idea really 98 | memoentry = [] 99 | for i in range(bytesForMemo(height)): 100 | memoentry.append(0x0) 101 | 102 | # now for actual real live pattern data! 103 | pattmemnibs = [] 104 | for r in range(height): 105 | row = [] # we'll chunk in bits and then put em into nibbles 106 | for s in range(width): 107 | x = s if methodWithPointers else width - s - 1 108 | value = TheImage.getpixel((x, height - r - 1)) 109 | isBlack = (value == 0) if methodWithPointers else (value != 0) 110 | if (isBlack): 111 | row.append(1) 112 | else: 113 | row.append(0) 114 | # print row 115 | # turn it into nibz 116 | for s in range(roundfour(width) / 4): 117 | n = 0 118 | for nibs in range(4): 119 | # print "row size = ", len(row), "index = ",s*4+nibs 120 | 121 | if (len(row) == (s * 4 + nibs)): 122 | break # padding! 123 | 124 | if (row[s * 4 + nibs]): 125 | n |= 1 << nibs 126 | pattmemnibs.append(n) 127 | # print hex(n), 128 | 129 | if (len(pattmemnibs) % 2): 130 | # odd nibbles, buffer to a byte 131 | pattmemnibs.append(0x0) 132 | 133 | # print len(pattmemnibs), "nibbles of data" 134 | 135 | # turn into bytes 136 | pattmem = [] 137 | for i in range(len(pattmemnibs) / 2): 138 | pattmem.append(pattmemnibs[i * 2] | (pattmemnibs[i * 2 + 1] << 4)) 139 | 140 | # print map(hex, pattmem) 141 | # whew. 142 | 143 | # now to insert this data into the file 144 | # now we have to figure out the -end- of the last pattern is 145 | endaddr = 0x6df 146 | 147 | beginaddr = thePattern["pattend"] 148 | endaddr = beginaddr + bytesForMemo(height) + len(pattmem) 149 | self.printInfoCallback("beginning will be at " + str(hex(beginaddr)) + ", end at " + str(hex(endaddr))) 150 | 151 | # Note - It's note certain that in all cases this collision test is needed. What's happening 152 | # when you write below this address (as the pattern grows downward in memory) in that you begin 153 | # to overwrite the pattern index data that starts at low memory. Since you overwrite the info 154 | # for highest memory numbers first, you may be able to get away with it as long as you don't 155 | # attempt to use higher memories. 156 | # Steve 157 | 158 | if beginaddr <= 0x2B8: 159 | self.printErrorCallback( 160 | "Sorry, this will collide with the pattern entry data since %s is <= 0x2B8!" % hex(beginaddr)) 161 | # exit 162 | 163 | # write the memo and pattern entry from the -end- to the -beginning- (up!) 164 | for i in range(len(memoentry)): 165 | bf.setIndexedByte(endaddr, 0) 166 | endaddr -= 1 167 | 168 | for i in range(len(pattmem)): 169 | bf.setIndexedByte(endaddr, pattmem[i]) 170 | endaddr -= 1 171 | 172 | # push the data to a file 173 | outfile = open(newbrotherfile, 'wb') 174 | 175 | d = bf.getFullData() 176 | outfile.write(d) 177 | outfile.close() 178 | 179 | 180 | class InserterException(Exception): 181 | def getMessage(self): 182 | msg = '' 183 | for arg in self.args: 184 | if msg != '': 185 | msg += ' ' 186 | msg += str(arg) 187 | 188 | return msg 189 | 190 | 191 | class PatternNotFoundException(InserterException): 192 | def __init__(self, patternNumber): 193 | self.patternNumber = patternNumber 194 | 195 | 196 | if __name__ == "__main__": 197 | if len(sys.argv) < 5: 198 | print 'Usage: %s oldbrotherfile pattern# image.bmp newbrotherfile' % sys.argv[0] 199 | sys.exit() 200 | inserter = PatternInserter() 201 | argv = sys.argv 202 | try: 203 | inserter.insertPattern(argv[1], argv[2], argv[3], argv[4]) 204 | except PatternNotFoundException as e: 205 | print 'ERROR: Pattern %d not found' % e.patternNumber 206 | sys.exit(1) 207 | except InserterException as e: 208 | print 'ERROR: ', e.getMessage() 209 | sys.exit(1) 210 | -------------------------------------------------------------------------------- /src/knitlib/plugins/ayab_plugin/ayab_image.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of AYAB. 3 | # 4 | # AYAB is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # AYAB is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with AYAB. If not, see . 16 | # 17 | # Copyright 2013 Christian Obersteiner, Andreas Müller 18 | # https://bitbucket.org/chris007de/ayab-apparat/ 19 | 20 | from PIL import Image 21 | 22 | 23 | class ayabImage(object): 24 | def __init__(self, pil_image, pNumColors=2): 25 | self.__numColors = pNumColors 26 | 27 | self.__imgPosition = 'center' 28 | self.__imgStartNeedle = '0' 29 | self.__imgStopNeedle = '0' 30 | 31 | self.__knitStartNeedle = 0 32 | self.__knitStopNeedle = 199 33 | 34 | self.__startLine = 0 35 | 36 | self.__image = pil_image 37 | 38 | self.__image = self.__image.convert('L') # convert to 1 byte depth 39 | self.__updateImageData() 40 | 41 | def imageIntern(self): 42 | return self.__imageIntern 43 | 44 | def imageExpanded(self): 45 | return self.__imageExpanded 46 | 47 | def imgWidth(self): 48 | return self.__imgWidth 49 | 50 | def imgHeight(self): 51 | return self.__imgHeight 52 | 53 | def knitStartNeedle(self): 54 | return self.__knitStartNeedle 55 | 56 | def knitStopNeedle(self): 57 | return self.__knitStopNeedle 58 | 59 | def imgStartNeedle(self): 60 | return self.__imgStartNeedle 61 | 62 | def imgStopNeedle(self): 63 | return self.__imgStopNeedle 64 | 65 | def imgPosition(self): 66 | return self.__imgPosition 67 | 68 | def startLine(self): 69 | return self.__startLine 70 | 71 | def numColors(self): 72 | return self.__numColors 73 | 74 | def __updateImageData(self): 75 | self.__imgWidth = self.__image.size[0] 76 | self.__imgHeight = self.__image.size[1] 77 | 78 | self.__convertImgToIntern() 79 | self.__calcImgStartStopNeedles() 80 | 81 | def __convertImgToIntern(self): 82 | """Converts an image to AYAB's internal representation, a big 2D array.""" 83 | num_colors = self.__numColors 84 | clr_range = float(256) / num_colors 85 | 86 | imgWidth = self.__imgWidth 87 | imgHeight = self.__imgHeight 88 | 89 | self.__imageIntern = [[0 for i in range(imgWidth)] for j in range(imgHeight)] 90 | self.__imageColors = [[0 for i in range(num_colors)] for j in range(imgHeight)] 91 | self.__imageExpanded = [[0 for i in range(imgWidth)] for j in range(num_colors * imgHeight)] 92 | 93 | # Distill image to x colors 94 | for row in range(0, imgHeight): 95 | for col in range(0, imgWidth): 96 | pxl = self.__image.getpixel((col, row)) 97 | 98 | for color in range(0, num_colors): 99 | lowerBound = int(color * clr_range) 100 | upperBound = int((color + 1) * clr_range) 101 | if pxl >= lowerBound and pxl < upperBound: 102 | # color map 103 | self.__imageIntern[row][col] = color 104 | # amount of bits per color per line 105 | self.__imageColors[row][color] += 1 106 | # colors separated per line 107 | self.__imageExpanded[(num_colors * row) + color][col] = 1 108 | 109 | # print(self.__imageIntern) 110 | # print(self.__imageColors) 111 | # print(self.__imageExpanded) 112 | 113 | def __calcImgStartStopNeedles(self): 114 | if self.__imgPosition == 'center': 115 | needleWidth = (self.__knitStopNeedle - self.__knitStartNeedle) + 1 116 | self.__imgStartNeedle = (self.__knitStartNeedle + needleWidth / 2) - self.__imgWidth / 2 117 | self.__imgStopNeedle = self.__imgStartNeedle + self.__imgWidth - 1 118 | 119 | elif self.__imgPosition == 'left': 120 | self.__imgStartNeedle = self.__knitStartNeedle 121 | self.__imgStopNeedle = self.__imgStartNeedle + self.__imgWidth 122 | 123 | elif self.__imgPosition == 'right': 124 | self.__imgStopNeedle = self.__knitStopNeedle 125 | self.__imgStartNeedle = self.__imgStopNeedle - self.__imgWidth 126 | 127 | elif int(self.__imgPosition) > 0 and int(self.__imgPosition) < 200: 128 | self.__imgStartNeedle = int(self.__imgPosition) 129 | self.__imgStopNeedle = self.__imgStartNeedle + self.__imgWidth 130 | 131 | else: 132 | return False 133 | return True 134 | 135 | def setNumColors(self, pNumColors): 136 | """ 137 | sets the number of colors the be used for knitting 138 | """ 139 | if pNumColors > 1 and pNumColors < 7: 140 | self.__numColors = pNumColors 141 | self.__updateImageData() 142 | return 143 | 144 | def invertImage(self): 145 | """ 146 | invert the pixels of the image 147 | """ 148 | for y in range(0, self.__image.size[1]): 149 | for x in range(0, self.__image.size[0]): 150 | pxl = self.__image.getpixel((x, y)) 151 | self.__image.putpixel((x, y), 255 - pxl) 152 | self.__updateImageData() 153 | return 154 | 155 | def rotateImage(self): 156 | """ 157 | rotate the image 90 degrees clockwise 158 | """ 159 | self.__image = self.__image.rotate(-90) 160 | 161 | self.__updateImageData() 162 | return 163 | 164 | def resizeImage(self, pNewWidth): 165 | """ 166 | resize the image to a given width, keeping the aspect ratio 167 | """ 168 | wpercent = (pNewWidth / float(self.__image.size[0])) 169 | hsize = int((float(self.__image.size[1]) * float(wpercent))) 170 | self.__image = self.__image.resize((pNewWidth, hsize), Image.ANTIALIAS) 171 | 172 | self.__updateImageData() 173 | return 174 | 175 | def setKnitNeedles(self, pKnitStart, pKnitStop): 176 | """ 177 | set the start and stop needle 178 | """ 179 | if (pKnitStart < pKnitStop) \ 180 | and pKnitStart >= 0 \ 181 | and pKnitStop < 200: 182 | self.__knitStartNeedle = pKnitStart 183 | self.__knitStopNeedle = pKnitStop 184 | 185 | self.__updateImageData() 186 | return 187 | 188 | def setImagePosition(self, pImgPosition): 189 | """ 190 | set the position of the pattern 191 | """ 192 | ok = False 193 | if pImgPosition == 'left' \ 194 | or pImgPosition == 'center' \ 195 | or pImgPosition == 'right': 196 | ok = True 197 | elif (int(pImgPosition) >= 0 and int(pImgPosition) < 200): 198 | ok = True 199 | 200 | if ok: 201 | self.__imgPosition = pImgPosition 202 | self.__updateImageData() 203 | return 204 | 205 | def setStartLine(self, pStartLine): 206 | """ 207 | set the line where to start knitting 208 | """ 209 | # Check if StartLine is in valid range (picture height) 210 | if pStartLine >= 0 \ 211 | and pStartLine < self.__image.size[1]: 212 | self.__startLine = pStartLine 213 | return 214 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/knitlib/plugins/pdd_plugin/brother.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2009 Steve Conklin 4 | # steve at conklinhouse dot com 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 19 | 20 | # import sys 21 | # import array 22 | # import os 23 | # import os.path 24 | # import string 25 | # import ctypes 26 | from array import array 27 | 28 | __version__ = '1.0' 29 | 30 | methodWithPointers = False 31 | # uncomment this to use new, more precise method of finding patterns in the dat file, 32 | # based on kh940 format documentation from https://github.com/stg/knittington/blob/master/doc/kh940_format.txt 33 | # (should work with all kh930 and kh940 models) 34 | methodWithPointers = True 35 | # Some file location constants 36 | initPatternOffset = 0x06DF # programmed patterns start here, grow down 37 | currentPatternAddr = 0x07EA # stored in MSN and following byte 38 | currentRowAddr = 0x06FF 39 | nextRowAddr = 0x072F 40 | currentRowNumberAddr = 0x0702 41 | carriageStatusAddr = 0x070F 42 | selectAddr = 0x07EA 43 | 44 | 45 | # various unknowns which are probably something we care about 46 | unknownList = {'0700': 0x0700, '0701': 0x0701, 47 | '0704': 0x0704, '0705': 0x0705, '0706': 0x0706, '0707': 0x0707, 48 | '0708': 0x0708, '0709': 0x0709, '070A': 0x070A, '070B': 0x070B, 49 | '070C': 0x070C, '070D': 0x070D, '070E': 0x070E, '0710': 0x0710, 50 | '0711': 0x0711, '0712': 0x0712, '0713': 0x0713, '0714': 0x0714, 51 | '0715': 0x0715} 52 | 53 | 54 | def nibbles(achar): 55 | # print '0x%02X' % ord(achar) 56 | msn = (ord(achar) & 0xF0) >> 4 57 | lsn = ord(achar) & 0x0F 58 | return msn, lsn 59 | 60 | 61 | def hto(hundreds, tens, ones): 62 | return (100 * hundreds) + (10 * tens) + ones 63 | 64 | 65 | def roundeven(val): 66 | return (val + (val % 2)) 67 | 68 | 69 | def roundeight(val): 70 | if val % 8: 71 | return val + (8 - (val % 8)) 72 | else: 73 | return val 74 | 75 | 76 | def roundfour(val): 77 | if val % 4: 78 | return val + (4 - (val % 4)) 79 | else: 80 | return val 81 | 82 | 83 | def nibblesPerRow(stitches): 84 | # there are four stitches per nibble 85 | # each row is nibble aligned 86 | return (roundfour(stitches) / 4) 87 | 88 | 89 | def bytesPerPattern(stitches, rows): 90 | nibbs = rows * nibblesPerRow(stitches) 91 | bytes = roundeven(nibbs) / 2 92 | return bytes 93 | 94 | 95 | def bytesForMemo(rows): 96 | bytes = roundeven(rows) / 2 97 | return bytes 98 | 99 | 100 | def bytesPerPatternAndMemo(stitches, rows): 101 | patbytes = bytesPerPattern(stitches, rows) 102 | memobytes = bytesForMemo(rows) 103 | return patbytes + memobytes 104 | 105 | 106 | class brotherFile(object): 107 | def __init__(self, fn): 108 | self.dfn = None 109 | self.verbose = False 110 | try: 111 | try: 112 | self.df = open(fn, 'rb+') # YOU MUST HAVE BINARY FORMAT!!! 113 | except IOError: 114 | # for now, read only 115 | raise 116 | # self.df = open(fn, 'w') 117 | except: 118 | print 'Unable to open brother file <%s>' % fn 119 | raise 120 | try: 121 | if methodWithPointers: 122 | self.data = self.df.read(-1) 123 | else: 124 | self.data = self.df.read(2048) 125 | self.df.close() 126 | if len(self.data) == 0: 127 | raise Exception() 128 | except: 129 | if methodWithPointers: 130 | print 'Unable to read 2048 bytes from file <%s>' % fn 131 | else: 132 | print 'Unable to read data from file <%s>' % fn 133 | raise 134 | self.dfn = fn 135 | return 136 | 137 | def __del__(self): 138 | return 139 | 140 | def getIndexedByte(self, index): 141 | return ord(self.data[index]) 142 | 143 | def setIndexedByte(self, index, b): 144 | # python strings are mutable so we 145 | # will convert the string to a char array, poke 146 | # and convert back 147 | dataarray = array('c') 148 | dataarray.fromstring(self.data) 149 | 150 | if self.verbose: 151 | print "* writing ", hex(b), "to", hex(index) 152 | # print dataarray 153 | 154 | # this is the actual edit 155 | dataarray[index] = chr(b) 156 | 157 | # save the new string. sure its not very memory-efficient 158 | # but who cares? 159 | self.data = dataarray.tostring() 160 | 161 | # handy for debugging 162 | def getFullData(self): 163 | return self.data 164 | 165 | def getIndexedNibble(self, offset, nibble): 166 | # nibbles is zero based 167 | bytes = nibble / 2 168 | m, l = nibbles(self.data[offset - bytes]) 169 | if nibble % 2: 170 | return m 171 | else: 172 | return l 173 | 174 | def getRowData(self, pattOffset, stitches, rownumber): 175 | row = array('B') 176 | nibspr = nibblesPerRow(stitches) 177 | startnib = nibspr * rownumber 178 | endnib = startnib + nibspr 179 | 180 | for i in range(startnib, endnib, 1): 181 | nib = self.getIndexedNibble(pattOffset, i) 182 | row.append(nib & 0x01) 183 | stitches = stitches - 1 184 | if stitches: 185 | row.append((nib & 0x02) >> 1) 186 | stitches = stitches - 1 187 | if stitches: 188 | row.append((nib & 0x04) >> 2) 189 | stitches = stitches - 1 190 | if stitches: 191 | row.append((nib & 0x08) >> 3) 192 | stitches = stitches - 1 193 | return row 194 | 195 | def getPatterns(self, patternNumber=None): 196 | """ 197 | Get a list of custom patterns stored in the file, or 198 | information for a single pattern. 199 | Pattern information is stored at the beginning 200 | of the file, with seven bytes per pattern and 201 | 99 possible patterns, numbered 901-999. 202 | Returns: A list of tuples: 203 | patternNumber 204 | stitches 205 | rows 206 | patternOffset 207 | memoOffset 208 | """ 209 | patlist = [] 210 | idx = 0 211 | pptr = initPatternOffset 212 | for pi in range(1, 100): 213 | flag = ord(self.data[idx]) 214 | if self.verbose: 215 | print 'Entry %d, flag is 0x%02X' % (pi, flag) 216 | idx = idx + 1 217 | unknown = ord(self.data[idx]) 218 | idx = idx + 1 219 | rh, rt = nibbles(self.data[idx]) 220 | idx = idx + 1 221 | ro, sh = nibbles(self.data[idx]) 222 | idx = idx + 1 223 | st, so = nibbles(self.data[idx]) 224 | idx = idx + 1 225 | unk, ph = nibbles(self.data[idx]) 226 | idx = idx + 1 227 | pt, po = nibbles(self.data[idx]) 228 | idx = idx + 1 229 | rows = hto(rh, rt, ro) 230 | stitches = hto(sh, st, so) 231 | patno = hto(ph, pt, po) 232 | # we have this entry 233 | if self.verbose: 234 | print ' Pattern %3d: %3d Rows, %3d Stitches - ' % (patno, rows, stitches) 235 | if flag != 0: 236 | # valid entry 237 | if methodWithPointers: 238 | pptr = len(self.data) - 1 - ((flag << 8) + unknown) 239 | memoff = pptr 240 | if self.verbose: 241 | print "Memo #", patno, "offset ", memoff 242 | patoff = pptr - bytesForMemo(rows) 243 | if self.verbose: 244 | print "Pattern #", patno, "offset ", patoff 245 | pptr = pptr - bytesPerPatternAndMemo(stitches, rows) 246 | if self.verbose: 247 | print "Ending offset ", hex(pptr) 248 | if patternNumber: 249 | if patternNumber == patno: 250 | patlist.append( 251 | {'number': patno, 'stitches': stitches, 'rows': rows, 'memo': memoff, 'pattern': patoff, 252 | 'pattend': pptr}) 253 | else: 254 | patlist.append( 255 | {'number': patno, 'stitches': stitches, 'rows': rows, 'memo': memoff, 'pattern': patoff, 256 | 'pattend': pptr}) 257 | else: 258 | break 259 | return patlist 260 | 261 | def getMemo(self): 262 | """ 263 | Return an array containing the memo 264 | information for the pattern currently in memory 265 | """ 266 | patt = self.patternNumber() 267 | if patt > 900: 268 | return self.getPatternMemo(patt) 269 | else: 270 | pass 271 | # rows = 0 # TODO XXXXXXXXX 272 | return [0] 273 | 274 | def patternNumber(self): 275 | sn, pnh = nibbles(self.data[currentPatternAddr]) 276 | pnt, pno = nibbles(self.data[currentPatternAddr + 1]) 277 | pattern = hto(pnh, pnt, pno) 278 | return (pattern) 279 | 280 | def getPatternMemo(self, patternNumber): 281 | """ 282 | Return an array containing the memo 283 | information for a custom pattern. The array 284 | is the same length as the number of rows 285 | in the pattern. 286 | """ 287 | list = self.getPatterns(patternNumber) 288 | if len(list) == 0: 289 | return None 290 | memos = array('B') 291 | memoOff = list[0]['memo'] 292 | rows = list[0]['rows'] 293 | memlen = roundeven(rows) / 2 294 | # memo is padded to en even byte 295 | for i in range(memoOff, memoOff - memlen, -1): 296 | msn, lsn = nibbles(self.data[i]) 297 | memos.append(lsn) 298 | rows = rows - 1 299 | if (rows): 300 | memos.append(msn) 301 | rows = rows - 1 302 | return memos 303 | 304 | def getPattern(self, patternNumber): 305 | """ 306 | Return an array containing the pattern 307 | information for a pattern. 308 | """ 309 | list = self.getPatterns(patternNumber) 310 | if len(list) == 0: 311 | return None 312 | pattern = [] 313 | 314 | patoff = list[0]['pattern'] 315 | rows = list[0]['rows'] 316 | stitches = list[0]['stitches'] 317 | 318 | # print 'patoff = 0x%04X' % patoff 319 | # print 'rows = ', rows 320 | # print 'stitches = ', stitches 321 | for i in range(0, rows): 322 | arow = self.getRowData(patoff, stitches, i) 323 | # print arow 324 | pattern.append(arow) 325 | return pattern 326 | 327 | def displayPattern(self, patternNumber): 328 | """ 329 | Display a user pattern stored in file saved 330 | from the brother knitting machine. Patterns 331 | in memory are stored with the beginning of the 332 | pattern at the highest memory address. 333 | """ 334 | 335 | return 336 | 337 | def rowNumber(self): 338 | sn, rnh = nibbles(self.data[currentRowNumberAddr]) 339 | rnt, rno = nibbles(self.data[currentRowNumberAddr + 1]) 340 | rowno = hto(rnh, rnt, rno) 341 | return (rowno) 342 | 343 | def nextRow(self): 344 | return self.getRowData(nextRowAddr, 200, 0) 345 | 346 | def selectorValue(self): 347 | return ord(self.data[selectAddr]) 348 | 349 | def carriageStatus(self): 350 | return ord(self.data[carriageStatusAddr]) 351 | 352 | def motifData(self): 353 | motiflist = [] 354 | addr = 0x07FB 355 | for i in range(6): 356 | mph, mpt = nibbles(self.data[addr]) 357 | if mph & 8: 358 | mph = mph - 8 359 | side = 'right' 360 | else: 361 | side = 'left' 362 | mpo, foo = nibbles(self.data[addr + 1]) 363 | mch, mct = nibbles(self.data[addr + 2]) 364 | mco, bar = nibbles(self.data[addr + 3]) 365 | pos = hto(mph, mpt, mpo) 366 | cnt = hto(mch, mct, mco) 367 | motiflist.append({'position': pos, 'copies': cnt, 'side': side}) 368 | addr = addr - 3 369 | return motiflist 370 | 371 | def patternPosition(self): 372 | addr = 0x07FE 373 | foo, ph = nibbles(self.data[addr]) 374 | if ph & 8: 375 | ph = ph - 8 376 | side = 'right' 377 | else: 378 | side = 'left' 379 | pt, po = nibbles(self.data[addr + 1]) 380 | pos = hto(ph, pt, po) 381 | 382 | return {'position': pos, 'side': side} 383 | 384 | # these are hardcoded for now 385 | def unknownOne(self): 386 | info = array('B') 387 | for i in range(0x06E0, 0x06E5): 388 | info.append(ord(self.data[i])) 389 | return info 390 | 391 | def unknownMemoRange(self): 392 | info = array('B') 393 | for i in range(0x0731, 0x0787): 394 | info.append(ord(self.data[i])) 395 | return info 396 | 397 | def unknownEndRange(self): 398 | info = array('B') 399 | for i in range(0x07D0, 0x07E9): 400 | info.append(ord(self.data[i])) 401 | return info 402 | 403 | def unknownAddrs(self): 404 | return unknownList.items() 405 | -------------------------------------------------------------------------------- /src/knitlib/plugins/ayab_plugin/ayab_control.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is part of AYAB. 3 | # 4 | # AYAB is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # AYAB is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with AYAB. If not, see . 16 | # 17 | # Copyright 2013, 2014 Sebastian Oliva, Christian Obersteiner, Andreas Müller 18 | # https://bitbucket.org/chris007de/ayab-apparat/ 19 | 20 | from ayab_communication import AyabCommunication 21 | import ayab_image 22 | import time 23 | import logging 24 | import os 25 | from PIL import Image 26 | from knitlib.plugins.knitting_plugin import BaseKnittingPlugin 27 | from knitlib import knitpat 28 | 29 | 30 | class AyabPluginControl(BaseKnittingPlugin): 31 | 32 | __PLUGIN_NAME__ = "AYAB" 33 | 34 | def __init__(self): 35 | super(AyabPluginControl, self).__init__() 36 | # KnittingPlugin.__init__(self) 37 | 38 | # From AYAB's ayab_control 39 | self.__API_VERSION = 0x03 40 | self.__ayabCom = AyabCommunication() 41 | self.conf = {} 42 | self.__portname = "" 43 | 44 | self.__formerRequest = 0 45 | self.__lineBlock = 0 46 | 47 | def __del__(self): 48 | self.__close_serial() 49 | 50 | def onknit(self, e): 51 | logging.debug("called onknit on AyabPluginControl") 52 | self.__knitImage(self.__image, self.conf) 53 | self.finish() 54 | 55 | def onconfigure(self, e): 56 | logging.debug("called onconfigure on AYAB Knitting Plugin") 57 | logging.debug("With args: {}".format(e.args)) 58 | logging.debug("With conf kwarg: {}".format(e.conf)) 59 | 60 | # Start to knit with the bottom first 61 | # pil_image = self.pil_image.rotate(180) 62 | 63 | # conf = e.event.conf 64 | # self.conf = e.event.conf 65 | if hasattr(e, "conf") and e.conf is not None: 66 | if knitpat.validate_dict(e.conf): 67 | conf = e.conf 68 | else: 69 | conf = self.generate_test_configuration() 70 | 71 | else: 72 | conf = self.generate_test_configuration() 73 | 74 | self.conf = conf 75 | logging.debug("Working with final conf: {}".format(e.conf)) 76 | 77 | script_dir = os.path.dirname(os.path.abspath(__file__)) # Temp fix for testing Image opening 78 | pil_image = Image.open(os.path.join(script_dir, conf["file_url"])) 79 | 80 | try: 81 | self.__image = ayab_image.ayabImage(pil_image, self.conf["colors"]) 82 | except: 83 | self.__notify_user("You need to set an image.", "error") 84 | return 85 | 86 | if conf.get("start_needle") and conf.get("stop_needle"): 87 | self.__image.setKnitNeedles(conf.get("start_needle"), conf.get("stop_needle")) 88 | if conf.get("alignment"): 89 | self.__image.setImagePosition(conf.get("alignment")) 90 | if conf.get("start_line"): 91 | self.__image.setStartLine(conf.get("start_line")) 92 | 93 | # Do Knit. 94 | 95 | def onfinish(self, e): 96 | logging.info("Finished Knitting.") 97 | self.__close_serial() 98 | # self.__parent_ui.resetUI() 99 | # self.__parent_ui.emit(QtCore.SIGNAL('updateProgress(int,int,int)'), 0, 0, 0) 100 | 101 | @staticmethod 102 | def supported_config_features(): 103 | return { 104 | "$schema": "http://json-schema.org/schema#", 105 | "type": "object", 106 | "properties": { 107 | "start_needle": {"type": "integer"}, 108 | "stop_needle": {"type": "integer"}, 109 | "start_line": {"type": "integer"}, 110 | "machine_type": {"type": "string", 111 | "enum": ["single", "double"]}, 112 | "alignment": {"type": "string", 113 | "enum": ["left", "center", "right"]}, 114 | "inf_repeat": {"type": "integer"}, 115 | "port": {"type": "string"} 116 | } 117 | } 118 | 119 | def cancel(self): 120 | self._knitImage = False 121 | # self.finish() 122 | 123 | def __close_serial(self): 124 | try: 125 | self.__ayabCom.close_serial() 126 | logging.debug("Closing Serial port successful.") 127 | except: 128 | logging.debug("Closing Serial port failed. Was it ever open?") 129 | 130 | def onerror(self, e): 131 | # TODO add message info from event 132 | logging.error("Error while Knitting.") 133 | self.__close_serial() 134 | 135 | def validate_configuration(self, conf): 136 | if conf.get("start_needle") and conf.get("stop_needle"): 137 | if conf.get("start_needle") > conf.get("stop_needle"): 138 | self.__notify_user("Invalid needle start and end.", "warning") 139 | return False 140 | if conf.get("start_line") > self.__image.imgHeight(): 141 | self.__notify_user("Start Line is larger than the image.") 142 | return False 143 | 144 | # if conf.get("portname") == '': 145 | # self.__notify_user("Please choose a valid port.") 146 | # return False 147 | return True 148 | 149 | def __wait_for_user_action(self, message="", message_type="info"): 150 | """Waits for the user to react, blocking it.""" 151 | self.interactive_callbacks["blocking_user_action"](message, message_type) 152 | # logging.info(message) 153 | # time.sleep(3) 154 | # raw_input() 155 | # pass 156 | # self.__parent_ui.emit(QtCore.SIGNAL('display_blocking_pop_up_signal(QString, QString)'), message, message_type) 157 | 158 | def __notify_user(self, message="", message_type="info"): 159 | """Sends the a notification without blocking.""" 160 | self.interactive_callbacks["message"](message, message_type) 161 | # logging.info(message) 162 | # pass 163 | # self.__parent_ui.emit(QtCore.SIGNAL('display_pop_up_signal(QString, QString)'), message, message_type) 164 | 165 | def __emit_progress(self, percent, done, total): 166 | """Shows the current job progress.""" 167 | self.interactive_callbacks["progress"](percent, done, total) 168 | # logging.info("Knitting at {}% . {} out of {}.".format(percent, done, total)) 169 | # pass 170 | # self.__parent_ui.emit(QtCore.SIGNAL('updateProgress(int,int,int)'), int(percent), int(done), int(total)) 171 | 172 | def generate_test_configuration(self): 173 | """Creates a configuration dict from the ui elements. 174 | 175 | Returns: 176 | dict: A dict with configuration. 177 | 178 | """ 179 | 180 | conf = {} 181 | conf[u"colors"] = 2 182 | conf[u"start_line"] = 0 183 | 184 | start_needle_color = stop_needle_color = u"orange" # or green. 185 | start_needle_value = stop_needle_value = 0 186 | 187 | def set_value_by_color(conf_dict, needle_position, needle_color, start_needle_value): 188 | if needle_color == u"orange": 189 | conf_dict[needle_position] = 100 - start_needle_value 190 | elif needle_color == u"green": 191 | conf_dict[needle_position] = 99 + start_needle_value 192 | else: 193 | conf_dict[needle_position] = start_needle_value 194 | return conf_dict 195 | 196 | conf = set_value_by_color(conf, u"start_needle", start_needle_color, start_needle_value) 197 | conf = set_value_by_color(conf, u"stop_needle", stop_needle_color, stop_needle_value) 198 | 199 | conf["alignment"] = "center" 200 | conf["inf_repeat"] = 0 201 | conf["machine_type"] = "single" 202 | 203 | # serial_port = u"/dev/ttyACM0" 204 | # conf["portname"] = serial_port # Should be related to self.getSerialPorts()[0][0] 205 | self.set_port() 206 | # getting file location from textbox 207 | filename_text = u"mushroom.png" 208 | conf["file_url"] = filename_text 209 | logging.debug(conf) 210 | # TODO: Add more config options. 211 | return conf 212 | 213 | def set_port(self, portname=u"/dev/ttyACM0"): 214 | self.__portname = portname 215 | 216 | # From ayab_control 217 | ##################################### 218 | 219 | def __setBit(self, int_type, offset): 220 | mask = 1 << offset 221 | return (int_type | mask) 222 | 223 | def __setPixel(self, bytearray, pixel): 224 | numByte = int(pixel / 8) 225 | bytearray[numByte] = self.__setBit( 226 | int(bytearray[numByte]), pixel - (8 * numByte)) 227 | return bytearray 228 | 229 | def __checkSerial(self): 230 | time.sleep(1) # TODO if problems in communication, tweak here 231 | 232 | line = self.__ayabCom.read_line() 233 | 234 | if line != '': 235 | msgId = ord(line[0]) 236 | if msgId == 0xC1: # cnfStart 237 | # print "> cnfStart: " + str(ord(line[1])) 238 | return ("cnfStart", ord(line[1])) 239 | 240 | elif msgId == 0xC3: # cnfInfo 241 | # print "> cnfInfo: Version=" + str(ord(line[1])) 242 | logging.debug("Detected device with API v" + str(ord(line[1]))) 243 | return ("cnfInfo", ord(line[1])) 244 | 245 | elif msgId == 0x82: # reqLine 246 | # print "> reqLine: " + str(ord(line[1])) 247 | return ("reqLine", ord(line[1])) 248 | 249 | else: 250 | self.__printError("unknown message: " + line[:]) # drop crlf 251 | return ("unknown", 0) 252 | return ("none", 0) 253 | 254 | def __cnfLine(self, lineNumber): 255 | imgHeight = self.__image.imgHeight() 256 | color = 0 257 | indexToSend = 0 258 | sendBlankLine = False 259 | lastLine = 0x00 260 | 261 | # TODO optimize performance 262 | # initialize bytearray to 0x00 263 | bytes = bytearray(25) 264 | for x in range(0, 25): 265 | bytes[x] = 0x00 266 | 267 | if lineNumber < 256: 268 | # TODO some better algorithm for block wrapping 269 | # if the last requested line number was 255, wrap to next block of 270 | # lines 271 | if self.__formerRequest == 255 and lineNumber == 0: 272 | self.__lineBlock += 1 273 | # store requested line number for next request 274 | self.__formerRequest = lineNumber 275 | reqestedLine = lineNumber 276 | 277 | # adjust lineNumber with current block 278 | lineNumber = lineNumber + (self.__lineBlock * 256) 279 | 280 | # when knitting infinitely, keep the requested lineNumber in its limits 281 | if self.__infRepeat: 282 | lineNumber = lineNumber % imgHeight 283 | 284 | ######################### 285 | # decide which line to send according to machine type and amount of colors 286 | # singlebed, 2 color 287 | if self.__machineType == 'single' and self.__numColors == 2: 288 | 289 | # color is always 0 in singlebed, 290 | # because both colors are knitted at once 291 | color = 0 292 | 293 | # calculate imgRow 294 | imgRow = lineNumber + self.__startLine 295 | 296 | # 0 1 2 3 4 .. (imgRow) 297 | # | | | | | 298 | # 0 1 2 3 4 5 6 7 8 .. (imageExpanded) 299 | indexToSend = imgRow * 2 300 | 301 | # Check if the last line of the image was requested 302 | if imgRow == imgHeight - 1: 303 | lastLine = 0x01 304 | 305 | # doublebed, 2 color 306 | elif self.__machineType == 'double' \ 307 | and self.__numColors == 2: 308 | 309 | # calculate imgRow 310 | imgRow = int(lineNumber / 2) + self.__startLine 311 | 312 | # 0 0 1 1 2 2 3 3 4 4 .. (imgRow) 313 | # 0 1 2 3 4 5 6 7 8 9 .. (lineNumber) 314 | # | | X | | X | | 315 | # 0 1 3 2 4 5 7 6 8 9 .. (imageExpanded) 316 | lenImgExpanded = len(self.__image.imageExpanded()) 317 | indexToSend = self.__startLine * 2 318 | 319 | # TODO more beautiful algo 320 | if lineNumber % 4 == 1 or lineNumber % 4 == 2: 321 | color = 1 322 | else: 323 | color = 0 324 | 325 | if (lineNumber - 2) % 4 == 0: 326 | indexToSend += lineNumber + 1 327 | 328 | elif (lineNumber - 2) % 4 == 1: 329 | indexToSend += lineNumber - 1 330 | if (imgRow == imgHeight - 1) \ 331 | and (indexToSend == lenImgExpanded - 2): 332 | lastLine = 0x01 333 | else: 334 | indexToSend += lineNumber 335 | if (imgRow == imgHeight - 1) \ 336 | and (indexToSend == lenImgExpanded - 1): 337 | lastLine = 0x01 338 | 339 | # doublebed, multicolor 340 | elif self.__machineType == 'double' \ 341 | and self.__numColors > 2: 342 | 343 | # calculate imgRow 344 | imgRow = int( 345 | lineNumber / (self.__numColors * 2)) + self.__startLine 346 | 347 | if (lineNumber % 2) == 0: 348 | color = (lineNumber / 2) % self.__numColors 349 | indexToSend = (imgRow * self.__numColors) + color 350 | logging.debug("COLOR" + str(color)) 351 | else: 352 | sendBlankLine = True 353 | 354 | lenImgExpanded = len(self.__image.imageExpanded()) 355 | 356 | # TODO Check assignment 357 | if imgRow == imgHeight - 1 \ 358 | and (indexToSend == lenImgExpanded - 1): 359 | lastLine = 0x01 360 | ######################### 361 | 362 | # assign pixeldata 363 | imgStartNeedle = self.__image.imgStartNeedle() 364 | if imgStartNeedle < 0: 365 | imgStartNeedle = 0 366 | 367 | imgStopNeedle = self.__image.imgStopNeedle() 368 | if imgStopNeedle > 199: 369 | imgStopNeedle = 199 370 | 371 | # set the bitarray 372 | if color == 0 \ 373 | and self.__machineType == 'double': 374 | for col in range(0, 200): 375 | if col < imgStartNeedle \ 376 | or col > imgStopNeedle: 377 | bytes = self.__setPixel(bytes, col) 378 | 379 | for col in range(0, self.__image.imgWidth()): 380 | pxl = (self.__image.imageExpanded())[indexToSend][col] 381 | # take the image offset into account 382 | if pxl is True and sendBlankLine is False: 383 | bytes = self.__setPixel( 384 | bytes, col + self.__image.imgStartNeedle()) 385 | 386 | # TODO implement CRC8 387 | crc8 = 0x00 388 | 389 | # send line to machine 390 | if self.__infRepeat: 391 | self.__ayabCom.cnf_line(reqestedLine, bytes, 0, crc8) 392 | else: 393 | self.__ayabCom.cnf_line(reqestedLine, bytes, lastLine, crc8) 394 | 395 | # screen output 396 | msg = str((self.__image.imageExpanded())[indexToSend]) 397 | msg += ' Image Row: ' + str(imgRow) 398 | msg += ' (indexToSend: ' + str(indexToSend) 399 | msg += ', reqLine: ' + str(reqestedLine) 400 | msg += ', lineNumber: ' + str(lineNumber) 401 | msg += ', lineBlock:' + str(self.__lineBlock) + ')' 402 | logging.debug(msg) 403 | # sending line progress to gui 404 | progress_int = 100 * float(imgRow) / self.__image.imgHeight() 405 | self.__emit_progress(progress_int, imgRow, imgHeight) 406 | 407 | else: 408 | logging.error("requested lineNumber out of range") 409 | 410 | if lastLine: 411 | if self.__infRepeat: 412 | self.__lineBlock = 0 413 | return 0 # keep knitting 414 | else: 415 | return 1 # image finished 416 | else: 417 | return 0 # keep knitting 418 | 419 | def __knitImage(self, pImage, pOptions): 420 | self.__formerRequest = 0 421 | self.__image = pImage 422 | self.__startLine = pImage.startLine() 423 | 424 | self.__numColors = pOptions.get("colors", 2) 425 | self.__machineType = pOptions.get("machine_type", "single") 426 | self.__infRepeat = pOptions.get("inf_repeat", False) 427 | 428 | API_VERSION = self.__API_VERSION 429 | curState = 's_init' 430 | oldState = 'none' 431 | 432 | if not self.__ayabCom.open_serial(self.__portname): 433 | logging.error("Could not open serial port") 434 | return 435 | 436 | self._knitImage = True 437 | logging.debug("Ready to start knitting process.") 438 | while self._knitImage: 439 | # TODO catch keyboard interrupts to abort knitting 440 | # TODO: port to state machine or similar. 441 | rcvMsg, rcvParam = self.__checkSerial() 442 | if curState == 's_init': 443 | if oldState != curState: 444 | self.__ayabCom.req_info() 445 | 446 | if rcvMsg == 'cnfInfo': 447 | if rcvParam == API_VERSION: 448 | curState = 's_start' 449 | self.__wait_for_user_action( 450 | "Please init machine. (Set the carriage to mode KC-I or KC-II and move the carriage over the left turn mark).") 451 | else: 452 | self.__notify_user("Wrong API.") 453 | logging.error("wrong API version: " + str(rcvParam) 454 | + (" (expected: )") + str(API_VERSION)) 455 | return 456 | 457 | if curState == 's_start': 458 | if oldState != curState: 459 | self.__ayabCom.req_start(self.__image.knitStartNeedle(), 460 | self.__image.knitStopNeedle()) 461 | 462 | if rcvMsg == 'cnfStart': 463 | if rcvParam == 1: 464 | curState = 's_operate' 465 | self.__wait_for_user_action("Ready to Operate") 466 | else: 467 | self.__wait_for_user_action("Device not ready, configure and try again.") 468 | logging.error("device not ready") 469 | return 470 | 471 | if curState == 's_operate': 472 | if rcvMsg == 'reqLine': 473 | imageFinished = self.__cnfLine(rcvParam) 474 | if imageFinished: 475 | curState = 's_finished' 476 | 477 | if curState == 's_finished': 478 | self.__wait_for_user_action( 479 | "Image transmission finished. Please knit until you hear the double beep sound.") 480 | return 481 | 482 | oldState = curState 483 | 484 | return 485 | -------------------------------------------------------------------------------- /src/knitlib/plugins/pdd_plugin/PDDemulate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright 2009 Steve Conklin 4 | # steve at conklinhouse dot com 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 19 | 20 | 21 | # This software emulates the external floppy disk drive used 22 | # by the Brother Electroknit KH-930E computerized knitting machine. 23 | # It may work for other models, but has only been tested with the 24 | # Brother KH-930E 25 | # 26 | # This emulates the disk drive and stores the saved data from 27 | # the knitting machine on the linux file system. It does not 28 | # read or write floppy disks. 29 | # 30 | # The disk drive used by the brother knitting machine is the same 31 | # as a Tandy PDD1 drive. This software does not support the entire 32 | # command API of the PDD1, only what is required for the knitting 33 | # machine. 34 | # 35 | 36 | # 37 | # Notes about data storage: 38 | # 39 | # The external floppy disk is formatted with 80 sectors of 1024 40 | # bytes each. These sectors are numbered (internally) from 0-79. 41 | # When starting this emulator, a base directory is specified. 42 | # In this directory the emulator creates 80 files, one for each 43 | # sector. These are kept sync'd with the emulator's internal 44 | # storage of the same sectors. For each sector, there are two 45 | # files, nn.dat, and nn.id, where 00 <= nn <= 79. 46 | # 47 | # The knitting machine uses two sectors for each saved set of 48 | # information, which are referred to in the knitting machine 49 | # manual as 'tracks' (which they were on the floppy disk). Each 50 | # pair of even/odd numbered sectors is a track. Tracks are 51 | # numbered 1-40. The knitting machine always writes sectors 52 | # in even/odd pairs, and when the odd sector is written, both 53 | # sectors are concatenated to a file named fileqq.dat, where 54 | # qq is the sector number. 55 | # 56 | 57 | # The Knitting machine does not parse the returned hex ascii values 58 | # unless they are ALL UPPER CASE. Lower case characters a-f appear 59 | # to parse az zeros. 60 | 61 | # You will need the (very nice) pySerial module, found here: 62 | # http://pyserial.wiki.sourceforge.net/pySerial 63 | 64 | import sys 65 | import os 66 | import os.path 67 | import string 68 | from array import array 69 | import serial 70 | import logging 71 | 72 | version = '2.5' 73 | 74 | # 75 | # Note that this code makes a fundamental assumption which 76 | # is only true for the disk format used by the brother knitting 77 | # machine, which is that there is only one logical sector (LS) per 78 | # physical sector (PS). The PS size is fixed at 1280 bytes, and 79 | # the brother uses a LS size of 1024 bytes, so only one can fit. 80 | # 81 | 82 | 83 | class DiskSector(): 84 | def __init__(self, fn): 85 | self.sectorSz = 1024 86 | self.idSz = 12 87 | self.data = '' 88 | self.id = '' 89 | # self.id = array('c') 90 | 91 | dfn = fn + ".dat" 92 | idfn = fn + ".id" 93 | 94 | try: 95 | try: 96 | self.df = open(dfn, 'r+') 97 | except IOError: 98 | self.df = open(dfn, 'w') 99 | 100 | try: 101 | self.idf = open(idfn, 'r+') 102 | except IOError: 103 | self.idf = open(idfn, 'w') 104 | 105 | dfs = os.path.getsize(dfn) 106 | idfs = os.path.getsize(idfn) 107 | 108 | except: 109 | logging.error('Unable to open files using base name <%s>' % fn) 110 | raise 111 | 112 | try: 113 | if dfs == 0: 114 | # New or empty file 115 | self.data = ''.join([chr(0) for num in xrange(self.sectorSz)]) 116 | self.writeDFile() 117 | elif dfs == self.sectorSz: 118 | # Existing file 119 | self.data = self.df.read(self.sectorSz) 120 | else: 121 | logging.error('Found a data file <%s> with the wrong size' % dfn) 122 | raise IOError 123 | except: 124 | print 'Unable to handle data file <%s>' % fn 125 | raise 126 | 127 | try: 128 | if idfs == 0: 129 | # New or empty file 130 | self.id = ''.join([chr(0) for num in xrange(self.idSz)]) 131 | self.writeIdFile() 132 | elif idfs == self.idSz: 133 | # Existing file 134 | self.id = self.idf.read(self.idSz) 135 | else: 136 | logging.error('Found an ID file <%s> with the wrong size, is %d should be %d' % (idfn, idfs, self.idSz)) 137 | raise IOError 138 | except: 139 | logging.error('Unable to handle id file <%s>' % fn) 140 | raise 141 | 142 | return 143 | 144 | def __del__(self): 145 | return 146 | 147 | def format(self): 148 | self.data = ''.join([chr(0) for num in xrange(self.sectorSz)]) 149 | self.writeDFile() 150 | self.id = ''.join([chr(0) for num in xrange(self.idSz)]) 151 | self.writeIdFile() 152 | 153 | def writeDFile(self): 154 | self.df.seek(0) 155 | self.df.write(self.data) 156 | self.df.flush() 157 | return 158 | 159 | def writeIdFile(self): 160 | self.idf.seek(0) 161 | self.idf.write(self.id) 162 | self.idf.flush() 163 | return 164 | 165 | def read(self, length): 166 | if length != self.sectorSz: 167 | logging.error('Error, read of %d bytes when expecting %d' % (length, self.sectorSz)) 168 | raise IOError 169 | return self.data 170 | 171 | def write(self, indata): 172 | if len(indata) != self.sectorSz: 173 | logging.error('Error, write of %d bytes when expecting %d' % (len(indata), self.sectorSz)) 174 | raise IOError 175 | self.data = indata 176 | self.writeDFile() 177 | return 178 | 179 | def getSectorId(self): 180 | return self.id 181 | 182 | def setSectorId(self, newid): 183 | if len(newid) != self.idSz: 184 | logging.error('Error, bad id length of %d bytes when expecting %d' % (len(newid), self.id)) 185 | raise IOError 186 | self.id = newid 187 | self.writeIdFile() 188 | logging.info('Wrote New ID: {}'.format(self.dumpId())) 189 | return 190 | 191 | def dumpId(self): 192 | id_l = [] 193 | for i in self.id: 194 | id_l.append('%02X ' % ord(i)) 195 | return " ".join(id_l) 196 | 197 | 198 | class Disk(): 199 | """ 200 | Fields: 201 | self.lastDatFilePath : string 202 | """ 203 | 204 | def __init__(self, basename): 205 | self.numSectors = 80 206 | self.Sectors = [] 207 | self.filespath = "" 208 | "" 209 | self.lastDatFilePath = None 210 | # Set up disk Files and internal buffers 211 | 212 | # if absolute path, just accept it 213 | if os.path.isabs(basename): 214 | dirpath = basename 215 | else: 216 | dirpath = os.path.abspath(basename) 217 | 218 | if os.path.exists(dirpath): 219 | if not os.access(dirpath, os.R_OK | os.W_OK): 220 | logging.error('Directory <%s> exists but cannot be accessed, check permissions' % dirpath) 221 | raise IOError 222 | elif not os.path.isdir(dirpath): 223 | logging.error('Specified path <%s> exists but is not a directory' % dirpath) 224 | raise IOError 225 | else: 226 | try: 227 | os.mkdir(dirpath) 228 | except: 229 | logging.error('Unable to create directory <%s>' % dirpath) 230 | raise IOError 231 | 232 | self.filespath = dirpath 233 | # we have a directory now - set up disk sectors 234 | for i in range(self.numSectors): 235 | fname = os.path.join(dirpath, '%02d' % i) 236 | ds = DiskSector(fname) 237 | self.Sectors.append(ds) 238 | return 239 | 240 | def __del__(self): 241 | return 242 | 243 | def format(self): 244 | for i in range(self.numSectors): 245 | self.Sectors[i].format() 246 | return 247 | 248 | def findSectorID(self, psn, id): 249 | for i in range(psn, self.numSectors): 250 | sid = self.Sectors[i].getSectorId() 251 | if id == sid: 252 | return '00' + '%02X' % i + '0000' 253 | return '40000000' 254 | 255 | def getSectorID(self, psn): 256 | return self.Sectors[psn].getSectorId() 257 | 258 | def setSectorID(self, psn, id): 259 | self.Sectors[psn].setSectorId(id) 260 | return 261 | 262 | def writeSector(self, psn, lsn, indata): 263 | self.Sectors[psn].write(indata) 264 | if psn % 2: 265 | filenum = ((psn - 1) / 2) + 1 266 | filename = 'file-%02d.dat' % filenum 267 | # we wrote an odd sector, so create the 268 | # associated file 269 | fn1 = os.path.join(self.filespath, '%02d.dat' % (psn - 1)) 270 | fn2 = os.path.join(self.filespath, '%02d.dat' % psn) 271 | outfn = os.path.join(self.filespath, filename) 272 | # TODO: change this to a pure Python binary merge. 273 | cmd = 'cat %s %s > %s' % (fn1, fn2, outfn) 274 | os.system(cmd) 275 | self.lastDatFilePath = outfn 276 | return 277 | 278 | def readSector(self, psn, lsn): 279 | return self.Sectors[psn].read(1024) 280 | 281 | 282 | class PDDemulator(): 283 | def __init__(self, basename): 284 | self.listeners = [] # list of PDDEmulatorListener 285 | self.verbose = True 286 | self.noserial = False 287 | self.ser = None 288 | self.disk = Disk(basename) 289 | self.FDCmode = False 290 | # bytes per logical sector 291 | self.bpls = 1024 292 | self.formatLength = {'0': 64, '1': 80, '2': 128, '3': 256, '4': 512, '5': 1024, '6': 1280} 293 | return 294 | 295 | def __del__(self): 296 | return 297 | 298 | def open(self, cport='/dev/ttyUSB0'): 299 | if self.noserial is False: 300 | self.ser = serial.Serial(port=cport, baudrate=9600, parity='N', stopbits=1, timeout=1, xonxoff=0, rtscts=0, 301 | dsrdtr=0) 302 | # self.ser.setRTS(True) 303 | if self.ser is None: 304 | logging.error('Unable to open serial device %s' % cport) 305 | raise IOError 306 | return 307 | 308 | def close(self): 309 | if self.noserial is not False: 310 | if self.ser: 311 | self.ser.close() 312 | return 313 | 314 | def dumpchars(self): 315 | num = 1 316 | while 1: 317 | inc = self.ser.read() 318 | if len(inc) != 0: 319 | logging.info('flushed 0x%02X (%d)' % (ord(inc), num)) 320 | num += 1 321 | else: 322 | break 323 | return 324 | 325 | def readsomechars(self, num): 326 | sch = self.ser.read(num) 327 | return sch 328 | 329 | def readchar(self): 330 | inc = '' 331 | while len(inc) == 0: 332 | inc = self.ser.read() 333 | return inc 334 | 335 | def writebytes(self, bytes): 336 | self.ser.write(bytes) 337 | return 338 | 339 | def readFDDRequest(self): 340 | inbuf = [] 341 | # read through a carriage return 342 | # parameters are seperated by commas 343 | while 1: 344 | inc = self.readchar() 345 | if inc == '\r': 346 | break 347 | elif inc == ' ': 348 | continue 349 | else: 350 | inbuf.append(inc) 351 | 352 | all = string.join(inbuf, '') 353 | rv = all.split(',') 354 | return rv 355 | 356 | def getPsnLsn(self, info): 357 | psn = 0 358 | lsn = 1 359 | if len(info) >= 1 and info[0] != '': 360 | val = int(info[0]) 361 | if psn <= 79: 362 | psn = val 363 | if len(info) > 1 and info[1] != '': 364 | val = int(info[0]) 365 | return psn, lsn 366 | 367 | def readOpmodeRequest(self, req): 368 | buff = array('b') 369 | sum = req 370 | reqlen = ord(self.readchar()) 371 | buff.append(reqlen) 372 | sum = sum + reqlen 373 | 374 | for x in range(reqlen, 0, -1): 375 | rb = ord(self.readchar()) 376 | buff.append(rb) 377 | sum = sum + rb 378 | 379 | # calculate ckecksum 380 | sum = sum % 0x100 381 | sum = sum ^ 0xFF 382 | 383 | cksum = ord(self.readchar()) 384 | 385 | if cksum == sum: 386 | return buff 387 | else: 388 | logging.warning('Checksum mismatch!!') 389 | return None 390 | 391 | def handleRequests(self): 392 | synced = False 393 | while True: 394 | self.handleRequest() 395 | if synced: 396 | break 397 | # never returns 398 | return 399 | 400 | def handleRequest(self, blocking=True): 401 | if not blocking: 402 | if self.ser.inWaiting() == 0: 403 | return 404 | inc = self.readchar() 405 | if self.FDCmode: 406 | self.handleFDCmodeRequest(inc) 407 | else: 408 | # in OpMode, look for ZZ 409 | # inc = self.readchar() 410 | if inc != 'Z': 411 | return 412 | inc = self.readchar() 413 | if inc == 'Z': 414 | self.handleOpModeRequest() 415 | 416 | def handleOpModeRequest(self): 417 | req = ord(self.ser.read()) 418 | logging.debug('Request: 0X%02X' % req) 419 | if req == 0x08: 420 | # Change to FDD emulation mode (no data returned) 421 | inbuf = self.readOpmodeRequest(req) 422 | if inbuf is not None: 423 | # Change Modes, leave any incoming serial data in buffer 424 | self.FDCmode = True 425 | else: 426 | logging.warning('Invalid OpMode request code 0X%02X received' % req) 427 | return 428 | 429 | def handleFDCmodeRequest(self, cmd): 430 | # Commands may be followed by an optional space 431 | # PSN (physical sector) range 0-79 432 | # LSN (logical sector) range 0-(number of logical sectors in a physical sector) 433 | # LSN defaults to 1 if not supplied 434 | # 435 | # Result code information (verbatim from the Tandy reference): 436 | # 437 | # After the drive receives a command in FDC-emulation mode, it transmits 438 | # 8 byte characters which represent 4 bytes of status code in hexadecimal. 439 | # 440 | # * The first and second bytes contain the error status. A value of '00' 441 | # indicates that no error occurred 442 | # 443 | # * The third and fourth bytes usually contain the number of the physical 444 | # sector where data is kept in the buffer 445 | # 446 | # For the D, F, and S commands, the contents of these bytes are different. 447 | # See the command descriptions in these cases. 448 | # 449 | # * The fifth-eighth bytes usual show the logical sector length of the data 450 | # kept in the RAM buffer, except the third and fourth digits are 'FF' 451 | # 452 | # In the case of an S, C, or M command -- or an F command that ends in 453 | # an error -- the bytes contain '0000' 454 | # 455 | 456 | if cmd == '\r': 457 | return 458 | 459 | if cmd == 'Z': 460 | # Hmmm, looks like we got the start of an Opmode Request 461 | inc = self.readchar() 462 | if inc == 'Z': 463 | # definitely! 464 | logging.info('Detected Opmode Request in FDC Mode, switching to OpMode') 465 | self.FDCmode = False 466 | self.handleOpModeRequest() 467 | 468 | elif cmd == 'M': 469 | # apparently not used by brother knitting machine 470 | logging.info('FDC Change Modes') 471 | raise 472 | # following parameter - 0=FDC, 1=Operating 473 | 474 | elif cmd == 'D': 475 | # apparently not used by brother knitting machine 476 | logging.info('FDC Check Device') 477 | raise 478 | # Sends result in third and fourth bytes of result code 479 | # See doc - return zero for disk installed and not swapped 480 | 481 | elif cmd == 'F' or cmd == 'G': 482 | # rint 'FDC Format', 483 | info = self.readFDDRequest() 484 | 485 | if len(info) != 1: 486 | logging.warning('wrong number of params (%d) received, assuming 1024 bytes per sector' % len(info)) 487 | bps = 1024 488 | else: 489 | try: 490 | bps = self.formatLength[info[0]] 491 | except KeyError: 492 | logging.error('Invalid code %c for format, assuming 1024 bytes per sector' % info[0]) 493 | bps = 1024 494 | # we assume 1024 because that's what the brother machine uses 495 | if self.bpls != bps: 496 | logging.error('Bad news, differing sector sizes') 497 | self.bpls = bps 498 | 499 | self.disk.format() 500 | 501 | # But this is probably more correct 502 | self.writebytes('00000000') 503 | 504 | # After a format, we always start out with OPMode again 505 | self.FDCmode = False 506 | 507 | elif cmd == 'A': 508 | # Followed by physical sector number (0-79), defaults to 0 509 | # returns ID data, not sector data 510 | info = self.readFDDRequest() 511 | psn, lsn = self.getPsnLsn(info) 512 | logging.debug('FDC Read ID Section %d' % psn) 513 | 514 | try: 515 | id = self.disk.getSectorID(psn) 516 | except: 517 | logging.error('Error getting Sector ID %d, quitting' % psn) 518 | self.writebytes('80000000') 519 | raise 520 | 521 | self.writebytes('00' + '%02X' % psn + '0000') 522 | 523 | # see whether to send data 524 | go = self.readchar() 525 | if go == '\r': 526 | self.writebytes(id) 527 | 528 | elif cmd == 'R': 529 | # Followed by Physical Sector Number PSN and Logical Sector Number LSN 530 | info = self.readFDDRequest() 531 | psn, lsn = self.getPsnLsn(info) 532 | logging.info('FDC Read one Logical Sector %d' % psn) 533 | 534 | try: 535 | sd = self.disk.readSector(psn, lsn) 536 | except: 537 | logging.error('Failed to read Sector %d, quitting' % psn) 538 | self.writebytes('80000000') 539 | raise 540 | 541 | self.writebytes('00' + '%02X' % psn + '0000') 542 | 543 | # see whether to send data 544 | go = self.readchar() 545 | if go == '\r': 546 | self.writebytes(sd) 547 | 548 | elif cmd == 'S': 549 | # We receive (optionally) PSN, (optionally) LSN 550 | # This is not documented well at all in the manual 551 | # What is expected is that all sectors will be searched 552 | # and the sector number of the first matching sector 553 | # will be returned. The brother machine always sends 554 | # PSN = 0, so it is unknown whether searching should 555 | # start at Sector 0 or at the PSN sector 556 | info = self.readFDDRequest() 557 | psn, lsn = self.getPsnLsn(info) 558 | logging.debug('FDC Search ID Section %d' % psn) 559 | 560 | # Now we must send status (success) 561 | self.writebytes('00' + '%02X' % psn + '0000') 562 | 563 | # self.writebytes('00000000') 564 | 565 | # we receive 12 bytes here 566 | # compare with the specified sector (formatted is apparently zeros) 567 | id = self.readsomechars(12) 568 | logging.debug('checking ID for sector %d' % psn) 569 | 570 | try: 571 | status = self.disk.findSectorID(psn, id) 572 | except: 573 | logging.error("Fail at finding sector ID") 574 | status = '30000000' 575 | raise 576 | 577 | print 'returning %s' % status 578 | # guessing - doc is unclear, but says that S always ends in 0000 579 | # MATCH 00000000 580 | # MATCH 02000000 581 | # infinite retries 10000000 582 | # infinite retries 20000000 583 | # blinking error 30000000 584 | # blinking error 40000000 585 | # infinite retries 50000000 586 | # infinite retries 60000000 587 | # infinite retries 70000000 588 | # infinite retries 80000000 589 | 590 | self.writebytes(status) 591 | 592 | # Stay in FDC mode 593 | 594 | elif cmd == 'B' or cmd == 'C': 595 | # Followed by PSN 0-79, defaults to 0 596 | # When received, send result status, if not error, wait 597 | # for data to be written, then after write, send status again 598 | info = self.readFDDRequest() 599 | psn, lsn = self.getPsnLsn(info) 600 | logging.debug('FDC Write ID section %d' % psn) 601 | 602 | self.writebytes('00' + '%02X' % psn + '0000') 603 | 604 | id = self.readsomechars(12) 605 | 606 | try: 607 | self.disk.setSectorID(psn, id) 608 | except: 609 | logging.error('Failed to write ID for sector %d, quitting' % psn) 610 | self.writebytes('80000000') 611 | raise 612 | 613 | self.writebytes('00' + '%02X' % psn + '0000') 614 | 615 | elif cmd == 'W' or cmd == 'X': 616 | info = self.readFDDRequest() 617 | psn, lsn = self.getPsnLsn(info) 618 | logging.debug('FDC Write logical sector %d' % psn) 619 | 620 | # Now we must send status (success) 621 | self.writebytes('00' + '%02X' % psn + '0000') 622 | 623 | indata = self.readsomechars(1024) 624 | try: 625 | self.disk.writeSector(psn, lsn, indata) 626 | for l in self.listeners: 627 | l.dataReceived(self.disk.lastDatFilePath) 628 | logging.info('Saved data in dat file: ', self.disk.lastDatFilePath) 629 | except: 630 | logging.error('Failed to write data for sector %d, quitting' % psn) 631 | self.writebytes('80000000') 632 | raise 633 | 634 | self.writebytes('00' + '%02X' % psn + '0000') 635 | 636 | else: 637 | print 'Unknown FDC command <0x02%X> received' % ord(cmd) 638 | 639 | # return to Operational Mode 640 | return 641 | 642 | 643 | class PDDEmulatorListener: 644 | def dataReceived(self, fullFilePath): 645 | pass 646 | 647 | # meat and potatos here 648 | 649 | if __name__ == "__main__": 650 | if len(sys.argv) < 3: 651 | print '%s version %s' % (sys.argv[0], version) 652 | print 'Usage: %s basedir serialdevice' % sys.argv[0] 653 | sys.exit() 654 | 655 | print 'Preparing . . . Please Wait' 656 | emu = PDDemulator(sys.argv[1]) 657 | 658 | # TODO: fix usb port hardcoding 659 | emu.open(cport=sys.argv[2]) 660 | 661 | print 'Emulator Ready!' 662 | try: 663 | while 1: 664 | emu.handleRequests() 665 | except (KeyboardInterrupt): 666 | pass 667 | 668 | emu.close() 669 | --------------------------------------------------------------------------------