Skip to content

Commit 0a4b204

Browse files
yekomSdl1com
authored andcommitted
add eknitter ip handling
1 parent 95d480d commit 0a4b204

File tree

7 files changed

+344
-4
lines changed

7 files changed

+344
-4
lines changed

src/main/python/main/ayab/ayab.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# Copyright 2014 Sebastian Oliva, Christian Obersteiner,
1818
# Andreas Müller, Christian Gerbrandt
1919
# https://github.com/AllYarnsAreBeautiful/ayab-desktop
20+
# Copyright 2024 Marcus Hoose (eKnitter.com)
2021
"""Provides a graphical interface for users to operate AYAB."""
2122

2223
from __future__ import annotations
@@ -58,6 +59,11 @@ class GuiMain(QMainWindow):
5859
components from `menu_gui.ui`.
5960
"""
6061

62+
def closeEvent(self, event):
63+
print("close")
64+
self.engine.close()
65+
event.accept()
66+
6167
def __init__(self, app_context: AppContext):
6268
super().__init__()
6369
self.app_context = app_context
@@ -131,6 +137,7 @@ def __set_prefs(self) -> None:
131137

132138
def __quit(self) -> None:
133139
logging.debug("Quitting")
140+
self.engine.close()
134141
instance = QCoreApplication.instance()
135142
if instance is not None:
136143
instance.quit()
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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 <http://www.gnu.org/licenses/>.
16+
#
17+
# Copyright 2024 Marcus Hoose (eKnitter.com)
18+
"""Handles the IP communication protocol.
19+
20+
This module handles IP communication, currently works in a synchronous way.
21+
"""
22+
23+
from .communication import *
24+
25+
import socket
26+
import ipaddress
27+
28+
import logging
29+
import pprint
30+
from time import sleep
31+
32+
# Port for TCP
33+
remotePort = 12346
34+
35+
class CommunicationIP(Communication):
36+
def __init__(self):
37+
logging.basicConfig(level=logging.DEBUG)
38+
self.logger = logging.getLogger(type(self).__name__)
39+
self.__tarAddressPort = ("255.255.255.255", 12345)
40+
self.__sockTCP = None
41+
# socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
42+
self.rx_msg_list = list()
43+
self.version = 6
44+
45+
def __del__(self):
46+
return self.close_socket()
47+
48+
def is_open(self):
49+
if self.__sockTCP is not None:
50+
return True
51+
else:
52+
return False
53+
54+
def open_serial(self, portname=None):
55+
print("open: " , portname)
56+
return self.open_tcp(portname)
57+
58+
def close_serial(self):
59+
return True
60+
61+
def open_tcp(self, pPortname=None):
62+
try:
63+
self.__portname = pPortname
64+
self.__tarAddressPort = (self.__portname, remotePort)
65+
self.__sockTCP = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
66+
self.__sockTCP.settimeout(10.0)
67+
self.__sockTCP.connect(self.__tarAddressPort)
68+
self.__sockTCP.settimeout(0.0)
69+
self.__sockTCP.setblocking(False)
70+
self.logger.info("Open TCP Socket successful")
71+
return True
72+
except:
73+
self.logger.info("Open TCP Socket faild")
74+
return False
75+
76+
def close_socket(self):
77+
if self.__sockTCP is not None:
78+
try:
79+
self.__sockTCP.close()
80+
del self.__sockTCP
81+
self.logger.info("Closing TCP Socket successful.")
82+
except:
83+
self.logger.warning("Closing TCP Socket failed. (mem Leak?)")
84+
self.__sockTCP = None
85+
86+
def send(self, data):
87+
if self.__sockTCP is not None:
88+
try:
89+
self.__sockTCP.send(bytes(data))
90+
# self.logger.info("SEND b'"+data+"'")
91+
sleep(0.5)
92+
except Exception as e:
93+
if hasattr(e, 'message'):
94+
self.logger.exception(e.message)
95+
else:
96+
self.logger.exception("Connection...Error")
97+
self.close_socket()
98+
99+
# NB this method must be the same for all API versions
100+
def req_info(self):
101+
self.send([Token.reqInfo.value,self.version])
102+
103+
def req_test_API6(self):
104+
self.send([Token.reqTest.value])
105+
106+
def req_start_API6(self, start_needle, stop_needle,
107+
continuous_reporting, disable_hardware_beep):
108+
"""Send a start message to the device."""
109+
data = bytearray()
110+
data.append(Token.reqStart.value)
111+
data.append(start_needle)
112+
data.append(stop_needle)
113+
data.append(
114+
1 * continuous_reporting +
115+
2 * (not disable_hardware_beep))
116+
hash = 0
117+
hash = add_crc(hash, data)
118+
data.append(hash)
119+
data = self.send(data)
120+
121+
def req_init_API6(self, machine: Machine):
122+
"""Send a start message to the device."""
123+
data = bytearray()
124+
data.append(Token.reqInit.value)
125+
data.append(machine.value)
126+
hash = 0
127+
hash = add_crc(hash, data)
128+
data.append(hash)
129+
data = self.send(data)
130+
131+
def cnf_line_API6(self, line_number, color, flags, line_data):
132+
"""Send a line of data via the serial port.
133+
134+
Send a line of data to the serial port. All arguments are mandatory.
135+
The data sent here is parsed by the Arduino controller which sets the
136+
knitting needles accordingly.
137+
138+
Args:
139+
line_number (int): The line number to be sent.
140+
color (int): The yarn color to be sent.
141+
flags (int): The flags sent to the controller.
142+
line_data (bytes): The bytearray to be sent to needles.
143+
"""
144+
data = bytearray()
145+
data.append(Token.cnfLine.value)
146+
data.append(line_number)
147+
data.append(color)
148+
data.append(flags)
149+
data.extend(line_data)
150+
hash = 0
151+
hash = add_crc(hash, data)
152+
data.append(hash)
153+
data = self.send(data)
154+
155+
def update_API6(self):
156+
"""Read data from serial and parse as SLIP packet."""
157+
return self.parse_API6(self.read_API6())
158+
159+
def parse_API6(self, msg):
160+
if msg is None:
161+
return None, Token.none, 0
162+
# else
163+
for t in list(Token):
164+
if msg[0] == t.value:
165+
return msg, t, msg[1]
166+
# fallthrough
167+
self.logger.debug("unknown message: ") # drop crlf
168+
pp = pprint.PrettyPrinter(indent=4)
169+
pp.pprint(msg[1: -1].decode())
170+
return msg, Token.unknown, 0
171+
172+
def read_API6(self):
173+
"""Read data from serial as SLIP packet."""
174+
if self.__sockTCP is not None:
175+
try:
176+
data = self.__sockTCP.recv(1024)
177+
except BlockingIOError:
178+
data = bytes()
179+
except Exception as e:
180+
if hasattr(e, 'message'):
181+
self.logger.exception(e.message)
182+
else:
183+
self.logger.exception("Connection...Error")
184+
self.close_socket()
185+
self.open_tcp(self.__portname)
186+
data = bytes()
187+
188+
if len(data) > 0:
189+
self.rx_msg_list.append(data)
190+
if len(self.rx_msg_list) > 0:
191+
return self.rx_msg_list.pop(0) # FIFO
192+
# else
193+
return None
194+
195+
def write_API6(self, cmd: str) -> None:
196+
# SLIP protocol, no CRC8
197+
if self.__ser:
198+
self.__ser.write(cmd)
199+
200+
201+
# CRC algorithm after Maxim/Dallas
202+
def add_crc(crc, data):
203+
for i in range(len(data)):
204+
n = data[i]
205+
for j in range(8):
206+
f = (crc ^ n) & 1
207+
crc >>= 1
208+
if f:
209+
crc ^= 0x8C
210+
n >>= 1
211+
return crc & 0xFF
212+
213+
214+
class CommunicationException(Exception):
215+
pass

src/main/python/main/ayab/engine/control.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# Copyright 2013-2020 Sebastian Oliva, Christian Obersteiner,
1818
# Andreas Müller, Christian Gerbrandt
1919
# https://github.com/AllYarnsAreBeautiful/ayab-desktop
20+
# Copyright 2024 Marcus Hoose (eKnitter.com)
2021

2122
from __future__ import annotations
2223
import logging
@@ -177,7 +178,13 @@ def __log_cnfInfo(self, msg: bytes) -> None:
177178
api = msg[1]
178179
log = "API v" + str(api)
179180
if api >= 5:
180-
log += ", FW v" + str(msg[2]) + "." + str(msg[3]) + "." + str(msg[4])
181+
if len(msg) < 5:
182+
if len(msg) < 4:
183+
log += ", FW v?"
184+
else:
185+
log += ", FW v" + str(msg[2]) + "." + str(msg[3])
186+
else:
187+
log += ", FW v" + str(msg[2]) + "." + str(msg[3]) + "." + str(msg[4])
181188
suffix = msg[5:21]
182189
suffix_null_index = suffix.find(0)
183190
suffix_str = suffix[: suffix_null_index + 1].decode()

src/main/python/main/ayab/engine/engine.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# Copyright 2013-2020 Sebastian Oliva, Christian Obersteiner,
1818
# Andreas Müller, Christian Gerbrandt
1919
# https://github.com/AllYarnsAreBeautiful/ayab-desktop
20+
# Copyright 2024 Marcus Hoose (eKnitter.com)
2021

2122
from __future__ import annotations
2223
import logging
@@ -33,14 +34,15 @@
3334
from .status import Status, StatusTab
3435
from .output import FeedbackHandler
3536
from .dock_gui import Ui_Dock
37+
from .udp_monitor import UDPMonitor
3638
from typing import TYPE_CHECKING, Literal, Optional, cast
3739
from ..signal_sender import SignalSender
3840

3941
if TYPE_CHECKING:
4042
from ..ayab import GuiMain
4143
from .control import Control
4244

43-
45+
udpMonitor = UDPMonitor()
4446
class Engine(SignalSender, QDockWidget):
4547
"""
4648
Top-level class for the slave thread that communicates with the shield.
@@ -56,6 +58,8 @@ class Engine(SignalSender, QDockWidget):
5658
def __init__(self, parent: GuiMain):
5759
# set up UI
5860
super().__init__(parent.signal_receiver)
61+
logging.info("StartUDPMonitor")
62+
udpMonitor.start()
5963
self.ui = Ui_Dock()
6064
self.ui.setupUi(self)
6165
self.config: OptionsTab = OptionsTab(parent)
@@ -69,6 +73,11 @@ def __init__(self, parent: GuiMain):
6973
self.control = Control(parent, self)
7074
self.__feedback = FeedbackHandler(parent)
7175
self.__logger = logging.getLogger(type(self).__name__)
76+
self.setWindowTitle("Machine: " + Machine(self.config.machine).name)
77+
78+
def close(self) -> None:
79+
logging.info("StopUDPMonitor")
80+
udpMonitor.stop()
7281

