Skip to content

Commit b3f9005

Browse files
committed
Rewrite parallel transport, linux dropped data fix
* Fix losing data in last chunk due to how usb * Use twisted.internet.abstract.FileDescriptor instead of StdIO as base * Add an option not to close connection after job for serial/parallel transports. It has caused multiple issues with various transport backends. And for some interactive usecases it might be more convenient
1 parent 4fa8515 commit b3f9005

File tree

8 files changed

+197
-38
lines changed

8 files changed

+197
-38
lines changed

inkcut/device/plugin.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ def disconnect(self):
8888
"""
8989
raise NotImplementedError
9090

91+
@property
92+
def always_disconnect_after_job(self) -> bool:
93+
True
94+
95+
@property
96+
def auto_disconnect_after_job(self) -> bool:
97+
return True
98+
9199

92100
class TestTransport(DeviceTransport):
93101
""" A transport that captures protocol output """
@@ -910,7 +918,7 @@ def submit(self, job, test=False):
910918
for path in model.toSubpathPolygons(m):
911919
for i, p in enumerate(path):
912920
whole_path.lineTo(p)
913-
total_lines += 1
921+
total_lines += 1
914922
total_length = whole_path.length()
915923
total_moved = 0
916924
log.debug("device | Path length: {}".format(total_length))
@@ -1033,7 +1041,7 @@ def submit(self, job, test=False):
10331041
log.error(traceback.format_exc())
10341042
raise
10351043
finally:
1036-
if connection.connected:
1044+
if connection.connected and connection.auto_disconnect_after_job:
10371045
yield defer.maybeDeferred(self.disconnect)
10381046

10391047
#: Set the origin

inkcut/device/transports/parallelport/plugin.py

+135-27
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
import traceback
1616
import subprocess
1717
from glob import glob
18-
from atom.api import Atom, List, Str, Instance
18+
from atom.api import Atom, List, Str, Instance, Value
1919
from inkcut.core.api import Plugin, log
20+
from inkcut.device.plugin import DeviceTransport
21+
from twisted.internet import abstract, fdesc
22+
import twisted.internet
2023

21-
from inkcut.device.transports.raw.plugin import (
22-
RawFdTransport, RawFdProtocol, RawFdConfig
23-
)
24+
from inkcut.device.transports.raw.plugin import RawFdProtocol, RawFdConfig
2425

2526

2627
class ParallelPortDescriptor(Atom):
@@ -32,7 +33,7 @@ def __str__(self):
3233

3334

3435
def find_dev_name(dev):
35-
""" Use udevadm to lookup info on a device
36+
"""Use udevadm to lookup info on a device
3637
3738
Parameters
3839
----------
@@ -46,33 +47,33 @@ def find_dev_name(dev):
4647
4748
"""
4849
try:
49-
cmd = 'udevadm info -a %s' % dev
50+
cmd = "udevadm info -a %s" % dev
5051
manufacturer = ""
5152
product = ""
5253

5354
output = subprocess.check_output(cmd.split())
5455
if sys.version_info.major > 2:
5556
output = output.decode()
56-
for line in output.split('\n'):
57+
for line in output.split("\n"):
5758
log.debug(line)
5859
m = re.search(r'ATTRS{(.+)}=="(.+)"', line)
5960
if m:
6061
k, v = m.groups()
61-
if k == 'manufacturer':
62+
if k == "manufacturer":
6263
manufacturer = v.strip()
63-
elif k == 'product':
64+
elif k == "product":
6465
product = v.strip()
6566
if manufacturer and product:
66-
return '{} {}'.format(manufacturer, product)
67-
log.warning('Could not lookup device info for %s' % dev)
67+
return "{} {}".format(manufacturer, product)
68+
log.warning("Could not lookup device info for %s" % dev)
6869
except Exception as e:
6970
tb = traceback.format_exc()
70-
log.warning('Could not lookup device info for %s %s' % (dev, tb))
71-
return 'usb%s' % dev.split('/')[-1]
71+
log.warning("Could not lookup device info for %s %s" % (dev, tb))
72+
return "usb%s" % dev.split("/")[-1]
7273

7374

