def test_plotplanner_walk_raster(self): """ Test plotplanner operation of walking to a raster. PLOT_FINISH = 256 PLOT_RAPID = 4 PLOT_JOG = 2 PLOT_SETTING = 128 PLOT_AXIS = 64 PLOT_DIRECTION = 32 PLOT_LEFT_UPPER = 512 PLOT_RIGHT_LOWER = 1024 1 means cut. 0 means move. :return: """ rasterop = RasterOpNode() image = Image.new("RGBA", (256, 256)) draw = ImageDraw.Draw(image) draw.ellipse((0, 0, 255, 255), "black") image = image.convert("L") inode = ImageNode(image=image, dpi=1000.0, matrix=Matrix()) inode.step_x = 1 inode.step_y = 1 inode.process_image() rasterop.add_node(inode) rasterop.raster_step_x = 1 rasterop.raster_step_y = 1 vectorop = EngraveOpNode() vectorop.add_node( PathNode(path=Path(Circle(cx=127, cy=127, r=128)), fill="black")) cutcode = CutCode() cutcode.extend(vectorop.as_cutobjects()) cutcode.extend(rasterop.as_cutobjects()) settings = {"power": 500} plan = PlotPlanner(settings) for c in cutcode.flat(): plan.push(c) setting_changed = False for x, y, on in plan.gen(): if on > 2: if setting_changed: # Settings change happens at vector to raster switch and must repost the axis. self.assertEqual(on, PLOT_AXIS) if on == PLOT_SETTING: setting_changed = True else: setting_changed = False
def test_plotplanner_constant_xy_end(self): """ With raster_smooth set to 1 we should smooth the x axis so that no y=0 occurs. @return: """ for q in range(100): settings = { "power": 1000, "constant_move_x": bool(random.randint(0, 1)), "constant_move_y": bool(random.randint(0, 1)), } plan = PlotPlanner(settings) goal_x = None goal_y = None for i in range(10): goal_x = random.randint(0, 100) goal_y = random.randint(0, 100) plan.push( LineCut( Point(random.randint(0, 100), random.randint(0, 100)), Point(goal_x, goal_y), settings=settings, )) break_list = list(plan.queue) last_x = None last_y = None for x, y, on in plan.gen(): if on == 4: last_x = x last_y = y if on > 1: continue last_x = x last_y = y if last_x != goal_x: print(settings.get("constant_move_x")) print(settings.get("constant_move_y")) for seg in break_list: print(repr(seg)) self.assertEqual(last_x, goal_x) if last_y != goal_y: print(settings.get("constant_move_x")) print(settings.get("constant_move_y")) for seg in break_list: print(repr(seg)) self.assertEqual(last_y, goal_y)
def test_plotplanner_constant_move_xy(self): """ With raster_smooth set to 1 we should smooth the x axis so that no y=0 occurs. @return: """ settings = { "power": 1000, "constant_move_x": True, "constant_move_y": True } plan = PlotPlanner(settings) self.constant_move_x = True self.constant_move_y = True for i in range(100): plan.push( LineCut( Point(random.randint(0, 1000), random.randint(0, 1000)), Point(random.randint(0, 1000), random.randint(0, 1000)), settings=settings, )) last_x = None last_y = None for x, y, on in plan.gen(): if on == 4: last_x = x last_y = y if on > 1: continue cx = x cy = y if cx is None: continue if last_x is not None: total_dx = cx - last_x total_dy = cy - last_y dx = 1 if total_dx > 0 else 0 if total_dx == 0 else -1 dy = 1 if total_dy > 0 else 0 if total_dy == 0 else -1 for i in range(1, max(abs(total_dx), abs(total_dy)) + 1): nx = last_x + (i * dx) ny = last_y + (i * dy) # print(nx, ny, on) # print(x, y, on) last_x = cx last_y = cy print(f"Moving to {x} {y}")
def __init__(self, service): self.service = service self.native_x = 0x8000 self.native_y = 0x8000 self.name = str(self.service) self.connection = GalvoController(service) self.service.add_service_delegate(self.connection) self.paused = False self.is_relative = False self.laser = False self._shutdown = False self.queue = list() self.plot_planner = PlotPlanner( dict(), single=True, smooth=False, ppi=False, shift=False, group=True ) self.value_penbox = None self.plot_planner.settings_then_jog = True self._aborting = False self._list_bits = None
def test_plotplanner_constant_move_x_ppi(self): """ With raster_smooth set to 1 we should smooth the x axis so that no y=0 occurs. @return: """ settings = {"power": 500, "constant_move_x": True} plan = PlotPlanner(settings) plan.push(LineCut(Point(0, 0), Point(20, 2), settings=settings)) plan.push(LineCut(Point(20, 2), Point(20, 5), settings=settings)) plan.push(LineCut(Point(20, 5), Point(100, 10), settings=settings)) last_x = None last_y = None last_on = None for x, y, on in plan.gen(): if on == 4: last_x = x last_y = y if on > 1: continue if last_on is not None: self.assertNotEqual(last_on, on) last_on = on cx = x cy = y if cx is None: continue if last_x is not None: total_dx = cx - last_x total_dy = cy - last_y dx = 1 if total_dx > 0 else 0 if total_dx == 0 else -1 dy = 1 if total_dy > 0 else 0 if total_dy == 0 else -1 self.assertFalse(dx == 0) for i in range(1, max(abs(total_dx), abs(total_dy)) + 1): nx = last_x + (i * dx) ny = last_y + (i * dy) # print(nx, ny, on) # print(x, y, on) last_x = cx last_y = cy
def test_plotplanner_constant_move_y(self): """ With smooth_raster set to 2 we should never have x = 0. The x should *always* be in motion. @return: """ settings = {"power": 1000, "constant_move_y": True} plan = PlotPlanner(settings) plan.push(LineCut(Point(0, 0), Point(2, 20), settings=settings)) plan.push(LineCut(Point(2, 20), Point(5, 20), settings=settings)) plan.push(LineCut(Point(5, 20), Point(10, 100), settings=settings)) last_x = None last_y = None for x, y, on in plan.gen(): if on == 4: last_x = x last_y = y if on > 1: continue cx = x cy = y if cx is None: continue if last_x is not None: total_dx = cx - last_x total_dy = cy - last_y dx = 1 if total_dx > 0 else 0 if total_dx == 0 else -1 dy = 1 if total_dy > 0 else 0 if total_dy == 0 else -1 self.assertFalse(dy == 0) for i in range(0, max(abs(total_dx), abs(total_dy))): nx = last_x + (i * dx) ny = last_y + (i * dy) # print(nx, ny, on) last_x = cx last_y = cy print(f"Moving to {x} {y}")
def test_plotplanner_static_issue(self): settings = { "power": 1000, "constant_move_x": True, "constant_move_y": False, } plan = PlotPlanner(settings) plan.debug = True lines = ( ((41, 45), (14, 43)), ((32, 67), (32, 61)), ) for line in lines: plan.push( LineCut( Point(line[0][0], line[0][1]), Point(line[1][0], line[1][1]), settings=settings, )) goal_x = lines[-1][-1][0] goal_y = lines[-1][-1][1] break_list = list(plan.queue) last_x = None last_y = None for x, y, on in plan.gen(): if on == 4: last_x = x last_y = y if on > 1: continue last_x = x last_y = y if last_x != goal_x: for seg in break_list: print(repr(seg)) self.assertEqual(last_x, goal_x) if last_y != goal_y: for seg in break_list: print(repr(seg)) self.assertEqual(last_y, goal_y)
def test_plotplanner_flush(self): """ Intro test for plotplanner. This is needlessly complex. final value is "on", and provides commands. 128 means settings were changed. 64 indicates x_axis major 32 indicates x_dir, y_dir 256 indicates ended. 1 means cut. 0 means move. :return: """ settings = {"power": 1000} plan = PlotPlanner(settings) for i in range(211): plan.push(LineCut(Point(0, 0), Point(5, 100), settings=settings)) plan.push(LineCut(Point(100, 50), Point(0, 0), settings=settings)) plan.push( LineCut(Point(50, -50), Point(100, -100), settings={"power": 0})) q = 0 for x, y, on in plan.gen(): # print(x, y, on) if q == i: # for x, y, on in plan.process_plots(None): # print("FLUSH!", x, y, on) plan.clear() break q += 1
class BalorDriver: def __init__(self, service): self.service = service self.native_x = 0x8000 self.native_y = 0x8000 self.name = str(self.service) self.connection = GalvoController(service) self.service.add_service_delegate(self.connection) self.paused = False self.is_relative = False self.laser = False self._shutdown = False self.queue = list() self.plot_planner = PlotPlanner( dict(), single=True, smooth=False, ppi=False, shift=False, group=True ) self.value_penbox = None self.plot_planner.settings_then_jog = True self._aborting = False self._list_bits = None def __repr__(self): return "BalorDriver(%s)" % self.name @property def connected(self): if self.connection is None: return False return self.connection.connected def service_attach(self): self._shutdown = False def service_detach(self): self._shutdown = True def connect(self): self.connection.connect_if_needed() def disconnect(self): self.connection.disconnect() def abort_retry(self): self.connection.abort_connect() def hold_work(self, priority): """ This is checked by the spooler to see if we should hold any work from being processed from the work queue. For example if we pause, we don't want it trying to call some functions. Only priority jobs will execute if we hold the work queue. This is so that "resume" commands can be processed. @return: """ return priority <= 0 and self.paused def laser_off(self, *values): """ This command expects to stop pulsing the laser in place. @param values: @return: """ self.laser = False def laser_on(self, *values): """ This command expects to start pulsing the laser in place. @param values: @return: """ self.laser = True def plot(self, plot): """ This command is called with bits of cutcode as they are processed through the spooler. This should be optimized bits of cutcode data with settings on them from paths etc. @param plot: @return: """ self.queue.append(plot) def plot_start(self): """ This is called after all the cutcode objects are sent. This says it shouldn't expect more cutcode for a bit. @return: """ con = self.connection con.program_mode() self._list_bits = con._port_bits last_on = None con.set_wobble(None) queue = self.queue self.queue = list() for q in queue: settings = q.settings penbox = settings.get("penbox_value") if penbox is not None: try: self.value_penbox = self.service.elements.penbox[penbox] except KeyError: self.value_penbox = None con.set_settings(settings) con.set_wobble(settings) # LOOP CHECKS if self._aborting: con.abort() self._aborting = False return if con._port_bits != self._list_bits: con.list_write_port() self._list_bits = con._port_bits if isinstance(q, LineCut): last_x, last_y = con.get_last_xy() x, y = q.start if last_x != x or last_y != y: con.goto(x, y) con.mark(*q.end) elif isinstance(q, (QuadCut, CubicCut)): last_x, last_y = con.get_last_xy() x, y = q.start if last_x != x or last_y != y: con.goto(x, y) interp = self.service.interpolate step_size = 1.0 / float(interp) t = step_size for p in range(int(interp)): # LOOP CHECKS if self._aborting: con.abort() self._aborting = False return if con._port_bits != self._list_bits: con.list_write_port() self._list_bits = con._port_bits while self.paused: time.sleep(0.05) p = q.point(t) con.mark(*p) t += step_size elif isinstance(q, PlotCut): last_x, last_y = con.get_last_xy() x, y = q.start if last_x != x or last_y != y: con.goto(x, y) for x, y, on in q.plot[1:]: # LOOP CHECKS if self._aborting: con.abort() self._aborting = False return if con._port_bits != self._list_bits: con.list_write_port() self._list_bits = con._port_bits while self.paused: time.sleep(0.05) # q.plot can have different on values, these are parsed if last_on is None or on != last_on: last_on = on if self.value_penbox: # There is an active value_penbox settings = dict(q.settings) limit = len(self.value_penbox) - 1 m = int(round(on * limit)) try: pen = self.value_penbox[m] settings.update(pen) except IndexError: pass # Power scaling is exclusive to this penbox. on is used as a lookup and does not scale power. con.set_settings(settings) else: # We are using traditional power-scaling settings = self.plot_planner.settings current_power = ( float(settings.get("power", self.service.default_power)) / 10.0 ) con.power(current_power * on) con.mark(x, y) elif isinstance(q, DwellCut): start = q.start con.goto(start[0], start[1]) dwell_time = q.dwell_time * 100 # Dwell time in ms units in 10 us while dwell_time > 0: d = min(dwell_time, 60000) con.list_laser_on_point(int(d)) dwell_time -= d con.list_delay_time(int(self.service.delay_end / 10.0)) elif isinstance(q, WaitCut): dwell_time = q.dwell_time * 100 # Dwell time in ms units in 10 us while dwell_time > 0: d = min(dwell_time, 60000) con.list_delay_time(int(d)) dwell_time -= d elif isinstance(q, OutputCut): con.port_set(q.output_mask, q.output_value) con.list_write_port() elif isinstance(q, InputCut): input_value = q.input_value con.list_wait_for_input(input_value) else: self.plot_planner.push(q) for x, y, on in self.plot_planner.gen(): # LOOP CHECKS if self._aborting: con.abort() self._aborting = False return if con._port_bits != self._list_bits: self._list_bits = con._port_bits con.list_write_port() while self.paused: time.sleep(0.05) if on > 1: # Special Command. if on & PLOT_FINISH: # Plot planner is ending. break elif on & PLOT_SETTING: # Plot planner settings have changed. settings = self.plot_planner.settings penbox = settings.get("penbox_value") if penbox is not None: try: self.value_penbox = self.service.elements.penbox[ penbox ] except KeyError: self.value_penbox = None con.set_settings(settings) con.set_wobble(settings) elif on & ( PLOT_RAPID | PLOT_JOG ): # Plot planner requests position change. con.list_jump_speed(self.service.default_rapid_speed) con.goto(x, y) continue if on == 0: con.list_jump_speed(self.service.default_rapid_speed) con.goto(x, y) else: # on is in range 0 exclusive and 1 inclusive. # This is a regular cut position if last_on is None or on != last_on: last_on = on if self.value_penbox: # There is an active value_penbox settings = dict(self.plot_planner.settings) limit = len(self.value_penbox) - 1 m = int(round(on * limit)) try: pen = self.value_penbox[m] settings.update(pen) except IndexError: pass # Power scaling is exclusive to this penbox. on is used as a lookup and does not scale power. con.set_settings(settings) else: # We are using traditional power-scaling settings = self.plot_planner.settings current_power = ( float( settings.get( "power", self.service.default_power ) ) / 10.0 ) con.power(current_power * on) con.mark(x, y) con.list_delay_time(int(self.service.delay_end / 10.0)) self._list_bits = None con.rapid_mode() if self.service.redlight_preferred: con.light_on() con.write_port() else: con.light_off() con.write_port() def move_abs(self, x, y): """ This is called with the actual x and y values with units. If without units we should expect to move in native units. @param x: @param y: @return: """ old_current = self.service.current self.native_x, self.native_y = self.service.physical_to_device_position(x, y) if self.native_x > 0xFFFF: self.native_x = 0xFFFF if self.native_x < 0: self.native_x = 0 if self.native_y > 0xFFFF: self.native_y = 0xFFFF if self.native_y < 0: self.native_y = 0 self.connection.set_xy(self.native_x, self.native_y) new_current = self.service.current self.service.signal( "driver;position", (old_current[0], old_current[1], new_current[0], new_current[1]), ) def move_rel(self, dx, dy): """ This is called with dx and dy values to move a relative amount. @param dx: @param dy: @return: """ old_current = self.service.current unit_dx, unit_dy = self.service.physical_to_device_length(dx, dy) self.native_x += unit_dx self.native_y += unit_dy if self.native_x > 0xFFFF: self.native_x = 0xFFFF if self.native_x < 0: self.native_x = 0 if self.native_y > 0xFFFF: self.native_y = 0xFFFF if self.native_y < 0: self.native_y = 0 self.connection.set_xy(self.native_x, self.native_y) new_current = self.service.current self.service.signal( "driver;position", (old_current[0], old_current[1], new_current[0], new_current[1]), ) def home(self, x=None, y=None): """ This is called with x, and y, to set an adjusted home or use whatever home we have. @param x: @param y: @return: """ self.move_abs("50%", "50%") def rapid_mode(self): """ Expects to be in rapid jogging mode. @return: """ self.connection.rapid_mode() def program_mode(self): """ Expects to run jobs at a speed in a programmed mode. @return: """ self.connection.program_mode() def raster_mode(self, *args): """ Expects to run a raster job. Raster information is set in special modes to stop the laser head from moving too far. @return: """ pass def wait_finished(self): """ Expects to be caught up such that the next command will happen immediately rather than get queued. @return: """ self.connection.wait_finished() def function(self, function): function() def wait(self, time_in_ms): time.sleep(time_in_ms * 1000.0) def console(self, value): self.service(value) def beep(self): """ Wants a system beep to be issued. @return: """ self.service("beep\n") def signal(self, signal, *args): """ Wants a system signal to be sent. @param signal: @param args: @return: """ self.service.signal(signal, *args) def pause(self): """ Wants the driver to pause. @return: """ if self.paused: self.resume() return self.paused = True self.connection.pause() def resume(self): """ Wants the driver to resume. This typically issues from the realtime queue which means it will call even if we tell work_hold that we should hold the work. @return: """ self.paused = False self.connection.resume() def reset(self): """ Wants the job to be aborted and action to be stopped. @return: """ self.connection.abort() def status(self): """ Wants a status report of what the driver is doing. @return: """ pass def pulse(self, pulse_time): con = self.connection con.program_mode() con.frequency(self.service.default_frequency) con.power(self.service.default_power) if self.service.pulse_width_enabled: con.list_fiber_ylpm_pulse_width(self.service.default_pulse_width) dwell_time = pulse_time * 100 # Dwell time in ms units in 10 us while dwell_time > 0: d = min(dwell_time, 60000) con.list_laser_on_point(int(d)) dwell_time -= d con.list_delay_time(int(self.service.delay_end / 10.0)) con.rapid_mode() if self.service.redlight_preferred: con.light_on() con.write_port() else: con.light_off() con.write_port() def set_abort(self): self._aborting = True