7382
def __del__(self) -> None:
7483
self.control.stop()
@@ -110,6 +119,11 @@ def __activate_ui(self) -> None:
110119
def __populate_ports(self, port_list: Optional[list[str]] = None) -> None:
111120
combo_box = self.ui.serial_port_dropdown
112121
utils.populate_ports(combo_box, port_list)
122+
ip_list = udpMonitor.getIPlist()
123+
print(ip_list)
124+
for item in ip_list:
125+
# TODO: should display the info of the device.
126+
combo_box.addItem(item)
113127
# Add Simulation item to indicate operation without machine
114128
combo_box.addItem(QCoreApplication.translate("KnitEngine", "Simulation"))
115129

@@ -174,7 +188,8 @@ def run(self, operation: Operation) -> None:
174188
output = self.control.operate(operation)
175189
if output != self.control.notification:
176190
self.__feedback.handle(output)
177-
self.control.notification = output
191+
if output.name != "CONNECTING_TO_MACHINE":
192+
self.control.notification = output
178193
if operation == Operation.KNIT:
179194
self.__handle_status()
180195
if self.__canceled or self.control.state == State.FINISHED:

src/main/python/main/ayab/engine/engine_fsm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# Copyright 2013-2020 Sebastian Oliva, Christian Obersteiner,
1818
# Andreas Müller, Christian Gerbrandt
1919
# https://github.com/AllYarnsAreBeautiful/ayab-desktop
20+
# Copyright 2024 Marcus Hoose (eKnitter.com)
2021

2122
from __future__ import annotations
2223

@@ -27,6 +28,7 @@
2728

2829
from .communication import Communication, Token
2930
from .communication_mock import CommunicationMock
31+
from .communication_ip import CommunicationIP
3032
from .hw_test_communication_mock import HardwareTestCommunicationMock
3133
from .output import Output
3234
from typing import TYPE_CHECKING
@@ -88,6 +90,8 @@ def _API6_connect(control: Control, operation: Operation) -> Output:
8890
control.com = CommunicationMock()
8991
else:
9092
control.com = HardwareTestCommunicationMock() # type: ignore
93+
elif "." in control.portname:
94+
control.com = CommunicationIP()
9195
else:
9296
control.com = Communication()
9397
if not control.com.open_serial(control.portname):

0 commit comments

Comments
 (0)