7475
def find_ports():
75-
""" Lookup ports from known locations on the system
76+
"""Lookup ports from known locations on the system
7677
7778
Returns
7879
-------
@@ -81,22 +82,22 @@ def find_ports():
8182
8283
"""
8384
ports = []
84-
if 'win32' in sys.platform:
85+
if "win32" in sys.platform:
8586
pass # TODO
86-
elif 'darwin' in sys.platform:
87+
elif "darwin" in sys.platform:
8788
pass # TODO
8889
else:
89-
for p in glob('/dev/lp*'):
90+
for p in glob("/dev/lp*"):
9091
# TODO: Get friendly device name
91-
name = p.split('/')[-1]
92+
name = p.split("/")[-1]
9293
ports.append(ParallelPortDescriptor(device=p, name=name))
9394

94-
for p in glob('/dev/parport*'):
95+
for p in glob("/dev/parport*"):
9596
# TODO: Get friendly device name
96-
name = p.split('/')[-1]
97+
name = p.split("/")[-1]
9798
ports.append(ParallelPortDescriptor(device=p, name=name))
9899

99-
for p in glob('/dev/usb/lp*'):
100+
for p in glob("/dev/usb/lp*"):
100101
name = find_dev_name(p)
101102
ports.append(ParallelPortDescriptor(device=p, name=name))
102103

@@ -122,15 +123,122 @@ def refresh(self):
122123
self.ports = self._default_ports()
123124

124125

125-
class ParallelTransport(RawFdTransport):
126-
""" This is just a wrapper for the RawFdTransport
126+
class ParallelTwistedTransport(abstract.FileDescriptor):
127+
connected = 1
128+
129+
def __init__(
130+
self,
131+
port_name: str,
132+
protocol: RawFdProtocol,
133+
reactor=None,
134+
):
135+
abstract.FileDescriptor.__init__(self, reactor)
136+
self.fd = open(port_name, "r+b")
137+
fdesc.setNonBlocking(self.fileno())
138+
self.protocol = protocol
139+
self.written_something = False
140+
self.protocol.makeConnection(self)
141+
self.startReading()
142+
143+
def fileno(self):
144+
return self.fd.fileno()
145+
146+
def writeSomeData(self, data):
147+
res = fdesc.writeToFD(self.fileno(), data)
148+
self.written_something = self.written_something or res > 0
149+
return res
150+
151+
def doRead(self):
152+
return fdesc.readFromFD(self.fileno(), self.protocol.dataReceived)
153+
154+
def doWrite(self):
155+
self.written_something = False
156+
return abstract.FileDescriptor.doWrite(self)
157+
158+
def stopWriting(self):
159+
abstract.FileDescriptor.stopWriting(self)
160+
161+
def connectionLost(self, reason):
162+
if (
163+
self.written_something
164+
and "linux" in sys.platform
165+
and isinstance(reason.value, twisted.internet.error.ConnectionDone)
166+
):
167+
168+
# Queue up one more iteration in select loop to wait until write is really done.
169+
# Linux usblp driver has a documented quirk where closing it in nonblocking mode can cause dropping
170+
# pending data.
171+
# https://github.com/torvalds/linux/blob/df87d843c6eb4dad31b7bf63614549dd3521fe71/drivers/usb/class/usblp.c#L893C1-L898C1
172+
# https://github.com/karliss/inkcut/issues/56
173+
self.startWriting()
174+
return
175+
176+
abstract.FileDescriptor.connectionLost(self, reason)
177+
self.fd.close()
178+
self.protocol.connectionLost(reason)
179+
log.debug("Closed {}".format(self.fd.name))
180+
181+
182+
class ParallelTransport(DeviceTransport):
183+
"""This is just a wrapper for the RawFdTransport"""
127184

128-
"""
129185
#: Default config
130186
config = Instance(ParallelConfig, ()).tag(config=True)
131187

188+
#: Current path
189+
device_path = Str()
190+
#: Wrapper which forwards twisted protocol to inkcut
191+
_wrapper_protocol = Instance(RawFdProtocol)
192+
193+
#: A raw device connection
194+
connection = Instance(ParallelTwistedTransport)
195+
196+
def connect(self):
197+
config = self.config
198+
device_path = self.device_path = config.device_path
199+
try:
200+
log.debug("-- {} | opened".format(device_path))
201+
self._wrapper_protocol = RawFdProtocol(self, self.protocol)
202+
self.connection = ParallelTwistedTransport(
203+
device_path, self._wrapper_protocol
204+
)
205+
except Exception as e:
206+
#: Make sure to log any issues as these tracebacks can get
207+
#: squashed by twisted
208+
log.error(
209+
"Parallel port open failed {} | {}".format(
210+
device_path, traceback.format_exc()
211+
)
212+
)
213+
raise
214+
215+
def write(self, data):
216+
if not self.connection:
217+
raise IOError("{} is not opened".format(self.device_path))
218+
log.debug("-> {} | {}".format(self.device_path, data))
219+
if hasattr(data, "encode"):
220+
# TODO: cleanup str/byte handling transport should always receive bytes not strings
221+
data = data.encode()
222+
self.last_write = data
223+
self.connection.write(data)
224+
225+
def disconnect(self):
226+
if self.connection:
227+
log.debug("-- {} | closed by request".format(self.device_path))
228+
self.connection.loseConnection()
229+
self.connection = None
230+
231+
def __repr__(self):
232+
return self.device_path
233+
234+
@property
235+
def always_disconnect_after_job(self) -> bool:
236+
return False
237+
238+
@property
239+
def auto_disconnect_after_job(self) -> bool:
240+
return self.config.close_after_job
132241

133-
class ParallelPlugin(Plugin):
134-
""" Plugin for handling parallel port communication
135242

