meerk40t 0.9.7010__py2.py3-none-any.whl → 0.9.7020__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. meerk40t/balormk/galvo_commands.py +1 -2
  2. meerk40t/core/elements/branches.py +18 -4
  3. meerk40t/core/elements/element_treeops.py +34 -0
  4. meerk40t/core/elements/elements.py +49 -63
  5. meerk40t/core/elements/offset_clpr.py +4 -3
  6. meerk40t/core/elements/shapes.py +1 -1
  7. meerk40t/core/elements/testcases.py +105 -0
  8. meerk40t/core/node/op_cut.py +9 -8
  9. meerk40t/core/node/op_dots.py +8 -8
  10. meerk40t/core/node/op_engrave.py +7 -7
  11. meerk40t/core/node/op_raster.py +8 -8
  12. meerk40t/extra/encode_detect.py +8 -2
  13. meerk40t/extra/hershey.py +2 -3
  14. meerk40t/extra/inkscape.py +3 -5
  15. meerk40t/extra/outerworld.py +2 -3
  16. meerk40t/extra/param_functions.py +1 -1
  17. meerk40t/grbl/device.py +4 -1
  18. meerk40t/grbl/gui/grblcontroller.py +2 -2
  19. meerk40t/gui/busy.py +75 -13
  20. meerk40t/gui/choicepropertypanel.py +364 -375
  21. meerk40t/gui/consolepanel.py +3 -3
  22. meerk40t/gui/hersheymanager.py +13 -3
  23. meerk40t/gui/laserpanel.py +12 -7
  24. meerk40t/gui/materialmanager.py +33 -6
  25. meerk40t/gui/plugin.py +9 -3
  26. meerk40t/gui/propertypanels/operationpropertymain.py +1 -1
  27. meerk40t/gui/ribbon.py +4 -1
  28. meerk40t/gui/scene/widget.py +1 -1
  29. meerk40t/gui/scenewidgets/rectselectwidget.py +19 -16
  30. meerk40t/gui/scenewidgets/selectionwidget.py +26 -20
  31. meerk40t/gui/simpleui.py +13 -8
  32. meerk40t/gui/simulation.py +22 -2
  33. meerk40t/gui/spoolerpanel.py +2 -2
  34. meerk40t/gui/tips.py +2 -3
  35. meerk40t/gui/toolwidgets/toolmeasure.py +4 -1
  36. meerk40t/gui/wxmeerk40t.py +6 -3
  37. meerk40t/gui/wxmmain.py +72 -6
  38. meerk40t/gui/wxmscene.py +2 -6
  39. meerk40t/gui/wxmtree.py +17 -11
  40. meerk40t/gui/wxutils.py +1 -1
  41. meerk40t/image/imagetools.py +20 -5
  42. meerk40t/kernel/kernel.py +21 -2
  43. meerk40t/main.py +1 -1
  44. meerk40t/network/console_server.py +52 -14
  45. meerk40t/network/web_server.py +15 -1
  46. meerk40t/ruida/device.py +5 -1
  47. meerk40t/tools/polybool.py +2 -1
  48. meerk40t/tools/shxparser.py +92 -34
  49. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/METADATA +1 -1
  50. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/RECORD +55 -54
  51. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/LICENSE +0 -0
  52. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/WHEEL +0 -0
  53. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/entry_points.txt +0 -0
  54. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/top_level.txt +0 -0
  55. {meerk40t-0.9.7010.dist-info → meerk40t-0.9.7020.dist-info}/zip-safe +0 -0
meerk40t/gui/wxmmain.py CHANGED
@@ -2,6 +2,7 @@ import datetime
2
2
  import os
3
3
  import platform
4
4
  import sys
5
+ import threading
5
6
  from functools import partial
6
7
  from math import isinf
7
8
 
@@ -126,6 +127,42 @@ from .mwindow import MWindow
126
127
  _ = wx.GetTranslation
127
128
  MULTIPLE = "<Multiple files loaded>"
128
129
 
