meerk40t 0.9.7051__py2.py3-none-any.whl → 0.9.7910__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 (69) hide show
  1. meerk40t/balormk/controller.py +3 -3
  2. meerk40t/balormk/device.py +7 -0
  3. meerk40t/balormk/driver.py +23 -14
  4. meerk40t/balormk/galvo_commands.py +18 -3
  5. meerk40t/balormk/gui/balorconfig.py +6 -0
  6. meerk40t/balormk/livelightjob.py +36 -14
  7. meerk40t/camera/camera.py +1 -0
  8. meerk40t/camera/gui/camerapanel.py +154 -58
  9. meerk40t/camera/plugin.py +46 -5
  10. meerk40t/core/elements/branches.py +90 -20
  11. meerk40t/core/elements/elements.py +59 -37
  12. meerk40t/core/elements/trace.py +10 -6
  13. meerk40t/core/node/node.py +2 -0
  14. meerk40t/core/plotplanner.py +7 -4
  15. meerk40t/device/gui/defaultactions.py +78 -14
  16. meerk40t/dxf/dxf_io.py +42 -0
  17. meerk40t/grbl/controller.py +245 -35
  18. meerk40t/grbl/device.py +102 -26
  19. meerk40t/grbl/driver.py +8 -2
  20. meerk40t/grbl/gui/grblconfiguration.py +6 -0
  21. meerk40t/grbl/gui/grblcontroller.py +1 -1
  22. meerk40t/gui/about.py +7 -0
  23. meerk40t/gui/choicepropertypanel.py +20 -30
  24. meerk40t/gui/devicepanel.py +27 -16
  25. meerk40t/gui/help_assets/help_assets.py +126 -2
  26. meerk40t/gui/icons.py +15 -0
  27. meerk40t/gui/laserpanel.py +102 -54
  28. meerk40t/gui/materialtest.py +10 -0
  29. meerk40t/gui/mkdebug.py +268 -9
  30. meerk40t/gui/navigationpanels.py +74 -8
  31. meerk40t/gui/propertypanels/operationpropertymain.py +185 -91
  32. meerk40t/gui/scenewidgets/elementswidget.py +7 -1
  33. meerk40t/gui/scenewidgets/selectionwidget.py +24 -9
  34. meerk40t/gui/simulation.py +1 -1
  35. meerk40t/gui/statusbarwidgets/shapepropwidget.py +50 -40
  36. meerk40t/gui/statusbarwidgets/statusbar.py +2 -2
  37. meerk40t/gui/toolwidgets/toolmeasure.py +1 -1
  38. meerk40t/gui/toolwidgets/toolnodeedit.py +4 -1
  39. meerk40t/gui/toolwidgets/tooltabedit.py +9 -7
  40. meerk40t/gui/wxmeerk40t.py +45 -15
  41. meerk40t/gui/wxmmain.py +23 -9
  42. meerk40t/gui/wxmribbon.py +36 -0
  43. meerk40t/gui/wxutils.py +66 -42
  44. meerk40t/kernel/inhibitor.py +120 -0
  45. meerk40t/kernel/kernel.py +38 -0
  46. meerk40t/lihuiyu/controller.py +33 -3
  47. meerk40t/lihuiyu/device.py +99 -4
  48. meerk40t/lihuiyu/driver.py +65 -5
  49. meerk40t/lihuiyu/gui/lhycontrollergui.py +69 -24
  50. meerk40t/lihuiyu/gui/lhydrivergui.py +6 -0
  51. meerk40t/lihuiyu/laserspeed.py +17 -10
  52. meerk40t/lihuiyu/parser.py +23 -0
  53. meerk40t/main.py +2 -2
  54. meerk40t/moshi/gui/moshidrivergui.py +7 -0
  55. meerk40t/newly/controller.py +3 -2
  56. meerk40t/newly/device.py +23 -2
  57. meerk40t/newly/driver.py +8 -3
  58. meerk40t/newly/gui/newlyconfig.py +7 -0
  59. meerk40t/ruida/gui/ruidaconfig.py +7 -0
  60. meerk40t/tools/geomstr.py +142 -49
  61. meerk40t/tools/rasterplotter.py +0 -5
  62. meerk40t/tools/ttfparser.py +921 -168
  63. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/METADATA +1 -1
  64. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/RECORD +69 -68
  65. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/LICENSE +0 -0
  66. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/WHEEL +0 -0
  67. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/entry_points.txt +0 -0
  68. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/top_level.txt +0 -0
  69. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7910.dist-info}/zip-safe +0 -0