136-
"""
243+
class ParallelPlugin(Plugin):
244+
"""Plugin for handling parallel port communication"""

inkcut/device/transports/parallelport/settings.enaml

+5
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ enamldef ParallelPortSettingsView(Container):
4949
text = QApplication.translate("parallelport", "Refresh")
5050
icon = load_icon("arrow_refresh")
5151
clicked :: model.refresh()
52+
Label:
53+
pass
54+
CheckBox:
55+
text = QApplication.translate("transport", "Disconnect after job")
56+
checked := model.close_after_job

inkcut/device/transports/qtserialport/plugin.py

+8
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ def disconnect(self):
151151
def __repr__(self):
152152
return self.device_path
153153

154+
@property
155+
def auto_disconnect_after_job(self) -> bool:
156+
return self.config.close_after_job
157+
158+
@property
159+
def always_disconnect_after_job(self) -> bool:
160+
return False
161+
154162

155163
class QtSerialPlugin(Plugin):
156164
""" Plugin for handling serial port communication

inkcut/device/transports/raw/plugin.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,26 @@
1414
import sys
1515
import traceback
1616
from atom.atom import set_default
17-
from atom.api import Value, Instance, Str, Enum
17+
from atom.api import Value, Instance, Str, Enum, Bool
1818
from inkcut.core.api import Plugin, Model, log
19-
from inkcut.device.plugin import DeviceTransport
19+
from inkcut.device.plugin import DeviceTransport, DeviceProtocol
2020
from twisted.internet import reactor, stdio
2121
from twisted.internet.protocol import Protocol, connectionDone
2222

2323

2424
class RawFdConfig(Model):
2525
device_path = Str("/dev/null").tag(config=True)
26-
mode = Enum('r+b', 'wb', 'r+', 'w').tag(config=True)
26+
mode = Enum("r+b", "wb", "r+", "w").tag(config=True)
27+
close_after_job = Bool(True).tag(config=True)
2728

2829

2930
class RawFdProtocol(Protocol, object):
3031
"""Make a twisted protocol that delegates to the inkcut protocol
3132
implementation to have a consistent api (and use proper pep 8 formatting!).
3233
3334
"""
34-
def __init__(self, transport, protocol):
35+
36+
def __init__(self, transport: DeviceTransport, protocol: DeviceProtocol):
3537
self._transport = transport
3638
self.delegate = protocol
3739

@@ -47,7 +49,8 @@ def dataReceived(self, data):
4749

4850
def connectionLost(self, reason=connectionDone):
4951
self._transport.connected = False
50-
self._transport.fd = None
52+
if hasattr(self._transport, "fd"):
53+
self._transport.fd = None
5154
device_path = self._transport.device_path
5255
log.debug("-- {} | dropped: {}".format(device_path, reason))
5356
self.delegate.connection_lost()
@@ -73,7 +76,7 @@ class RawFdTransport(DeviceTransport):
7376
def connect(self):
7477
config = self.config
7578
device_path = self.device_path = config.device_path
76-
if 'win32' in sys.platform:
79+
if "win32" in sys.platform:
7780
# Well, technically it works, but only with stdin and stdout
7881
raise OSError("Raw device support cannot be used on Windows")
7982

@@ -93,7 +96,7 @@ def write(self, data):
9396
if not self.connection:
9497
raise IOError("{} is not opened".format(self.device_path))
9598
log.debug("-> {} | {}".format(self.device_path, data))
96-
if hasattr(data, 'encode'):
99+
if hasattr(data, "encode"):
97100
data = data.encode()
98101
self.last_write = data
99102
self.connection.write(data)
@@ -104,6 +107,13 @@ def disconnect(self):
104107
self.connection.loseConnection()
105108
self.connection = None
106109

110+
@property
111+
def auto_disconnect_after_job(self) -> bool:
112+
return self.config.close_after_job
113+
114+
@property
115+
def always_disconnect_after_job(self) -> bool:
116+
return False
117+
107118
def __repr__(self):
108119
return self.device_path
109-

inkcut/device/transports/raw/settings.enaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Created on Feb 2, 2019
1212
"""
1313
from inkcut.core.utils import load_icon
1414
from enaml.qt.QtWidgets import QApplication
15-
from enaml.widgets.api import Container, Form, Label, Field, ObjectCombo
15+
from enaml.widgets.api import Container, Form, Label, Field, ObjectCombo, CheckBox
1616

1717

1818
enamldef RawFdSettingsView(Container):
@@ -28,3 +28,8 @@ enamldef RawFdSettingsView(Container):
2828
ObjectCombo:
2929
items << list(model.get_member('mode').items)
3030
selected := model.mode
31+
Label:
32+
pass
33+
CheckBox:
34+
text = QApplication.translate("transport", "Disconnect after job")
35+
checked := model.close_after_job

0 commit comments

Comments
 (0)