130
+ class GUIThread:
131
+ """
132
+ This will take from any thread a command to be executed and inserts it into the main thread
133
+ This prevents threading & lock issues exhibited by passing along commands
134
+ via ``consoleserver`` or ``webserver``
135
+ """
136
+ def __init__(self, context, *args, **kwargs):
137
+ self.context = context
138
+ self._execution_lock = threading.Lock()
139
+ self._execution_buffer = []
140
+ self._execution_timer = Job(
141
+ process=self.execute_command,
142
+ job_name="console-execute",
143
+ interval=0.1,
144
+ run_main=True,
145
+ )
146
+ self.context.kernel.register("gui/handover", self.process_command)
147
+
148
+ def execute_command(self):
149
+ cmd = ""
150
+ another = False
151
+ with self._execution_lock:
152
+ if self._execution_buffer:
153
+ cmd = self._execution_buffer[0]
154
+ self._execution_buffer.pop(0)
155
+ another = len(self._execution_buffer) > 0
156
+ if cmd:
157
+ self.context(cmd + "\n")
158
+ if another:
159
+ self.context.kernel.schedule(self._execution_timer)
160
+
161
+ def process_command(self, command):
162
+ with self._execution_lock:
163
+ self._execution_buffer.append(command)
164
+ self.context.kernel.schedule(self._execution_timer)
165
+
129
166
  class Autosaver:
130
167
  """
131
168
  Minimal autosave functionality.
@@ -137,7 +174,7 @@ class Autosaver:
137
174
  def __init__(self, context, *args, **kwargs):
138
175
  self.context = context
139
176
  self.needs_saving = False
140
- safe_dir = os.path.realpath(get_safe_path(self.context.kernel.name))
177
+ safe_dir = self.context.kernel.os_information["WORKDIR"]
141
178
  self.autosave_file = os.path.join(safe_dir, "_autosave.svg")
142
179
 
143
180
  choices = [
@@ -394,6 +431,7 @@ class MeerK40t(MWindow):
394
431
  self.tips_at_startup()
395
432
  self.parametric_info = None
396
433
  self.autosave = Autosaver(self.context)
434
+ self.handover = GUIThread(self.context)
397
435
  kernel = self.context.kernel
398
436
  if hasattr(kernel.args, "maximized") and kernel.args.maximized:
399
437
  self.Maximize()
@@ -538,10 +576,18 @@ class MeerK40t(MWindow):
538
576
  myPilImage = Image.new(
539
577
  "RGB", (myWxImage.GetWidth(), myWxImage.GetHeight())
540
578
  )
541
- myPilImage.frombytes(myWxImage.GetData())
579
+ try:
580
+ byte_data = bytes(myWxImage.GetData())
581
+ myPilImage.frombytes(byte_data)
582
+ except TypeError as e:
583
+ console = self.context.root.channel("console")
584
+ console(f"Error while pasting image: {e}")
585
+ return None
542
586
  return myPilImage
543
587
 
544
588
  image = imageToPil(WxBitmapToWxImage(bmp))
589
+ if image is None:
590
+ return
545
591
  dpi = DEFAULT_PPI
546
592
  matrix = Matrix(f"scale({UNITS_PER_PIXEL})")
547
593
  # _("Paste image")
@@ -2345,6 +2391,22 @@ class MeerK40t(MWindow):
2345
2391
  > 0,
2346
2392
  },
2347
2393
  )
2394
+ secondary_commands = [
2395
+ "element union",
2396
+ "element difference",
2397
+ "element xor",
2398
+ "element intersection",
2399
+ ]
2400
+ try:
2401
+ import pyclipr
2402
+ primary_commands = [
2403
+ "clipper union",
2404
+ "clipper difference",
2405
+ "clipper xor",
2406
+ "clipper intersection",
2407
+ ]
2408
+ except ImportError:
2409
+ primary_commands = list(secondary_commands)
2348
2410
  kernel.register(
2349
2411
  "button/geometry/Union",
2350
2412
  {
@@ -2352,7 +2414,8 @@ class MeerK40t(MWindow):
2352
2414
  "icon": icon_cag_union,
2353
2415
  "tip": _("Create a union of the selected elements"),
2354
2416
  "help": "cag",
2355
- "action": exec_in_undo_scope("Union", "element union\n"),
2417
+ "action": exec_in_undo_scope("Union", f"{primary_commands[0]}\n"),
2418
+ "action_right": exec_in_undo_scope("Union", f"{secondary_commands[0]}\n"),
2356
2419
  "size": bsize_small,
2357
2420
  "rule_enabled": lambda cond: len(
2358
2421
  list(kernel.elements.elems(emphasized=True))
@@ -2367,7 +2430,8 @@ class MeerK40t(MWindow):
2367
2430
  "icon": icon_cag_subtract,
2368
2431
  "tip": _("Create a difference of the selected elements"),
2369
2432
  "help": "cag",
2370
- "action": exec_in_undo_scope("Difference", "element difference\n"),
2433
+ "action": exec_in_undo_scope("Difference", f"{primary_commands[1]}\n"),
2434
+ "action_right": exec_in_undo_scope("Difference", f"{secondary_commands[1]}\n"),
2371
2435
  "size": bsize_small,
2372
2436
  "rule_enabled": lambda cond: len(
2373
2437
  list(kernel.elements.elems(emphasized=True))
@@ -2382,7 +2446,8 @@ class MeerK40t(MWindow):
2382
2446
  "icon": icon_cag_xor,
2383
2447
  "tip": _("Create a xor of the selected elements"),
2384
2448
  "help": "cag",
2385
- "action": exec_in_undo_scope("Xor", "element xor\n"),
2449
+ "action": exec_in_undo_scope("XOR", f"{primary_commands[2]}\n"),
2450
+ "action_right": exec_in_undo_scope("XOR", f"{secondary_commands[2]}\n"),
2386
2451
  "size": bsize_small,
2387
2452
  "rule_enabled": lambda cond: len(
2388
2453
  list(kernel.elements.elems(emphasized=True))
@@ -2397,7 +2462,8 @@ class MeerK40t(MWindow):
2397
2462
  "icon": icon_cag_common,
2398
2463
  "tip": _("Create a intersection of the selected elements"),
2399
2464
  "help": "cag",
2400
- "action": exec_in_undo_scope("Intersection", "element intersection\n"),
2465
+ "action": exec_in_undo_scope("Intersection", f"{primary_commands[3]}\n"),
2466
+ "action_right": exec_in_undo_scope("Intersection", f"{secondary_commands[3]}\n"),
2401
2467
  "size": bsize_small,
2402
2468
  "rule_enabled": lambda cond: len(
2403
2469
  list(kernel.elements.elems(emphasized=True))
meerk40t/gui/wxmscene.py CHANGED
@@ -156,13 +156,9 @@ class MeerK40tScenePanel(wx.Panel):
156
156
  self.context.setting(bool, "clear_magnets", True)
157
157
 
158
158
  # Save / Load the content of magnets
159
- from os.path import join, realpath
159
+ from os.path import join
160
160
 
161
- from meerk40t.kernel.functions import get_safe_path
162
-
163
- self._magnet_file = join(
164
- realpath(get_safe_path(self.context.kernel.name)), "magnets.cfg"
165
- )
161
+ self._magnet_file = join(self.context.kernel.os_information["WORKDIR"], "magnets.cfg")
166
162
  self.load_magnets()
167
163
  # Add a plugin routine to be called at the time of a full new start
168
164
  context.kernel.register(
meerk40t/gui/wxmtree.py CHANGED
@@ -580,7 +580,7 @@ class ShadowTree:
580
580
  self.tree_images = None
581
581
  self.name = "Project"
582
582
  self._freeze = False
583
- testsize = dip_size(self, 20, 20)
583
+ testsize = dip_size(self.wxtree, 20, 20)
584
584
  self.iconsize = testsize[1]
585
585
  self.iconstates = {}
586
586
  self.last_call = 0
@@ -1430,17 +1430,23 @@ class ShadowTree:
1430
1430
  @param kwargs:
1431
1431
  @return:
1432
1432
  """
1433
- parent = node.parent
1434
- parent_item = parent._item
1435
- if parent_item is None:
1436
- # We are appending items in tree before registration.
1433
+ try:
1434
+ parent = node.parent
1435
+ parent_item = parent._item
1436
+ if parent_item is None:
1437
+ # We are appending items in tree before registration.
1438
+ return
1439
+ tree = self.wxtree
1440
+ if pos is None:
1441
+ node._item = tree.AppendItem(parent_item, self.name)
1442
+ else:
1443
+ node._item = tree.InsertItem(parent_item, pos, self.name)
1444
+ tree.SetItemData(node._item, node)
1445
+ except Exception as e:
1446
+ # Invalid tree?
1447
+ self.context.signal("rebuild_tree", "all")
1448
+ print (f"We encountered an error at node registration: {e}")
1437
1449
  return
