diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61331646..9036dc91 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ Pull requests are the best way to propose changes to the codebase (we use [Githu In short, when you submit code changes, your submissions are understood to be under the same [GPL-3.0 License](https://choosealicense.com/licenses/gpl-3.0/) that covers the project. Feel free to contact the maintainers if that's a concern. ## Report bugs using Github's [issues](https://github.com/AllYarnsAreBeautiful/ayab-desktop/issues) -We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/AllYarnsAreBeautiful/ayab-desktop/issues/new); it's that easy! +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy! ## Write bug reports with detail, background, and sample code diff --git a/README.md b/README.md index 7f22f8a5..ea30a3a1 100644 --- a/README.md +++ b/README.md @@ -86,11 +86,19 @@ macOS You can install Git using Homebrew: ```bash brew install git + brew install esptool ``` You will also need the Xcode command line tools: ```bash xcode-select --install ``` + +get ayab-desktop for eKnitter from github: + +```bash + git clone -b eKnitter https://github.com/yekomS/ayab-desktop ayab-desktop-eknitter +``` + Install python from [the official universal2 installer](https://www.python.org/ftp/python/3.11.8/python-3.11.8-macos11.pkg). (Conda does not produce universal binaries) If you encounter the pip `SSL:TLSV1_ALERT_PROTOCOL_VERSION` problem: diff --git a/requirements.txt b/requirements.txt index 6f1cbb7c..2c2b651a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ types-pyserial==3.5.0 types-mock types-Pillow==10 mypy==1.9.0 +esptool==3.3.3 diff --git a/setup-environment.ps1 b/setup-environment.ps1 index 713dd06c..ac7cda5f 100755 --- a/setup-environment.ps1 +++ b/setup-environment.ps1 @@ -35,6 +35,9 @@ pyside6-rcc src/main/python/main/ayab/ayab_logo_rc.qrc -o src/main/python/main/a pyside6-rcc src/main/python/main/ayab/engine/lowercase_e_rc.qrc -o src/main/python/main/ayab/engine/lowercase_e_rc.py pyside6-rcc src/main/python/main/ayab/engine/lowercase_e_reversed_rc.qrc -o src/main/python/main/ayab/engine/lowercase_e_reversed_rc.py +pyside6-rcc src/main/python/main/ayab/engine/eKnitter_rc.qrc -o src/main/python/main/ayab/engine/eKnitter_rc.py +pyside6-rcc src/main/python/main/ayab/engine/eKnitter_reversed_rc.qrc -o src/main/python/main/ayab/engine/eKnitter_reversed_rc.py + # generate translation files cd src/main/resources/base/ayab/translations/ perl ayab_trans.pl diff --git a/src/main/NSIS/Installer.nsi b/src/main/NSIS/Installer.nsi index d2d3f2dc..4cf6e4b1 100644 --- a/src/main/NSIS/Installer.nsi +++ b/src/main/NSIS/Installer.nsi @@ -16,7 +16,7 @@ Function .onInit ;Do not use InstallDir at all so we can detect empty $InstDir! ${If} $InstDir == "" ; /D not used ${If} $MultiUser.InstallMode == "AllUsers" - StrCpy $InstDir "C:\%{app_name}-v%{version}" + StrCpy $InstDir "$%HOMEDRIVE%\%{app_name}-v%{version}" ${Else} StrCpy $InstDir "$LOCALAPPDATA\%{app_name}" ${EndIf} diff --git a/src/main/NSIS/ShellExecAsUser.dll b/src/main/NSIS/ShellExecAsUser.dll index 01a8c85a..2bf1d0c3 100644 Binary files a/src/main/NSIS/ShellExecAsUser.dll and b/src/main/NSIS/ShellExecAsUser.dll differ diff --git a/src/main/icons/Icon.ico b/src/main/icons/Icon.ico index f6cfc6a1..d4de60be 100644 Binary files a/src/main/icons/Icon.ico and b/src/main/icons/Icon.ico differ diff --git a/src/main/python/main/ayab/about.py b/src/main/python/main/ayab/about.py index c7522a44..1273b1e8 100644 --- a/src/main/python/main/ayab/about.py +++ b/src/main/python/main/ayab/about.py @@ -38,7 +38,7 @@ def __init__(self, parent: GuiMain): super().__init__() self.__version = utils.package_version(parent.app_context) self.__ui = Ui_AboutForm() - self.__ui.setupUi(self) + self.__ui.setupUi(self) # type: ignore self.__ui.title_label.setText( QCoreApplication.translate("MainWindow", "All Yarns Are Beautiful") + " " diff --git a/src/main/python/main/ayab/about_gui.ui b/src/main/python/main/ayab/about_gui.ui index 1744d84f..dc434852 100644 --- a/src/main/python/main/ayab/about_gui.ui +++ b/src/main/python/main/ayab/about_gui.ui @@ -11,7 +11,7 @@ - About AYAB + About AYAB for eKnitter false @@ -41,7 +41,7 @@ - All Yarns Are Beautiful + All Yarns Are Beautiful for eKnitter diff --git a/src/main/python/main/ayab/ayab.py b/src/main/python/main/ayab/ayab.py index 9fd424ee..67e76233 100644 --- a/src/main/python/main/ayab/ayab.py +++ b/src/main/python/main/ayab/ayab.py @@ -17,6 +17,7 @@ # Copyright 2014 Sebastian Oliva, Christian Obersteiner, # Andreas Müller, Christian Gerbrandt # https://github.com/AllYarnsAreBeautiful/ayab-desktop +# Copyright 2024 Marcus Hoose (eKnitter.com) """Provides a graphical interface for users to operate AYAB.""" from __future__ import annotations @@ -25,6 +26,7 @@ from PySide6.QtWidgets import QMainWindow from PySide6.QtCore import QCoreApplication +from PySide6.QtCore import QEvent from .main_gui import Ui_MainWindow from .gui_fsm import gui_fsm @@ -58,6 +60,11 @@ class GuiMain(QMainWindow): components from `menu_gui.ui`. """ + def closeEvent(self, event: QEvent) -> None: + print("close") + self.engine.close() + event.accept() + def __init__(self, app_context: AppContext): super().__init__() self.app_context = app_context @@ -68,7 +75,7 @@ def __init__(self, app_context: AppContext): # create UI self.ui = Ui_MainWindow() - self.ui.setupUi(self) + self.ui.setupUi(self) # type: ignore # add modular components self.menu = Menu(self) @@ -131,6 +138,7 @@ def __set_prefs(self) -> None: def __quit(self) -> None: logging.debug("Quitting") + self.engine.close() instance = QCoreApplication.instance() if instance is not None: instance.quit() diff --git a/src/main/python/main/ayab/engine/communication_ip.py b/src/main/python/main/ayab/engine/communication_ip.py new file mode 100644 index 00000000..87b353dc --- /dev/null +++ b/src/main/python/main/ayab/engine/communication_ip.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# This file is part of AYAB. +# +# AYAB is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# AYAB is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with AYAB. If not, see . +# +# Copyright 2024 Marcus Hoose (eKnitter.com) +"""Handles the IP communication protocol. + +This module handles IP communication, currently works in a synchronous way. +""" + +from .communication import * +from ..machine import * + +import socket + +import logging +import pprint +from time import sleep + +# Port for TCP +remotePort = 12346 + +class CommunicationIP(Communication): + def __init__(self) -> None: + logging.basicConfig(level=logging.DEBUG) + self.logger = logging.getLogger(type(self).__name__) + self.__tarAddressPort: tuple[str | None, int] = ("255.255.255.255", 12345) + self.__sockTCP: None | socket.socket = None + # socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) + self.rx_msg_list = list() + self.version = 6 + + def __del__(self) -> None: + self.close_socket() + + def is_open(self) -> bool: + if self.__sockTCP is not None: + return True + else: + return False + + def open_serial(self, portname: str | None = None) -> bool: + print("open: " , portname) + return self.open_tcp(portname) + + def close_serial(self) -> None: + return None + + def open_tcp(self, pPortname: str | None = None) -> bool: + try: + self.__portname = pPortname + self.__tarAddressPort = (self.__portname, remotePort) + self.__sockTCP = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM) + self.__sockTCP.settimeout(10.0) + self.__sockTCP.connect(self.__tarAddressPort) + self.__sockTCP.settimeout(0.0) + self.__sockTCP.setblocking(False) + self.logger.info("Open TCP Socket successful") + return True + except: + self.logger.info("Open TCP Socket faild") + return False + + def close_socket(self) -> None: + if self.__sockTCP is not None: + try: + self.__sockTCP.close() + del self.__sockTCP + self.logger.info("Closing TCP Socket successful.") + except: + self.logger.warning("Closing TCP Socket failed. (mem Leak?)") + self.__sockTCP = None + + def send(self, data: bytearray) -> None: + if self.__sockTCP is not None: + try: + self.__sockTCP.send(bytes(data)) + # self.logger.info("SEND b'"+data+"'") + sleep(0.5) + except Exception as e: + if hasattr(e, 'message'): + self.logger.exception(e.message) + else: + self.logger.exception("Connection...Error") + self.close_socket() + + # NB this method must be the same for all API versions + def req_info(self) -> None: + self.send(bytearray([Token.reqInfo.value,self.version])) + + def req_test_API6(self) -> None: + self.send(bytearray([Token.reqTest.value])) + + def req_start_API6(self, start_needle: int, stop_needle: int, + continuous_reporting: bool, disable_hardware_beep: bool) -> None: + """Send a start message to the device.""" + data = bytearray() + data.append(Token.reqStart.value) + data.append(start_needle) + data.append(stop_needle) + data.append( + 1 * continuous_reporting + + 2 * (not disable_hardware_beep)) + hash = 0 + hash = add_crc(hash, data) + data.append(hash) + self.send(data) + + def req_init_API6(self, machine: Machine) -> None: + """Send a start message to the device.""" + data = bytearray() + data.append(Token.reqInit.value) + data.append(machine.value) + hash = 0 + hash = add_crc(hash, data) + data.append(hash) + self.send(data) + + def cnf_line_API6(self, line_number: int , color: int, flags: int, line_data: bytes) -> None: + """Send a line of data via the serial port. + + Send a line of data to the serial port. All arguments are mandatory. + The data sent here is parsed by the Arduino controller which sets the + knitting needles accordingly. + + Args: + line_number (int): The line number to be sent. + color (int): The yarn color to be sent. + flags (int): The flags sent to the controller. + line_data (bytes): The bytearray to be sent to needles. + """ + data = bytearray() + data.append(Token.cnfLine.value) + data.append(line_number) + data.append(color) + data.append(flags) + data.extend(line_data) + hash = 0 + hash = add_crc(hash, data) + data.append(hash) + self.send(data) + + def update_API6(self) -> tuple[bytes | None, Token, int]: + """Read data from serial and parse as SLIP packet.""" + return self.parse_API6(self.read_API6()) + + def parse_API6(self, msg: bytes | None) -> tuple[bytes | None, Token, int]: + if msg is None: + return None, Token.none, 0 + # else + for t in list(Token): + if msg[0] == t.value: + return msg, t, msg[1] + # fallthrough + self.logger.debug("unknown message: ") # drop crlf + pp = pprint.PrettyPrinter(indent=4) + pp.pprint(msg[1: -1].decode()) + return msg, Token.unknown, 0 + + def read_API6(self) -> bytes | None: + """Read data from serial as SLIP packet.""" + if self.__sockTCP is not None: + try: + data = self.__sockTCP.recv(1024) + except BlockingIOError: + data = bytes() + except Exception as e: + if hasattr(e, 'message'): + self.logger.exception(e.message) + else: + self.logger.exception("Connection...Error") + self.close_socket() + self.open_tcp(self.__portname) + data = bytes() + + if len(data) > 0: + self.rx_msg_list.append(data) + if len(self.rx_msg_list) > 0: + return self.rx_msg_list.pop(0) # FIFO + # else + return None + + def write_API6(self, cmd: bytes | bytearray) -> None: + # SLIP protocol, no CRC8 + if self.__ser: + self.__ser.write(bytes(cmd)) + + diff --git a/src/main/python/main/ayab/engine/control.py b/src/main/python/main/ayab/engine/control.py index 82d33760..d42cb026 100644 --- a/src/main/python/main/ayab/engine/control.py +++ b/src/main/python/main/ayab/engine/control.py @@ -17,6 +17,7 @@ # Copyright 2013-2020 Sebastian Oliva, Christian Obersteiner, # Andreas Müller, Christian Gerbrandt # https://github.com/AllYarnsAreBeautiful/ayab-desktop +# Copyright 2024 Marcus Hoose (eKnitter.com) from __future__ import annotations import logging @@ -177,7 +178,13 @@ def __log_cnfInfo(self, msg: bytes) -> None: api = msg[1] log = "API v" + str(api) if api >= 5: - log += ", FW v" + str(msg[2]) + "." + str(msg[3]) + "." + str(msg[4]) + if len(msg) < 5: + if len(msg) < 4: + log += ", FW v?" + else: + log += ", FW v" + str(msg[2]) + "." + str(msg[3]) + else: + log += ", FW v" + str(msg[2]) + "." + str(msg[3]) + "." + str(msg[4]) suffix = msg[5:21] suffix_null_index = suffix.find(0) suffix_str = suffix[: suffix_null_index + 1].decode() diff --git a/src/main/python/main/ayab/engine/eKnitter-reversed.png b/src/main/python/main/ayab/engine/eKnitter-reversed.png new file mode 100644 index 00000000..6c2b5376 Binary files /dev/null and b/src/main/python/main/ayab/engine/eKnitter-reversed.png differ diff --git a/src/main/python/main/ayab/engine/eKnitter.png b/src/main/python/main/ayab/engine/eKnitter.png new file mode 100644 index 00000000..6d6557ed Binary files /dev/null and b/src/main/python/main/ayab/engine/eKnitter.png differ diff --git a/src/main/python/main/ayab/engine/eKnitter_rc.qrc b/src/main/python/main/ayab/engine/eKnitter_rc.qrc new file mode 100644 index 00000000..b86062d9 --- /dev/null +++ b/src/main/python/main/ayab/engine/eKnitter_rc.qrc @@ -0,0 +1,5 @@ + + + eKnitter.png + + \ No newline at end of file diff --git a/src/main/python/main/ayab/engine/eKnitter_reversed_rc.qrc b/src/main/python/main/ayab/engine/eKnitter_reversed_rc.qrc new file mode 100644 index 00000000..732873e3 --- /dev/null +++ b/src/main/python/main/ayab/engine/eKnitter_reversed_rc.qrc @@ -0,0 +1,5 @@ + + + eKnitter-reversed.png + + \ No newline at end of file diff --git a/src/main/python/main/ayab/engine/engine.py b/src/main/python/main/ayab/engine/engine.py index 34eec425..cb10dab4 100644 --- a/src/main/python/main/ayab/engine/engine.py +++ b/src/main/python/main/ayab/engine/engine.py @@ -17,6 +17,7 @@ # Copyright 2013-2020 Sebastian Oliva, Christian Obersteiner, # Andreas Müller, Christian Gerbrandt # https://github.com/AllYarnsAreBeautiful/ayab-desktop +# Copyright 2024 Marcus Hoose (eKnitter.com) from __future__ import annotations import logging @@ -33,6 +34,7 @@ from .status import Status, StatusTab from .output import FeedbackHandler from .dock_gui import Ui_Dock +from .udp_monitor import UDPMonitor from typing import TYPE_CHECKING, Literal, Optional, cast from ..signal_sender import SignalSender @@ -40,7 +42,7 @@ from ..ayab import GuiMain from .control import Control - +udpMonitor = UDPMonitor() class Engine(SignalSender, QDockWidget): """ Top-level class for the slave thread that communicates with the shield. @@ -56,8 +58,10 @@ class Engine(SignalSender, QDockWidget): def __init__(self, parent: GuiMain): # set up UI super().__init__(parent.signal_receiver) + logging.info("StartUDPMonitor") + udpMonitor.start() self.ui = Ui_Dock() - self.ui.setupUi(self) + self.ui.setupUi(self) # type: ignore self.config: OptionsTab = OptionsTab(parent) self.config.portname = self.__read_portname() self.reload_settings() @@ -69,6 +73,12 @@ def __init__(self, parent: GuiMain): self.control = Control(parent, self) self.__feedback = FeedbackHandler(parent) self.__logger = logging.getLogger(type(self).__name__) + self.setWindowTitle("Machine: " + Machine(self.config.machine).name) + + def close(self) -> bool: + logging.info("StopUDPMonitor") + udpMonitor.stop() + return True def __del__(self) -> None: self.control.stop() @@ -110,6 +120,11 @@ def __activate_ui(self) -> None: def __populate_ports(self, port_list: Optional[list[str]] = None) -> None: combo_box = self.ui.serial_port_dropdown utils.populate_ports(combo_box, port_list) + ip_list = udpMonitor.getIPlist() + print(ip_list) + for item in ip_list: + # TODO: should display the info of the device. + combo_box.addItem(item) # Add Simulation item to indicate operation without machine combo_box.addItem(QCoreApplication.translate("KnitEngine", "Simulation")) @@ -174,7 +189,8 @@ def run(self, operation: Operation) -> None: output = self.control.operate(operation) if output != self.control.notification: self.__feedback.handle(output) - self.control.notification = output + if output.name != "CONNECTING_TO_MACHINE": + self.control.notification = output if operation == Operation.KNIT: self.__handle_status() if self.__canceled or self.control.state == State.FINISHED: diff --git a/src/main/python/main/ayab/engine/engine_fsm.py b/src/main/python/main/ayab/engine/engine_fsm.py index be81c76d..2e61658d 100644 --- a/src/main/python/main/ayab/engine/engine_fsm.py +++ b/src/main/python/main/ayab/engine/engine_fsm.py @@ -17,6 +17,7 @@ # Copyright 2013-2020 Sebastian Oliva, Christian Obersteiner, # Andreas Müller, Christian Gerbrandt # https://github.com/AllYarnsAreBeautiful/ayab-desktop +# Copyright 2024 Marcus Hoose (eKnitter.com) from __future__ import annotations @@ -27,6 +28,7 @@ from .communication import Communication, Token from .communication_mock import CommunicationMock +from .communication_ip import CommunicationIP from .hw_test_communication_mock import HardwareTestCommunicationMock from .output import Output from typing import TYPE_CHECKING @@ -88,6 +90,8 @@ def _API6_connect(control: Control, operation: Operation) -> Output: control.com = CommunicationMock() else: control.com = HardwareTestCommunicationMock() # type: ignore + elif "." in control.portname: + control.com = CommunicationIP() else: control.com = Communication() if not control.com.open_serial(control.portname): diff --git a/src/main/python/main/ayab/engine/options.py b/src/main/python/main/ayab/engine/options.py index 594ec4c7..331f39e2 100644 --- a/src/main/python/main/ayab/engine/options.py +++ b/src/main/python/main/ayab/engine/options.py @@ -30,8 +30,8 @@ from .options_gui import Ui_Options from .mode import Mode from ..machine import Machine -from . import lowercase_e_rc -from . import lowercase_e_reversed_rc +from . import eKnitter_rc +from . import eKnitter_reversed_rc from typing import TYPE_CHECKING, Literal, TypedDict if TYPE_CHECKING: @@ -65,8 +65,8 @@ class OptionsTab(SignalSender, QWidget): @date June 2020 """ - lowercase_e_rc - lowercase_e_reversed_rc + eKnitter_rc + eKnitter_reversed_rc alignment: Alignment auto_mirror: bool continuous_reporting: bool @@ -89,7 +89,7 @@ def __init__(self, parent: GuiMain): # self.__reset() def __setup_ui(self) -> None: - self.ui.setupUi(self) + self.ui.setupUi(self) # type: ignore # Combo boxes Mode.add_items(self.ui.knitting_mode_box) @@ -126,10 +126,10 @@ def update_needles(self) -> None: def __auto_mirror_changed(self) -> None: image_reversed = self.ui.auto_mirror_checkbox.isChecked() if image_reversed: - self.ui.auto_mirror_icon.setPixmap(QPixmap(":/garamond-lowercase-e.png")) + self.ui.auto_mirror_icon.setPixmap(QPixmap(":/eKnitter.png")) else: self.ui.auto_mirror_icon.setPixmap( - QPixmap(":/garamond-lowercase-e-reversed.png") + QPixmap(":/eKnitter-reversed.png") ) self.emit_image_reverser(image_reversed) @@ -180,11 +180,11 @@ def refresh(self) -> None: self.ui.alignment_combo_box.setCurrentIndex(self.alignment.value) if self.auto_mirror: self.ui.auto_mirror_checkbox.setCheckState(Qt.CheckState.Checked) - self.ui.auto_mirror_icon.setPixmap(QPixmap(":/garamond-lowercase-e.png")) + self.ui.auto_mirror_icon.setPixmap(QPixmap(":/eKnitter.png")) else: self.ui.auto_mirror_checkbox.setCheckState(Qt.CheckState.Unchecked) self.ui.auto_mirror_icon.setPixmap( - QPixmap(":/garamond-lowercase-e-reversed.png") + QPixmap(":/eKnitter-reversed.png") ) # self.ui.continuous_reporting_checkbox self.emit_image_reverser(self.auto_mirror) diff --git a/src/main/python/main/ayab/engine/options_gui.ui b/src/main/python/main/ayab/engine/options_gui.ui index 50e3d723..a24bc07c 100644 --- a/src/main/python/main/ayab/engine/options_gui.ui +++ b/src/main/python/main/ayab/engine/options_gui.ui @@ -247,7 +247,7 @@ - 59 + 200 64 diff --git a/src/main/python/main/ayab/engine/status.py b/src/main/python/main/ayab/engine/status.py index c9919ab6..9b02644f 100644 --- a/src/main/python/main/ayab/engine/status.py +++ b/src/main/python/main/ayab/engine/status.py @@ -224,7 +224,7 @@ class StatusTab(Status, QWidget): def __init__(self) -> None: super().__init__() self.ui = Ui_StatusTab() - self.ui.setupUi(self) + self.ui.setupUi(self) # type: ignore def refresh(self) -> None: pass # TODO diff --git a/src/main/python/main/ayab/engine/udp_monitor.py b/src/main/python/main/ayab/engine/udp_monitor.py new file mode 100644 index 00000000..20ba211a --- /dev/null +++ b/src/main/python/main/ayab/engine/udp_monitor.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# This file is part of AYAB. +# +# AYAB is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# AYAB is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with AYAB. If not, see . +# +# Copyright 2024 Marcus Hoose (eKnitter.com) + +import threading +import socket +from time import sleep + +# Port for UDP +localPort = 12345 +broadcast = ("255.255.255.255", 12345) + +queueLock = threading.Lock() + +class UDPMonitor(threading.Thread): + def __init__(self) -> None: + threading.Thread.__init__(self) + self.exitFlag = False + self.__sockUDP: socket.socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + self.__sockUDP.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + self.__sockUDP.settimeout(0.1) + self.__sockUDP.setblocking(False) + # self.__hostname = socket.gethostname() + self.__ip_addresses = socket.gethostbyname_ex('localhost') + self.__ip_address = "0.0.0.0" + self.addresslist: list[socket._RetAddress] = list() + + def __del__(self) -> None: + try: + self.stop() + except Exception: + return + + def run(self) -> None: + print("Starting ") + self.__sockUDP.bind((self.__ip_address ,localPort)) + + Run = True + while Run: + try: + dataadress = self.__sockUDP.recvfrom(250) + print("Recive") + adress = dataadress[1][0] + if not(adress in self.__ip_addresses[2]) and not(adress in self.addresslist): + queueLock.acquire(True) + self.addresslist.append(adress) + queueLock.release() + except: + sleep(1) + queueLock.acquire(True) + Run = not self.exitFlag + queueLock.release() + # print("RUN = ",Run) + + print("Exiting") + self.__sockUDP.close() + del self.__sockUDP + self.__sockUDP = socket.socket() + return + + def stop(self) -> None: + print("Stoping") + queueLock.acquire(True) + self.exitFlag = True + queueLock.release() + sleep(1) + + def getIPlist(self) -> list[str]: + print("GetList") + result: list[str] = list() + if queueLock.acquire(True,0.1): + result.extend(self.addresslist) + queueLock.release() + return result diff --git a/src/main/python/main/ayab/firmware_flash.py b/src/main/python/main/ayab/firmware_flash.py index dac4f0f5..d7ef024b 100644 --- a/src/main/python/main/ayab/firmware_flash.py +++ b/src/main/python/main/ayab/firmware_flash.py @@ -59,7 +59,7 @@ def __init__(self, parent: GuiMain): self.__app_context = parent.app_context self.ui = Ui_Firmware() - self.ui.setupUi(self) + self.ui.setupUi(self) # type: ignore self.ui.flash_firmware.setEnabled(False) self.ui.flash_firmware.setDefault(True) self.load_json() @@ -155,9 +155,15 @@ def execute_flash_command(self) -> bool: if firmware.get("version") == firmware_key: firmware_name = firmware.get("file") - command = self.generate_command( - base_dir, os_name, controller_name, firmware_name - ) + if "eknitter" in controller_name: + command = self.generate_command_eknitter( + base_dir, os_name, controller_name, firmware_name + ) + else: + command = self.generate_command( + base_dir, os_name, controller_name, firmware_name + ) + if command is None: return False # else @@ -176,6 +182,39 @@ def execute_flash_command(self) -> bool: self.close() return True + def generate_command_eknitter( + self, base_dir: str, os_name: str, controller_name: str, firmware_name: str + ) -> Optional[str]: + if os_name == "Windows": + exe_route = self.__app_context.get_resource("ayab/firmware/esp32/win64/esptool.exe") + exe_route = "\"" + exe_route + "\"" + elif os_name == "Linux": + exe_route = self.__app_context.get_resource("ayab/firmware/esp32/linux-amd64/esptool") + elif os_name == "Darwin": # macOS + #exe_route = self.__parent_ui.app_context.get_resource("ayab/firmware/esp32/macos/esptool") + exe_route = "esptool.py" + + binary_file = os.path.join( + self.__app_context.get_resource("ayab/firmware"), firmware_name + ) + + if "fw" in firmware_name: + exec_command = ( + f"{exe_route} -port {self.port} --baud 921600 --no-stub write_flash" + + f" --flash_size 8MB 0x10000 {binary_file} --force" + ) + elif "fs" in firmware_name: + exec_command = ( + f"{exe_route} -port {self.port} --baud 921600 --no-stub write_flash" + + f" --flash_size 8MB 0x670000 {binary_file} --force" + ) + else: + self.__logger.debug("error firmware or filesystem") + return None + + self.__logger.debug(exec_command) + return exec_command + def generate_command( self, base_dir: str, os_name: str, controller_name: str, firmware_name: str ) -> Optional[str]: diff --git a/src/main/python/main/ayab/firmware_flash_gui.ui b/src/main/python/main/ayab/firmware_flash_gui.ui index 2034ee37..d6787d69 100644 --- a/src/main/python/main/ayab/firmware_flash_gui.ui +++ b/src/main/python/main/ayab/firmware_flash_gui.ui @@ -11,7 +11,7 @@ - Firmware Flashing Utility + Update eKnitter True diff --git a/src/main/python/main/ayab/main_gui.ui b/src/main/python/main/ayab/main_gui.ui index a8672e49..21e8042a 100644 --- a/src/main/python/main/ayab/main_gui.ui +++ b/src/main/python/main/ayab/main_gui.ui @@ -11,7 +11,7 @@ - All Yarns Are Beautiful + All Yarns Are Beautiful for eKnitter diff --git a/src/main/python/main/ayab/menu.py b/src/main/python/main/ayab/menu.py index ad2e5977..3f46351e 100644 --- a/src/main/python/main/ayab/menu.py +++ b/src/main/python/main/ayab/menu.py @@ -45,7 +45,7 @@ def __init__(self, parent: GuiMain): self.setNativeMenuBar(False) self.ui = Ui_MenuBar() - self.ui.setupUi(self) + self.ui.setupUi(self) # type: ignore self.setup() def setup(self) -> None: diff --git a/src/main/python/main/ayab/preferences.py b/src/main/python/main/ayab/preferences.py index b0e02d65..c5fad52a 100644 --- a/src/main/python/main/ayab/preferences.py +++ b/src/main/python/main/ayab/preferences.py @@ -213,7 +213,7 @@ def __init__(self, parent: GuiMain): # set up preferences dialog self.__ui = Ui_Prefs() - self.__ui.setupUi(self) + self.__ui.setupUi(self) # type: ignore self.__form = QFormLayout(self.__ui.prefs_group) # add form items diff --git a/src/main/python/main/ayab/scene.py b/src/main/python/main/ayab/scene.py index 6157a86c..1bb25773 100644 --- a/src/main/python/main/ayab/scene.py +++ b/src/main/python/main/ayab/scene.py @@ -89,12 +89,21 @@ def refresh(self) -> None: machine_width = Machine(self.__prefs.value("machine")).width # draw "machine" + rect_pen = QPen(QColor("black")) + rect_pen.setWidthF(0.5) + + rect_height = 7 + rect_orange = QGraphicsRectItem( - -machine_width / 2 - 0.5, -5.5, machine_width / 2 + 0.5, 5 - ) - rect_orange.setBrush(QBrush(QColor("orange"))) - rect_green = QGraphicsRectItem(0, -5.5, machine_width / 2 + 0.5, 5) - rect_green.setBrush(QBrush(QColor("green"))) + -machine_width / 2 - 0.5, -0.5-rect_height, + machine_width / 2 + 0.5, rect_height + ) + rect_orange.setBrush(QBrush(QColor("#C96939"))) + rect_orange.setPen(rect_pen) + + rect_green = QGraphicsRectItem(0, -0.5-rect_height, machine_width / 2 + 0.5, rect_height) + rect_green.setBrush(QBrush(QColor("#8A9A5B"))) + rect_green.setPen(rect_pen) qscene.addItem(rect_orange) qscene.addItem(rect_green) @@ -127,22 +136,22 @@ def refresh(self) -> None: pattern.setPos(pos, 0) # draw limiting lines (start/stop needle) - qscene.addItem( - QGraphicsRectItem( + rect_start = QGraphicsRectItem( self.__start_needle - machine_width / 2 - 0.5, -5.5, 0, pixmap.height() + 5.5, ) - ) - qscene.addItem( - QGraphicsRectItem( + rect_start.setPen(rect_pen) + qscene.addItem(rect_start) + rect_stop = QGraphicsRectItem( self.__stop_needle - machine_width / 2 + 1.5, -5.5, 0, pixmap.height() + 5.5, ) - ) + rect_stop.setPen(rect_pen) + qscene.addItem(rect_stop) # Draw knitting progress qscene.addItem( diff --git a/src/main/python/main/ayab/thread.py b/src/main/python/main/ayab/thread.py index df7b275d..9b951547 100644 --- a/src/main/python/main/ayab/thread.py +++ b/src/main/python/main/ayab/thread.py @@ -17,6 +17,7 @@ # Copyright 2014 Sebastian Oliva, Christian Obersteiner, # Andreas Müller, Christian Gerbrandt # https://github.com/AllYarnsAreBeautiful/ayab-desktop +# Copyright 2024 Marcus Hoose (eKnitter.com) from __future__ import annotations from PySide6.QtCore import QThread @@ -33,7 +34,10 @@ def __init__(self, function: Callable[..., Any], *args: Any, **kwargs: Any): self.kwargs = kwargs def __del__(self) -> None: - self.wait() + try: + self.wait() + except Exception: + return def run(self) -> None: try: diff --git a/src/main/python/main/ayab/transforms.py b/src/main/python/main/ayab/transforms.py index ecb787c3..54f7c9ea 100644 --- a/src/main/python/main/ayab/transforms.py +++ b/src/main/python/main/ayab/transforms.py @@ -158,7 +158,7 @@ class MirrorDialog(QDialog): def __init__(self, parent: Mirrors): super().__init__() # FIXME set the parent widget as GuiMain self.__ui = Ui_Mirrors() - self.__ui.setupUi(self) + self.__ui.setupUi(self) # type: ignore self.__ui.check0.toggled.connect(lambda: parent.toggled(0)) self.__ui.check1.toggled.connect(lambda: parent.toggled(1)) self.__ui.check2.toggled.connect(lambda: parent.toggled(2)) diff --git a/src/main/resources/base/ayab/firmware/eknitter/Place-eKnitter-Firmware-here.txt b/src/main/resources/base/ayab/firmware/eknitter/Place-eKnitter-Firmware-here.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/main/resources/base/ayab/firmware/esp32/linux-amd64/esptool b/src/main/resources/base/ayab/firmware/esp32/linux-amd64/esptool new file mode 100644 index 00000000..b776a9d4 Binary files /dev/null and b/src/main/resources/base/ayab/firmware/esp32/linux-amd64/esptool differ diff --git a/src/main/resources/base/ayab/firmware/esp32/macos/esptool b/src/main/resources/base/ayab/firmware/esp32/macos/esptool new file mode 100644 index 00000000..721a7ac8 Binary files /dev/null and b/src/main/resources/base/ayab/firmware/esp32/macos/esptool differ diff --git a/src/main/resources/base/ayab/firmware/esp32/win64/esptool.exe b/src/main/resources/base/ayab/firmware/esp32/win64/esptool.exe new file mode 100644 index 00000000..5fbf1c6b Binary files /dev/null and b/src/main/resources/base/ayab/firmware/esp32/win64/esptool.exe differ diff --git a/src/main/resources/base/ayab/firmware/firmware.json b/src/main/resources/base/ayab/firmware/firmware.json index 78de3a09..87403683 100644 --- a/src/main/resources/base/ayab/firmware/firmware.json +++ b/src/main/resources/base/ayab/firmware/firmware.json @@ -6,6 +6,18 @@ "version":"1.0.0", "file":"ayab_monolithic_uno.hex" } + ], + "eknitter":[ + { + "url":"/", + "version":"Firmware", + "file":"eKnitter_fw.bin" + }, + { + "url":"/", + "version":"Filesystem", + "file":"eKnitter_fs.bin" + } ] } } diff --git a/windows-build/build.bat b/windows-build/build.bat new file mode 100644 index 00000000..15463e69 --- /dev/null +++ b/windows-build/build.bat @@ -0,0 +1,7 @@ +set VERSION=%1 + +python -m fbs freeze + +python -m fbs installer + +xcopy /Y .\target\AYABSetup.exe .\target\AYAB-eKnitter-v%VERSION%-Setup.exe \ No newline at end of file