meerk40t/gui/wxutils.py CHANGED
@@ -1650,58 +1650,31 @@ class wxCheckListBox(StaticBoxSizer):
1650
1650
  **kwargs,
1651
1651
  ):
1652
1652
  self.parent = parent
1653
- self.choices = choices
1653
+ self.choices = []
1654
1654
  self._children = []
1655
- self._tool_tip = None
1655
+ self._tool_tip = ""
1656
1656
  self._help = None
1657
1657
  super().__init__(
1658
1658
  parent=parent, id=wx.ID_ANY, label=label, orientation=wx.VERTICAL
1659
1659
  )
1660
1660
  self.majorDimension = majorDimension
1661
1661
  self.style = style
1662
- self._build_controls()
1663
-
1664
- def _build_controls(self):
1665
- """
1666
- Build the controls for the CheckListBox.
1667
- This method is called during initialization to create the checkboxes.
1668
- """
1669
- if self.choices is None:
1670
- self.choices = []
1671
- if self.majorDimension == 0 or self.style == wx.RA_SPECIFY_ROWS:
1672
- self.majorDimension = 1000
1673
- container = None
1674
- for idx, c in enumerate(self.choices):
1675
- if idx % self.majorDimension == 0:
1676
- container = wx.BoxSizer(wx.HORIZONTAL)
1677
- self.Add(container, 0, wx.EXPAND, 0)
1678
- check_option = wx.CheckBox(self.parent, wx.ID_ANY, label=c)
1679
- container.Add(check_option, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1680
- self._children.append(check_option)
1681
-
1682
- if platform.system() == "Linux":
1683
-
1684
- def on_mouse_over_check(ctrl):
1685
- def mouse(event=None):
1686
- ctrl.SetToolTip(self._tool_tip)
1687
- event.Skip()
1688
-
1689
- return mouse
1690
-
1691
- for ctrl in self._children:
1692
- ctrl.Bind(wx.EVT_MOTION, on_mouse_over_check(ctrl))
1693
-
1694
- for ctrl in self._children:
1695
- ctrl.Bind(wx.EVT_CHECKBOX, self.on_check)
1696
- ctrl.Bind(wx.EVT_RIGHT_DOWN, self.on_right_click)
1697
-
1698
- for ctrl in self._children:
1699
- set_color_according_to_theme(ctrl, "text_bg", "text_fg")
1662
+ if choices is None:
1663
+ choices = []
1664
+ self.Set(choices)
1700
1665
 
1701
1666
  @property
1702
1667
  def Children(self):
1703
1668
  return self._children
1704
1669
 
1670
+ def on_mouse_over_check(self, event):
1671
+ """
1672
+ Handle mouse over events to show tooltips on Linux.
1673
+ """
1674
+ if self._tool_tip:
1675
+ event.GetEventObject().SetToolTip(self._tool_tip)
1676
+ event.Skip()
1677
+
1705
1678
  def GetParent(self):
1706
1679
  return self.parent
1707
1680
 
@@ -1764,6 +1737,11 @@ class wxCheckListBox(StaticBoxSizer):
1764
1737
  ),
1765
1738
  id=item.GetId(),
1766
1739
  )
1740
+ # Test routines
1741
+ # item = menu.Append(wx.ID_ANY, "Test-Routine to set few items", "")
1742
+ # parent.Bind(wx.EVT_MENU, lambda e: self.Set(["A", "B", "C"]), id=item.GetId())
1743
+ # item = menu.Append(wx.ID_ANY, "Test-Routine to set many items", "")
1744
+ # parent.Bind(wx.EVT_MENU, lambda e: self.Set([f"Item {i}" for i in range(20)]), id=item.GetId())
1767
1745
  parent.PopupMenu(menu)
