meerk40t 0.9.7051__py2.py3-none-any.whl → 0.9.7900__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 (68) 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/icons.py +15 -0
  26. meerk40t/gui/laserpanel.py +102 -54
  27. meerk40t/gui/materialtest.py +10 -0
  28. meerk40t/gui/mkdebug.py +268 -9
  29. meerk40t/gui/navigationpanels.py +65 -7
  30. meerk40t/gui/propertypanels/operationpropertymain.py +185 -91
  31. meerk40t/gui/scenewidgets/elementswidget.py +7 -1
  32. meerk40t/gui/scenewidgets/selectionwidget.py +24 -9
  33. meerk40t/gui/simulation.py +1 -1
  34. meerk40t/gui/statusbarwidgets/shapepropwidget.py +50 -40
  35. meerk40t/gui/statusbarwidgets/statusbar.py +2 -2
  36. meerk40t/gui/toolwidgets/toolmeasure.py +1 -1
  37. meerk40t/gui/toolwidgets/toolnodeedit.py +4 -1
  38. meerk40t/gui/toolwidgets/tooltabedit.py +9 -7
  39. meerk40t/gui/wxmeerk40t.py +2 -0
  40. meerk40t/gui/wxmmain.py +23 -9
  41. meerk40t/gui/wxmribbon.py +36 -0
  42. meerk40t/gui/wxutils.py +66 -42
  43. meerk40t/kernel/inhibitor.py +120 -0
  44. meerk40t/kernel/kernel.py +38 -0
  45. meerk40t/lihuiyu/controller.py +33 -3
  46. meerk40t/lihuiyu/device.py +99 -4
  47. meerk40t/lihuiyu/driver.py +62 -5
  48. meerk40t/lihuiyu/gui/lhycontrollergui.py +69 -24
  49. meerk40t/lihuiyu/gui/lhydrivergui.py +6 -0
  50. meerk40t/lihuiyu/laserspeed.py +17 -10
  51. meerk40t/lihuiyu/parser.py +23 -0
  52. meerk40t/main.py +1 -1
  53. meerk40t/moshi/gui/moshidrivergui.py +7 -0
  54. meerk40t/newly/controller.py +3 -2
  55. meerk40t/newly/device.py +23 -2
  56. meerk40t/newly/driver.py +8 -3
  57. meerk40t/newly/gui/newlyconfig.py +7 -0
  58. meerk40t/ruida/gui/ruidaconfig.py +7 -0
  59. meerk40t/tools/geomstr.py +68 -48
  60. meerk40t/tools/rasterplotter.py +0 -5
  61. meerk40t/tools/ttfparser.py +155 -82
  62. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/METADATA +1 -1
  63. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/RECORD +68 -67
  64. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/LICENSE +0 -0
  65. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/WHEEL +0 -0
  66. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/entry_points.txt +0 -0
  67. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/top_level.txt +0 -0
  68. {meerk40t-0.9.7051.dist-info → meerk40t-0.9.7900.dist-info}/zip-safe +0 -0
@@ -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
 
@@ -194,6 +194,18 @@ class LihuiyuDriver(Parameters):
194
194
  def __call__(self, e):
195
195
  self.out_pipe.write(e)
196
196
 
197
+ @property
198
+ def has_adjustable_maximum_power(self):
199
+ return self.service.supports_pwm
200
+
201
+ @property
202
+ def max_power_scale(self):
203
+ return self.service.power_scale
204
+
205
+ @max_power_scale.setter
206
+ def max_power_scale(self, value):
207
+ self.service.power_scale = value
208
+
197
209
  def get_internal_queue_status(self):
198
210
  return self._queue_current, self._queue_total
199
211
 
@@ -205,6 +217,7 @@ class LihuiyuDriver(Parameters):
205
217
  self.plot_planner.force_shift = self.service.plot_shift
206
218
  self.plot_planner.phase_type = self.service.plot_phase_type
207
219
  self.plot_planner.phase_value = self.service.plot_phase_value
220
+ self.plot_planner.set_ppi(not self.service.supports_pwm)
208
221
 
209
222
  def hold_work(self, priority):
210
223
  """
@@ -384,7 +397,7 @@ class LihuiyuDriver(Parameters):
384
397
  self.rapid_mode()
385
398
  self._move_relative(unit_dx, unit_dy)
386
399
 
387
- def dwell(self, time_in_ms):
400
+ def dwell(self, time_in_ms, settings=None):
388
401
  """
389
402
  Requests that the laser fire in place for the given time period. This could be done in a series of commands,
390
403
  move to a location, turn laser on, wait, turn laser off. However, some drivers have specific laser-in-place
@@ -393,9 +406,12 @@ class LihuiyuDriver(Parameters):
393
406
  @param time_in_ms:
394
407
  @return:
395
408
  """
409
+ power = settings.get("power", None) if settings else None
396
410
  self.rapid_mode()
397
411
  self.wait_finish()
398
- self.laser_on() # This can't be sent early since these are timed operations.
412
+ self.laser_on(
413
+ power=power
414
+ ) # This can't be sent early since these are timed operations.
399
415
  self.wait(time_in_ms)
400
416
  self.laser_off()
401
417
 
@@ -421,7 +437,7 @@ class LihuiyuDriver(Parameters):
421
437
  self.laser = False
422
438
  return True
423
439
 
424
- def laser_on(self):
440
+ def laser_on(self, power=None):
425
441
  """