1438
- tree = self.wxtree
1439
- if pos is None:
1440
- node._item = tree.AppendItem(parent_item, self.name)
1441
- else:
1442
- node._item = tree.InsertItem(parent_item, pos, self.name)
1443
- tree.SetItemData(node._item, node)
1444
1450
  self.update_decorations(node, False)
1445
1451
  wxcolor = self.wxtree.GetForegroundColour()
1446
1452
  attribute_to_try = "fill" if node.type == "elem text" else "stroke"
meerk40t/gui/wxutils.py CHANGED
@@ -1222,7 +1222,7 @@ class StaticBoxSizer(wx.StaticBoxSizer):
1222
1222
  if label is None:
1223
1223
  label = ""
1224
1224
  self.sbox = wx.StaticBox(parent, id, label=label)
1225
- self.sbox.SetMinSize(dip_size(self, 50, 50))
1225
+ self.sbox.SetMinSize(dip_size(self.sbox, 50, 50))
1226
1226
  super().__init__(self.sbox, orientation)
1227
1227
  self.parent = parent
1228
1228
 
@@ -43,7 +43,12 @@ def img_to_polygons(
43
43
  _, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
44
44
 
45
45
  # Find contours in the binary image
46
- contours, hierarchies = cv2.findContours(th2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
46
+ try:
47
+ contours, hierarchies = cv2.findContours(th2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
48
+ except ValueError:
49
+ # Invalid data
50
+ return ([], [])
51
+
47
52
  # print(f"Found {len(contours)} contours and {len(hierarchies)} hierarchies")
48
53
  width, height = node_image.size
49
54
  minarea = int(minimal / 100.0 * width * height)
@@ -147,9 +152,14 @@ def do_innerwhite(
147
152
  _, thresh = cv2.threshold(gray, 250, 255, cv2.THRESH_BINARY)
148
153
 
149
154
  # Find contours
150
- contours, hierarchy = cv2.findContours(
151
- thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
152
- )
155
+ try:
156
+ contours, hierarchy = cv2.findContours(
157
+ thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
158
+ )
159
+ except ValueError:
160
+ # Invalid data
161
+ continue
162
+
153
163
  linecandidates = list()
154
164
 
155
165
  minarea = int(minimal / 100.0 * width * height)
@@ -402,7 +412,12 @@ def img_to_rectangles(
402
412
  _, th2 = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
403
413
 
404
414
  # Find contours in the binary image
405
- contours, hierarchies = cv2.findContours(th2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
415
+ try:
416
+ contours, hierarchies = cv2.findContours(th2, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
417
+ except ValueError:
418
+ # Invalid data
419
+ return ([], [])
420
+
406
421
  # print(f"Found {len(contours)} contours and {len(hierarchies)} hierarchies")
407
422
  width, height = node_image.size
408
423
  minarea = int(minimal / 100.0 * width * height)
meerk40t/kernel/kernel.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import functools
2
2
  import inspect
3
3
  import os
4
- import platform
5
4
  import re
6
5
  import subprocess
7
6
  import threading
@@ -181,9 +180,20 @@ class Kernel(Settings):
181
180
  # Arguments Objects
182
181
  self.args = None
183
182
 
183
+ self.os_information = self._get_environment()
184
+
184
185
  def __str__(self):
185
186
  return f"Kernel({self.name}, {self.profile}, {self.version})"
186
187
 
188
+ def _get_environment(self):
189
+ from platform import system
190
+ from tempfile import gettempdir
191
+ return {
192
+ "OS_NAME": system(),
193
+ "OS_TEMPDIR": os.path.realpath(gettempdir()),
194
+ "WORKDIR": os.path.realpath(get_safe_path(self.name, create=True)),
195
+ }
196
+
187
197
  def set_language(self, language, localedir="locale"):
188
198
  from . import set_language
189
199
 
@@ -1906,6 +1916,8 @@ class Kernel(Settings):
1906
1916
  # Could be recurring job. Reset on reschedule.
1907
1917
  except AttributeError:
1908
1918
  pass
1919
+ if job.job_name is None:
1920
+ job.job_name = f"job_{id(job)}"
1909
1921
  self.jobs[job.job_name] = job
1910
1922
  return job
1911
1923
 
@@ -2667,6 +2679,9 @@ class Kernel(Settings):
2667
2679
  channel(_("----------"))
2668
2680
  channel(_("Scheduled Processes:"))
2669
2681
  for i, job_name in enumerate(self.jobs):
2682
+ if job_name is None:
2683
+ channel(_("Empty job definition..."))
2684
+ continue
2670
2685
  job = self.jobs[job_name]
2671
2686
  parts = list()
2672
2687
  parts.append(f"{i + 1}:")
@@ -2737,6 +2752,8 @@ class Kernel(Settings):
2737
2752
  channel(_("Timers:"))
2738
2753
  i = 0
2739
2754
  for job_name in self.jobs:
2755
+ if job_name is None:
2756
+ continue
2740
2757
  if not job_name.startswith("timer"):
2741
2758
  continue
2742
2759
  i += 1
@@ -2772,6 +2789,8 @@ class Kernel(Settings):
2772
2789
  skipped = False
2773
2790
  canceled = False
2774
2791
  for job_name in list(self.jobs):
2792
+ if job_name is None:
2793
+ continue
2775
2794
  if not job_name.startswith("timer"):
2776
2795
  continue
2777
2796
  timer_name = job_name[5:]
@@ -2830,7 +2849,7 @@ class Kernel(Settings):
2830
2849
 
2831
2850
  @self.console_command("beep", _("Perform beep"))
2832
2851
  def beep(channel, _, **kwargs):
2833
- OS_NAME = platform.system()
2852
+ OS_NAME = self.os_information["OS_NAME"]
2834
2853
  system_sound = {
2835
2854
  "Windows": r"c:\Windows\Media\Sounds\Alarm01.wav",
2836
2855
  "Darwin": "/System/Library/Sounds/Ping.aiff",
meerk40t/main.py CHANGED
@@ -11,7 +11,7 @@ import os.path
11
11
  import sys
12
12
 
13
13
  APPLICATION_NAME = "MeerK40t"
14
- APPLICATION_VERSION = "0.9.7010"
14
+ APPLICATION_VERSION = "0.9.7020"
15
15
 
16
16
  if not getattr(sys, "frozen", False):
17
17
  # If .git directory does not exist we are running from a package like pypi
@@ -26,6 +26,8 @@ def plugin(kernel, lifecycle=None):
26
26
  command, channel, _, port=23, silent=False, quit=False, **kwargs
27
27
  ):
28
28
  root = kernel.root
29
+ # Variable to store input
30
+ root.__console_buffer = ""
29
31
  try:
30
32
  server = root.open_as("module/TCPServer", "console-server", port=port)
31
33
  if quit:
@@ -36,25 +38,61 @@ def plugin(kernel, lifecycle=None):
36
38
  "{kernel_name} {kernel_version} Telnet Console.\r\n"
37
39
  ).format(kernel_name=kernel.name, kernel_version=kernel.version)
38
40
  send.line_end = "\r\n"
39
-
40
41
  recv = root.channel("console-server/recv")
41
- recv.watch(root.console)
42
- channel(
43
- _(
44
- "{name} {version} console server on port: {port}".format(
45
- name=kernel.name, version=kernel.version, port=port
46
- )
42
+ except (OSError, ValueError):
43
+ channel(_("Server failed on port: {port}").format(port=port))
44
+ return
45
+
46
+ def exec_command(data: str) -> None:
47
+ # We are in a different thread, so let's hand over stuff to the gui
48
+ if isinstance(data, bytes):
49
+ try:
50
+ data = data.decode()
51
+ except UnicodeDecodeError as e:
52
+ return
53
+ start = 0
54
+ while True:
55
+ idx = data.find("|", start)
56
+ if idx < 0:
57
+ break
58
+ # Is the amount of quotation marks odd (ie non-even)?
59
+ # Yes: we are in the middle of a str
60
+ # No: we can split the command
61
+ quotations = data.count('"', 0, idx)
62
+ if quotations % 2 == 0:
63
+ data = data[:idx].rstrip() + "\n" + data[idx+1:].lstrip()
64
+ start = idx + 1
65
+ root.__console_buffer += data
66
+ while "\n" in root.__console_buffer:
67
+ pos = root.__console_buffer.find("\n")
68
+ command = root.__console_buffer[0:pos].strip("\r")
69
+ root.__console_buffer = root.__console_buffer[pos + 1 :]
70
+ if handover is None:
71
+ root.console(command + "\n")
72
+ else:
73
+ handover(command)
74
+
75
+ handover = None
76
+ for result in root.find("gui/handover"):
77
+ # Do we have a thread handover routine?
78
+ if result is not None:
79
+ handover, _path, suffix_path = result
80
+ break
81
+ recv.watch(exec_command)
82
+
83
+ channel(
84
+ _(
85
+ "{name} {version} console server on port: {port}".format(
86
+ name=kernel.name, version=kernel.version, port=port
47
87
  )
48
88
  )
89
+ )
49
90
 
50
- if not silent:
51
- console = root.channel("console")
52
- console.watch(send)
53
- server.events_channel.watch(console)
91
+ if not silent:
92
+ console = root.channel("console")
93
+ console.watch(send)
94
+ server.events_channel.watch(console)
54
95
 
55
- except (OSError, ValueError):
56
- channel(_("Server failed on port: {port}").format(port=port))
57
- return
58
96
 
59
97
  @kernel.console_option(
60
98
  "port", "p", type=int, default=2080, help=_("port to listen on.")
@@ -34,6 +34,13 @@ class WebServer(Module):
34
34
  )
35
35
  self.server_headers = dict()
36
36
  self.client_headers = dict()
37
+ self.handover = None
38
+ root = self.context.root
39
+ for result in root.find("gui/handover"):
40
+ # Do we have a thread handover routine?
41
+ if result is not None:
42
+ self.handover, _path, suffix_path = result
43
+ break
37
44
 
38
45
  def stop(self):
39
46
  self.state = "terminate"
@@ -45,6 +52,13 @@ class WebServer(Module):
45
52
  if self.socket is not None:
46
53
  self.socket.close()
47
54
  self.socket = None
55
+
56
+ def send(self, command):
57
+ if self.handover is None:
58
+ self.context(f"{command}\n")
59
+ else:
60
+ self.handover(command)
61
+
48
62
 
49
63
  def receive(self, data):
50
64
  def parse_received_data():
@@ -92,7 +106,7 @@ class WebServer(Module):
92
106
  content = f"Received command: '{command}'"
93
107
  elif self.client_headers["TYPE"] == "POST" and "post_cmd" in self.client_headers:
94
108
  command = self.client_headers["post_cmd"]
95
- self.context(command + "\n")
109
+ self.send(command)
96
110
  content = f"Received command: '{command}'"
97
111
  return content_type, content
98
112
 
meerk40t/ruida/device.py CHANGED
@@ -263,18 +263,22 @@ class RuidaDevice(Service):
263
263
  choice_dict["choices"] = ["UNCONFIGURED"]
264
264
  choice_dict["display"] = ["pyserial-not-installed"]
265
265
 
266
+ from platform import system
267
+ is_linux = system() == "Linux"
268
+
266
269
  choices = [
267
270
  {
268
271
  "attr": "serial_port",
269
272
  "object": self,
270
273
  "default": "UNCONFIGURED",
271
274
  "type": str,
272
- "style": "option",
275
+ "style": "combosmall" if is_linux else "option",
273
276
  "label": "",
274
277
  "tip": _("What serial interface does this device connect to?"),
275
278
  "section": "_10_Serial Interface",
276
279
  "subsection": "_00_",
277
280
  "dynamic": update,
281
+ "exclusive": not is_linux,
278
282
  },
279
283
  {
280
284
  "attr": "baud_rate",
@@ -275,7 +275,8 @@ class LinkedList:
275
275
  data.next = None
276
276
 
277
277
  def remove_func():
278
- data.previous.next = data.next
278
+ if data.previous is not None:
279
+ data.previous.next = data.next
279
280
  if data.next is not None:
280
281
  data.next.previous = data.previous
281
282
  data.previous = None