1768
1746
  menu.Destroy()
1769
1747
 
@@ -1843,9 +1821,55 @@ class wxCheckListBox(StaticBoxSizer):
1843
1821
  :param choices: A list of strings to set as choices.
1844
1822
  """
1845
1823
  # print (f"Setting choices for {self.GetLabel()}: {choices}")
1846
- self.Clear()
1824
+ """
1825
+ This is more efficient than clearing and rebuilding the controls.
1826
+ Update the labels of existing controls to match new_choices.
1827
+ If there are more controls than choices, extra controls are destroyed.
1828
+ If there are fewer controls than choices, new controls are created.
1829
+ """
1830
+ # Update existing controls
1831
+ if self.majorDimension == 0 or self.style == wx.RA_SPECIFY_ROWS:
1832
+ self.majorDimension = 1000
1833
+ last_container = None
1834
+ for idx, choice in enumerate(choices):
1835
+ if idx < len(self._children):
1836
+ self._children[idx].SetLabel(choice)
1837
+ # self._children[idx].Show(True)
1838
+ else:
1839
+ # Add new controls if needed
1840
+ check_option = wx.CheckBox(self.parent, wx.ID_ANY, label=choice)
1841
+ if self._tool_tip:
1842
+ check_option.SetToolTip(self._tool_tip)
1843
+ check_option.Bind(wx.EVT_CHECKBOX, self.on_check)
1844
+ check_option.Bind(wx.EVT_RIGHT_DOWN, self.on_right_click)
1845
+ if platform.system() == "Linux":
1846
+ check_option.Bind(wx.EVT_MOTION, self.on_mouse_over_check)
1847
+ set_color_according_to_theme(check_option, "text_bg", "text_fg")
1848
+ self._children.append(check_option)
1849
+ # Add to layout
1850
+ # Find or create the appropriate container
1851
+ if idx % self.majorDimension == 0:
1852
+ last_container = wx.BoxSizer(wx.HORIZONTAL)
1853
+ self.Add(last_container, 0, wx.EXPAND, 0)
1854
+ else:
1855
+ # Find the last container
1856
+ if last_container is None:
1857
+ for c in self.GetChildren():
1858
+ if c.IsSizer():
1859
+ last_container = c.GetSizer()
1860
+ if last_container is None:
1861
+ # fallback: create new container
1862
+ last_container = wx.BoxSizer(wx.HORIZONTAL)
1863
+ self.Add(last_container, 0, wx.EXPAND, 0)
1864
+ last_container.Add(check_option, 1, wx.ALIGN_CENTER_VERTICAL, 0)
1865
+ # Remove extra controls
1866
+ if len(self._children) > len(choices):
1867
+ for idx in range(len(choices), len(self._children)):
1868
+ self._children[idx].Destroy()
1869
+ self._children = self._children[: len(choices)]
1870
+ self.Layout()
1871
+ self.parent.Layout()
1847
1872
  self.choices = list(choices)
1848
- self._build_controls()
1849
1873
 
1850
1874
 
1851
1875
  ##############
@@ -0,0 +1,120 @@
1
+ """
2
+ This modules prevents the OS from sleeping / hibernating.
3
+ It is used to ensure that the system remains active during long-running operations.
4
+ It is not intended to be used for general-purpose tasks.
5
+ """
6
+
7
+ import ctypes
8
+ import platform
9
+ import subprocess
10
+
11
+ _ES_CONTINUOUS = 0x80000000
12
+ _ES_SYSTEM_REQUIRED = 0x00000001
13
+
14
+ # Extract common target list up top
15
+ _SYSTEMCTL_TARGETS = [
16
+ "sleep.target",
17
+ "suspend.target",
18
+ "hibernate.target",
19
+ "hybrid-sleep.target",
20
+ ]
21
+
22
+
23
+ def _run_systemctl(action: str):
24
+ try:
25
+ # Check if systemctl is available
26
+ proc = subprocess.run(["systemctl", action] + _SYSTEMCTL_TARGETS)
27
+ except (subprocess.CalledProcessError, FileNotFoundError):
28
+ print("systemctl is not available on this system.")
29
+ return False
30
+ except Exception as e:
31
+ print(f"An unexpected error occurred: {e}")
32
+ return False
33
+ # print(f"systemctl {action} returned {proc.returncode} [{proc}]")
34
+ return proc.returncode == 0
35
+
36
+
37
+ def _darwin_inhibit():
38
+ try:
39
+ proc = subprocess.run(["caffeinate", "-i"])
40
+ except (subprocess.CalledProcessError, FileNotFoundError):
41
+ print("caffeinate is not available on this system.")
42
+ return False
43
+ except Exception as e:
44
+ print(f"An unexpected error occurred: {e}")
45
+ return False
46
+ return proc.returncode == 0
47
+
48
+
49
+ def _darwin_release():
50
+ try:
51
+ # Use killall to stop caffeinate
52
+ proc = subprocess.run(["killall", "caffeinate"])
53
+ except Exception as e:
54
+ print(f"An unexpected error occurred: {e}")
55
+ return False
56
+ return proc.returncode == 0
57
+
58
+
59
+ def _linux_inhibit():
60
+ return _run_systemctl("mask")
61
+
62
+
63
+ def _linux_release():
64
+ return _run_systemctl("unmask")
65
+
66
+
67
+ def _windows_inhibit():
68
+ try:
69
+ # Set the thread execution state to prevent sleep
70
+ ctypes.windll.kernel32.SetThreadExecutionState(
71
+ _ES_CONTINUOUS | _ES_SYSTEM_REQUIRED
72
+ )
73
+ except Exception as e:
74
+ print(f"An error occurred while setting thread execution state: {e}")
75
+ return False
76
+ return True
77
+
78
+
79
+ def _windows_release():
80
+ try:
81
+ # Reset the thread execution state to allow sleep
82
+ ctypes.windll.kernel32.SetThreadExecutionState(_ES_CONTINUOUS)
83
+ except Exception as e:
84
+ print(f"An error occurred while resetting thread execution state: {e}")
85
+ return False
86
+ return True
87
+
88
+
89
+ # Darwin does not have a systemctl, but uses caffeinate
90
+ # Linux uses systemctl to mask/unmask sleep targets
91
+ # Windows uses SetThreadExecutionState to prevent sleep
92
+ # NB: Darwin does not to work relaibly on my testsystem, so has been disabled
93
+ _ACTIONS = {
94
+ # "Darwin": {"inhibit": _darwin_inhibit, "release": _darwin_release},
95
+ "Linux": {"inhibit": _linux_inhibit, "release": _linux_release},
96
+ "Windows": {"inhibit": _windows_inhibit, "release": _windows_release},
97
+ }
98
+
99
+
100
+ class Inhibitor:
101
+ def __init__(self):
102
+ self._os = platform.system()
103
+ self.active = False
104
+ self._actions = _ACTIONS.get(self._os)
105
+
106
+ @property
107
+ def available(self) -> bool:
108
+ return self._actions is not None
109
+
110
+ def inhibit(self):
111
+ if not self.available or self.active:
112
+ return
113
+ if self._actions["inhibit"]():
114
+ self.active = True
115
+
116
+ def release(self):
117
+ if not self.available or not self.active:
118
+ return
119
+ if self._actions["release"]():
120
+ self.active = False
meerk40t/kernel/kernel.py CHANGED
@@ -19,6 +19,7 @@ from .functions import (
19
19
  console_option,
20
20
  get_safe_path,
21
21
  )
22
+ from .inhibitor import Inhibitor
22
23
  from .jobs import ConsoleFunction, Job
23
24
  from .lifecycles import *
24
25
  from .module import Module
@@ -183,6 +184,7 @@ class Kernel(Settings):
183
184
  self.os_information = self._get_environment()
184
185
  self.show_aio_prompt = True
185
186
  self.silent_mode = False
187
+ self.inhibitor = Inhibitor()
186
188
 
187
189
  def __str__(self):
188
190
  return f"Kernel({self.name}, {self.profile}, {self.version})"
@@ -2855,6 +2857,42 @@ class Kernel(Settings):
2855
2857
  channel(_("Syntax Error: timer<name> <times> <interval> <command>"))
2856
2858
  return
2857
2859
 
2860
+ # ==========
2861
+ # Sleep Commands
2862
+ # ==========
2863
+ @self.console_argument(
2864
+ "mode", type=str, help=_("Mode to set: prevent/allow"), default=None
2865
+ )
2866
+ @self.console_command(
2867
+ "system_hibernate", _("Prevent/allow system hibernation.")
2868
+ )
2869
+ def system_hibernate(channel, _, mode=None, **kwargs):
2870
+ """
2871
+ Prevent or allow system hibernation. This is a toggle command.
2872
+ If no mode is specified, it will print the current state.
2873
+ """
2874
+ if not self.inhibitor.available:
2875
+ channel(_("Inhibitor is not available on this system."))
2876
+ return
2877
+ if mode is not None:
2878
+ if mode.lower() not in ("prevent", "allow"):
2879
+ channel(_("Please specify 'prevent' or 'allow'."))
2880
+ return
2881
+ if mode.lower() == "prevent":
2882
+ self.inhibitor.inhibit()
2883
+ else:
2884
+ self.inhibitor.release()
2885
+ sudo_msg = _("You might need system administrator priviliges.")
2886
+ if self.inhibitor.active:
2887
+ channel(_("System hibernation is prevented."))
2888
+ else:
2889
+ channel(_("System hibernation is allowed."))
2890
+ if self.os_information["OS_NAME"] == "Linux" and (
2891
+ (mode == "prevent" and not self.inhibitor.active)
2892
+ or (mode == "allow" and self.inhibitor.active)
2893
+ ):
2894
+ channel(sudo_msg)
2895
+
2858
2896
  # ==========
2859
2897
  # CORE OBJECTS COMMANDS
2860
2898
  # ==========
@@ -124,10 +124,21 @@ def onewire_crc_lookup(line):
124
124
  @param line: line to be CRC'd
125
125
  @return: 8 bit crc of line.
126
126
  """
