Example #1
0
    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
Example #2
0
    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)
Example #3
0
 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}")
Example #4
0
    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
Example #5
0
 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
Example #6
0
    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}")
Example #7
0
    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)
Example #8
0
    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
Example #9
0
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