426
442
  Turn laser on in place.
427
443
 
@@ -429,6 +445,9 @@ class LihuiyuDriver(Parameters):
429
445
  """
430
446
  if self.laser:
431
447
  return False
448
+ if power is not None and self.service.supports_pwm:
449
+ self.send_at_pwm_code(power, "laser_on")
450
+
432
451
  if self.state == DRIVER_STATE_RAPID:
433
452
  self(b"I")
434
453
  self(self.CODE_LASER_ON)
@@ -443,6 +462,18 @@ class LihuiyuDriver(Parameters):
443
462
  self.laser = True
444
463
  return True
445
464
 
465
+ def send_at_pwm_code(self, power: float = 1000.0):
466
+ if len(self.out_pipe) > 0:
467
+ self(b"\n")
468
+ self.wait_finish()
469
+ self.rapid_mode()
470
+ power = max(0.0, min(power, 1000.0)) * self.service.power_scale / 100.0
471
+ m = int(power / 254)
472
+ n = int(power % 254)
473
+ # AT commands will be flushed out immediately
474
+ packet = bytes((ord("A"), ord("T"), ord("1"), m, n, ord("\n")))
475
+ self(packet)
476
+
446
477
  def rapid_mode(self, *values):
447
478
  """
448
479
  Rapid mode sets the laser to rapid state. This is usually moving the laser around without it executing a large
@@ -537,6 +568,8 @@ class LihuiyuDriver(Parameters):
537
568
  else:
538
569
  # Unidirectional (step on forward swing - rasters only going forward)
539
570
  raster_step_value = self._raster_step_g_value, 0
571
+ # We don't allow in situ power changes in raster mode.
572
+ power_val = None
540
573
  speed_code = LaserSpeed(
541
574
  self.service.board,
542
575
  self.speed,
@@ -548,6 +581,7 @@ class LihuiyuDriver(Parameters):
548
581
  suffix_c=False,
549
582
  fix_speeds=self.service.fix_speeds,
550
583
  raster_horizontal=horizontal,
584
+ power_value=power_val,
551
585
  ).speedcode
552
586
  speed_code = bytes(speed_code, "utf8")
553
587
  self(speed_code)
@@ -593,7 +627,12 @@ class LihuiyuDriver(Parameters):
593
627
  self._leftward = False
594
628
  self._topward = False
595
629
  self._horizontal_major = False
596
-
630
+ if self.service.supports_pwm and self.service.pwm_speedcode:
631
+ # If we are using PWM speedcode, we need to set the power value.
632
+ power_val = self.power * self.service.power_scale / 100.0
633
+ else:
634
+ # If we are not using PWM speedcode, we do not set the power value.
635
+ power_val = None
597
636
  speed_code = LaserSpeed(
598
637
  self.service.board,
599
638
  self.speed,
@@ -605,6 +644,7 @@ class LihuiyuDriver(Parameters):
605
644
  suffix_c=suffix_c,
606
645
  fix_speeds=self.service.fix_speeds,
607
646
  raster_horizontal=self._horizontal_major,
647
+ power_value=power_val,
608
648
  ).speedcode
609
649
  speed_code = bytes(speed_code, "utf8")
610
650
  self(speed_code)
@@ -702,6 +742,7 @@ class LihuiyuDriver(Parameters):
702
742
  self.rapid_mode()
703
743
  self._move_absolute(start.real, start.imag)
704
744
  self.wait_finish()
745
+ self._set_power(sets.get("power", 1000.0))
705
746
  self.dwell(sets.get("dwell_time"))
706
747
  elif function == "wait":
707
748
  self.plot_start()
@@ -1029,6 +1070,8 @@ class LihuiyuDriver(Parameters):
1029
1070
  self.power = 1000.0
1030
1071
  if self.power <= 0:
1031
1072
  self.power = 0.0
1073
+ if self.service.supports_pwm:
1074
+ self.send_at_pwm_code(self.power)
1032
1075
 
1033
1076
  def _set_ppi(self, power=1000.0):
1034
1077
  self.power = power
@@ -1463,4 +1506,18 @@ class LihuiyuDriver(Parameters):
1463
1506
  self._x_engaged = False
1464
1507
  self._y_engaged = True
1465
1508
  return x_dir + y_dir
1466
-
1509
+
1510
+ # def get_board_info(self):
1511
+ # try:
1512
+ # self.out_pipe.write_raw(b"\xac\x2e", 73 - 27)
1513
+ # except AttributeError:
1514
+ # pass
1515
+
1516
+ # def get_param_info(self):
1517
+ # try:
1518
+ # self.out_pipe.write_raw(b"\xac\xe0", 251 - 27)
1519
+ # except AttributeError:
1520
+ # pass
1521
+
1522
+ def get_m3_hardware_info(self):
1523
+ self(b"AT01")