127
+
127
128
  crc = 0
128
129
  for i in range(0, 30):
129
130
  crc = line[i] ^ crc
130
131
  crc = crc_table[crc & 0x0F] ^ crc_table[16 + ((crc >> 4) & 0x0F)]
132
+
133
+ """ Print the line in hex and ascii format for debugging purposes.
134
+ def hex_repr(data):
135
+ return " ".join(f"{x:02x}" for x in data)
136
+
137
+ def ascii_repr(data):
138
+ return "".join(chr(x) if 32 <= x < 127 else "." for x in data)
139
+
140
+ print (f"Line ({len(line)} bytes): {hex_repr(line)} {ascii_repr(line)} CRC: {hex(crc)}")
141
+ """
131
142
  return crc
132
143
 
133
144
 
@@ -618,6 +629,16 @@ class LihuiyuController:
618
629
  self._preempt.clear()
619
630
  self.update_buffer()
620
631
 
632
+ def debug_packet(self, packet):
633
+ """
634
+ Debugging function to print the packet in a readable format.
635
+ We will output both hex and ascii representation of the packet.
636
+ @param packet: Packet to debug.
637
+ """
638
+ hex_packet = " ".join(f"{b:02x}" for b in packet)
639
+ ascii_packet = "".join(chr(b) if 32 <= b < 127 else "." for b in packet)
640
+ print(f"Packet: {hex_packet} | ASCII: {ascii_packet} (len={len(packet)})")
641
+
621
642
  def process_queue(self):
622
643
  """
623
644
  Attempts to process the buffer/queue
@@ -662,6 +683,13 @@ class LihuiyuController:
662
683
  post_send_command = None
663
684
  default_checksum = True
664
685
 
686
+ if packet.startswith(b"AT"):
687
+ # This is as special case for the M3 only:
688
+ # AT command packages are padded with 0x00 and not 'F' as usal
689
+ if packet.endswith(b"\n"):
690
+ packet = packet[:-1]
691
+ c = b"\x00"
692
+ packet += c * (30 - len(packet)) # Padding with 0 character
665
693
  # find pipe commands.
666
694
  if packet.endswith(b"\n"):
667
695
  packet = packet[:-1]
@@ -691,7 +719,7 @@ class LihuiyuController:
691
719
  self.update_state("terminate")
692
720
  self.is_shutdown = True
693
721
  packet = packet[:-1]
694
- if packet.startswith(b"A"):
722
+ if packet.startswith(b"A") and not packet.startswith(b"AT"):
695
723
  # This is a challenge code. A is only used for serial challenges.
696
724
  post_send_command = self._confirm_serial
697
725
  if len(packet) != 0:
@@ -703,13 +731,14 @@ class LihuiyuController:
703
731
  c = b"F" # Packet was simply #. We can do nothing.
704
732
  packet += bytes([c]) * (30 - len(packet)) # Padding. '\n'
705
733
  else:
706
- packet += b"F" * (30 - len(packet)) # Padding. '\n'
734
+ padder = b"\x00" if packet.startswith(b"AT") else b"F"
735
+ packet += padder * (30 - len(packet)) # Padding. '\n'
707
736
  if not realtime and self.state in ("pause", "busy"):
708
737
  return False # Processing normal queue, PAUSE and BUSY apply.
709
738
 
710
739
  # Packet is prepared and ready to send. Open Channel.
711
740
  self.open()
712
-
741
+ # print (f"Packet: {packet!r} (len={len(packet)})" )
713
742
  if len(packet) == 30:
714
743
  # We have a sendable packet.
715
744
  if not self.pre_ok:
@@ -718,6 +747,7 @@ class LihuiyuController:
718
747
  packet = b"\x00" + packet + bytes([onewire_crc_lookup(packet)])
719
748
  else:
720
749
  packet = b"\x00" + packet + bytes([onewire_crc_lookup(packet) ^ 0xFF])
750
+ # self.debug_packet(packet)
721
751
  self.connection.write(packet)
722
752
  self.pre_ok = False
723
753
 
@@ -129,6 +129,13 @@ class LihuiyuDevice(Service, Status):
129
129
  ]
130
130
  self.register_choices("bed_dim", choices)
131
131
 
132
+ def get_max_range():
133
+ """
134
+ Returns the maximum range of the device.
135
+ """
136
+ dev_mode = getattr(self.kernel.root, "developer_mode", False)
137
+ return 100 if dev_mode else 50
138
+
132
139
  choices = [
133
140
  {
134
141
  "attr": "label",
@@ -153,6 +160,52 @@ class LihuiyuDevice(Service, Status):
153
160
  "section": "_10_" + _("Configuration"),
154
161
  "subsection": _("Board Setup"),
155
162
  },
163
+ {
164
+ "attr": "supports_pwm",
165
+ "object": self,
166
+ "default": False,
167
+ "type": bool,
168
+ "label": _("Hardware-PWM"),
169
+ "tip": _(
170
+ "Does the board support Hardware-PWM. Only M3 and fireware >= 2024.01.18g support PWM. Earlier M3 revisions are just M2+."
171
+ ),
172
+ "section": "_10_" + _("Configuration"),
173
+ "subsection": _("Hardware-Laser-Power"),
174
+ "conditional": (self, "board", "M3"),
175
+ "signals": "pwm_mode_changed",
176
+ },
177
+ {
178
+ "attr": "pwm_speedcode",
179
+ "object": self,
180
+ "default": False,
181
+ "type": bool,
182
+ "label": _("Use PWM-Speedcode"),
183
+ "tip": _("PWM Method: set power as well in LHY-speedcodes."),
184
+ "section": "_10_" + _("Configuration"),
185
+ "subsection": _("Hardware-Laser-Power"),
186
+ "conditional": (self, "supports_pwm"),
187
+ "hidden": True,
188
+ },
189
+ {
190
+ "attr": "power_scale",
191
+ "object": self,
192
+ "default": 30,
193
+ "type": int,
194
+ "style": "slider",
195
+ "min": 1,
196
+ "max": get_max_range,
197
+ "label": _("Maximum Laser strength"),
198
+ "trailer": "%",
199
+ "tip": _(
200
+ "Set the maximum laser power level, any operation power will be a fraction of this"
201
+ )
202
+ + "\n"
203
+ + _("Setting this too high may cause damage to your laser tube!"),
204
+ "section": "_10_" + _("Configuration"),
205
+ "subsection": _("Hardware-Laser-Power"),
206
+ "conditional": (self, "supports_pwm"),
207
+ "signals": "pwm_mode_changed",
208
+ },
156
209
  {
157
210
  "attr": "flip_x",
158
211
  "object": self,
@@ -547,12 +600,15 @@ class LihuiyuDevice(Service, Status):
547
600
  action="store_true",
548
601
  help=_("override one second laser fire pulse duration"),
549
602
  )
603
+ @self.console_option("power", "p", type=str, help=_("Power level"))
550
604
  @self.console_argument("time", type=float, help=_("laser fire pulse duration"))
551
605
  @self.console_command(
552
606
  "pulse",
553
607
  help=_("pulse <time>: Pulse the laser in place."),
554
608
  )
555
- def pulse(command, channel, _, time=None, idonotlovemyhouse=False, **kwgs):
609
+ def pulse(
610
+ command, channel, _, time=None, power=None, idonotlovemyhouse=False, **kwgs
611
+ ):
556
612
  if time is None:
557
613
  channel(_("Must specify a pulse time in milliseconds."))
558
614
  return
@@ -568,16 +624,33 @@ class LihuiyuDevice(Service, Status):
568
624
  except IndexError:
569
625
  return
570
626
 
571
- def timed_fire():
627
+ def timed_fire(withpower=None):
572
628
  yield "wait_finish"
573
- yield "laser_on"
629
+ if withpower is not None:
630
+ yield "laser_on", withpower
631
+ else:
632
+ yield "laser_on"
574
633
  yield "wait", time
575
634
  yield "laser_off"
576
635
 
636
+ if power is not None:
637
+ try:
638
+ if power.endswith("%"):
639
+ power = power[:-1]
640
+ power = float(power) * 10
641
+ else:
642
+ power = float(power)
643
+ except ValueError:
644
+ channel(_("Power must be valid value."))
645
+ return
646
+ if not (0 <= power <= 1000):
647
+ channel(_("Power must be between 0 and 1000."))
648
+ return
649
+
577
650
  if self.spooler.is_idle:
578
651
  label = _("Pulse laser for {time}ms").format(time=time)
579
652
  self.spooler.laserjob(
580
- list(timed_fire()),
653
+ list(timed_fire(withpower=power)),
581
654
  label=label,
582
655
  helper=True,
583
656
  outline=[self.native] * 4,
@@ -892,6 +965,27 @@ class LihuiyuDevice(Service, Status):
892
965
  code = b"A%s\n" % challenge
893
966
  self.output.write(code)
894
967
 
968
+ def _validate_board(channel, board):
969
+ """
970
+ Validates the board type
971
+ """
972
+ if self.board != board:
973
+ channel(
974
+ _(
975
+ "This command is only available for {target} boards. This is a {board}."
976
+ ).format(target=board, board=self.board)
977
+ )
978
+ return False
979
+ return True
980
+
981
+ @self.console_command(
982
+ "get_m3nano_info",
983
+ help=_("Request M3Nano+ board info"),
984
+ )
985
+ def get_m3nano_info(command, channel, _, remainder=None, **kwgs):
986
+ if _validate_board(channel, "M3"):
987
+ self.driver.get_m3_hardware_info()
988
+
895
989
  @self.console_command("start", help=_("Start Pipe to Controller"))
896
990
  def pipe_start(command, channel, _, **kwgs):
897
991
  self.controller.update_state("active")
@@ -1050,6 +1144,7 @@ class LihuiyuDevice(Service, Status):
1050
1144
  @signal_listener("plot_shift")
1051
1145
  @signal_listener("plot_phase_type")
1052
1146
  @signal_listener("plot_phase_value")
1147
+ @signal_listener("supports_pwm")
1053
1148
  def plot_attributes_update(self, origin=None, *args):
1054
1149
  self.driver.plot_attribute_update()
1055
1150