Luis Pacheco

Week 15: Interface and Application Programming

May 1, 2024

Intro

This week’s objective is to write an application that interfaces a user with an input and/or output device. For this week, I reused a previous project where I wrote a serial G-code sender in Kivy with some OpenCV integration and used it for the machine in week 10. In this case I will connect to the robot using the serial protocol that its common on GRBL/Marlin based firmware.

Objectives

  • Connect to the Cable Robot from week 10 using a serial connection (USB) Serial
  • Move the Cable Robot on the XY axis (Jog)
  • Automatically move to a target using OpenCV
  • Send a G-code file

Tools

  • Python
  • Kivy
  • Cable Robot from week 10

Process

I suggest you follow the Kivy tutorial first.

This is a Python-based interface that uses the Kivy library.

Installation Process for Kivy and Required Libraries

Step 1: Install Python

Ensure that Python is installed on your system. You can download the latest version of Python from python.org. Make sure to add Python to your PATH during the installation process.

Step 2: Install pip

pip is the package installer for Python. It is usually installed by default with Python, but if not, you can install it using the following instructions:

  1. Download get-pip.py from bootstrap.pypa.io/get-pip.py.
  2. Run the following command in your terminal or command prompt:

    1
    
     python get-pip.py
    

Step 3: Set Up a Virtual Environment (Optional but Recommended)

It’s a good practice to create a virtual environment for your project to manage dependencies separately.

  1. Create a virtual environment:

    1
    
     python -m venv kivy_env
    
  2. Activate the virtual environment:

    • On Windows:

      1
      
        kivy_env\Scripts\activate
      
    • On macOS and Linux:

      1
      
        source kivy_env/bin/activate
      

Step 4: Install Kivy

Install Kivy using pip:

1
pip install "kivy[base]" kivy_examples

Step 5: Install Additional Libraries

Install the additional libraries required for your project:

  1. Install pyserial for serial communication:

    1
    
     pip install pyserial
    
  2. Install OpenCV for computer vision tasks:

    1
    
     pip install opencv-python
    
  3. Install imutils for image processing utilities:

    1
    
     pip install imutils
    
  4. Install numpy for numerical operations:

    1
    
     pip install numpy
    

Functionality

The UI should offer the following functionality:

  • Connect to the robot (Serial)
  • Define a “Home” location for the robot
  • Enable/Disable motors
  • Emergency Stop
  • Send G-code commands to move the robot (jog)
  • Custom G-code (move to coordinate)
  • Enable the camera and track the distance to the “target”

Kivy UI Definition

First, we use the custom Kivy language to define the UI. This involves dividing the screen into grids and adding text and IDs so we can use the ID to run a function in Python. This is similar to CSS, where you define each UI element and its properties. Each indentation signifies that there can be UI elements inside other UI elements. The on_press parameter allows us to connect that element to a function in our Python code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#__author__ = "Luis Pacheco"
#__copyright__ = "Copyright 2019, Luis Pacheco"
#__license__ = "GPL"
#__version__ = "0.0.1"
#__maintainer__ = "Luis Pacheco"
#__email__ = "luigi@luigipacheco.com"
#__status__ = "Alpha"

<mainLayout>:
    cols : 1
    BoxLayout:
        cols : 2
        BoxLayout: #functions
            orientation : "vertical"
            size_hint_x : None
            width:app.button_size
            Button:
                id: connect
                text: "Connect"
                size_hint_x : None
                width:app.button_size
                on_press: root.connect()

            Button:
                id: setHome
                text: "Set Home"
                size_hint_x : None
                width:app.button_size
                on_press: root.setHome(False)

            Button:
                id: motorsOff
                text: "Turn Off motors"
                size_hint_x : None
                width: app.button_size
                on_press: root.sendMotorsOff()
            Button:
                id: camera
                text: "CameraOn"
                size_hint_x : None
                width: app.button_size
                on_press: root.camera()
            Button:
                id: goto
                text: "goTo"
                size_hint_x : None
                width: app.button_size
                on_press: root.goTo()

            Button:
                id: stop
                text: "Emergency STOP"
                size_hint_x : None
                background_color: (1,0,0,1)
                width:app.button_size
                on_press: root.emergencyStop()

        BoxLayout:
            orientation : "vertical"
            col_default_width : 100
            BoxLayout:
                id: opencv
 #           Camera:
 #               id: camera
 #               index : 0
 #               resolution: (640, 480)
 #               play: True

            BoxLayout:
                GridLayout:
                    row_default_height:40
                    row_force_default : True
                    cols : 2
                    orientation : "lr-tb"
                    TextInput:
                        text:
                        id: port
                    Button:
                        text: "Set port"
                        on_press: root.btn_clk()
                    TextInput:
                        id: baudrate
                        text:
                    Button:
                        text: "Set Baudrate"
                        on_press: root.btn_clk()
                    TextInput:
                        id: machineWidth
                        text:
                    Button:
                        text: "Set Width"
                        on_press: root.btn_clk()
                    TextInput:
                        id: setStep
                        text:
                    Button:
                        text: "Steps per Rev."
                    TextInput:
                        id: mmPerMin
                        text:
                    Button:
                        text: "Set mm per Rev."
                        on_press: root.btn_clk()
                    TextInput:
                        id:speed
                        text:
                    Button:
                        text: "Set Speed"
                        on_press: root.btn_clk()
                    Label:
                    Button:
                        id: save
                        text: "Save parameters"
                        on_press: root.save()
                BoxLayout:
                    orientation: "vertical"
                    GridLayout:
                        padding: 10
                        cols: 3
                        Label:
                        Button:
                            id: up
                            text: "UP"
                            on_press: root.moveUP()
                        Label:
                        Button:
                            id: left
                            text: "LEFT"
                            on_press: root.moveLEFT()
                        TextInput:
                            id: step
                            text: "1"
                        Button:
                            id: right
                            text: "RIGHT"
                            on_press: root.moveRIGHT()
                        Label:
                        Button:
                            id: down
                            text: "DOWN"
                            on_press: root.moveDOWN()
                        Label:
                BoxLayout:
                    orientation: "vertical"
                    BoxLayout:
                        orientation : "horizontal"
                        size_hint_y : None
                        height:30
                        Label:
                            text: "X"
                        TextInput:
                            id: quadX
                            text: "1"
                        Label:
                            text: "Y"
                        TextInput:
                            id: quadY
                            text: "1"
                        Button:
                            id: right
                            text: "quad"
                            on_press: root.drawQuad()
                    BoxLayout:
                        orientation : "horizontal"
                        size_hint_y : None
                        height:30
                        Label:
                            text: "X"
                        TextInput:
                            id: quadX
                            text: "1"
                        Label:
                            text: "Y"
                        TextInput:
                            id: quadY
                            text: "1"
                        Button:
                            id: right
                            text: "quad"
                            on_press: root.drawQuad()
                    BoxLayout:
                        orientation : "horizontal"
                        size_hint_y : None
                        height:30
                        Label:
                            text: "X"
                        TextInput:
                            id: moveX
                            text: "1"
                        Label:
                            text: "Y"
                        TextInput:
                            id: moveY
                            text: "1"
                        Button:
                            id: right
                            text: "moveXY"
                            on_press: root.moveXY()
                    BoxLayout:
                        orientation : "horizontal"
                        size_hint_y : None
                        height:30
                        Button:
                            id: absolute
                            text: "Absolute"
                            on_press: root.absolute()
                        Button:
                            id: relative
                            text: "Relative"
                            on_press: root.relative()

Python Code

In the corresponding Python file, we define the functions that the UI elements will call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
__author__ = "Luis Pacheco"
__copyright__ = "Copyright 2019, Luis Pacheco"
__contributors__ = "Ricardo Mura"
__license__ = "GPL"
__version__ = "0.0.1"
__maintainer__ = "Luis Pacheco"
__email__ = "luigi@luigipacheco.com"
__status__ = "Alpha"

import os
import serial
import time
import cv2
import imutils
import math
import urllib
import numpy as np

from kivy.app import App
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.uix.gridlayout import GridLayout
from kivy.uix.boxlayout import  BoxLayout
from kivy.uix.textinput import TextInput
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.image import Image
from kivy.graphics.texture import Texture
import numpy as np



step = 1
testing = True
low_blue = np.array([100,50,100])
high_blue = np.array([140,255,255])
low_red = np.array([20, 50, 70])
high_red = np.array([70, 255, 255])
camera_index = 0
tol = 20
class KivyCamera(Image):
    def __init__(self, capture, fps, **kwargs):
        super(KivyCamera, self).__init__(**kwargs)
        self.capture = capture
        Clock.schedule_interval(self.update, 1.0 / fps)
        self.xdist = 0
        self.ydist = 0
        self.gotoOn = False
        Clock.schedule_interval(self.goto, 0.5)


    def xDistance(self, x, x1):
        dx = x1 - x
        return dx

    def yDistance(self, y, Y1):
        dy = y1 - y
        return dy

    def calculateDistance(self, dx, dy):
        dist = math.sqrt(dx ** 2 + dy ** 2)
        return dist

    def findcolor(self, a, b, hsv, frame):
        low = np.array([a])
        high = np.array([b])
        mask = cv2.inRange(hsv, low, high)
        mask1 = cv2.bitwise_and(frame, frame, mask=mask)
        cnts = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
        if len(cnts) < 1:
            return
        # cnts = imutils.grab_contours(cnts)
        c = max(cnts, key=cv2.contourArea)
        ((x, y), radius) = cv2.minEnclosingCircle(c)
        M = cv2.moments(c)
        if M["m00"] == 0 :  return
        center = (int(M["m10"] / M["m00"]), int(M["m01"] / M["m00"]))
        cv2.circle(frame, (int(x), int(y)), int(radius),(0, 255, 255), 2)
        cv2.circle(frame, center, 5, (255, 0, 0), -1, 8, 0)
        return center

    def update(self, dt):
        ret, frame = self.capture.read()
        hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        #cv2.line(frame, (0, 0), (511, 511), (255, 0, 0), 5)
        hsv_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        # Red color
        self.redcenter = self.findcolor(low_red, high_red, hsv_frame, frame)
        # Blue color
        self.bluecenter = self.findcolor(low_blue, high_blue,hsv_frame, frame)

        if self.redcenter and self.bluecenter:
            # self.xdist = self.xDistance(redcenter[0], bluecenter[0])
            # self.ydist = self.xDistance(redcenter[1], bluecenter[1])
            # absolutedistance = self.calculateDistance(xdist, ydist)
            # print(absolutedistance)
            self.xdist = self.xDistance(self.redcenter[0], self.bluecenter[0])
            self.ydist = self.xDistance(self.redcenter[1], self.bluecenter[1])



        if ret:
            # convert it to texture
            buf1 = cv2.flip(frame, 0)
            buf = buf1.tostring()
            image_texture = Texture.create(
                size=(frame.shape[1], frame.shape[0]), colorfmt='bgr')
            image_texture.blit_buffer(buf, colorfmt='bgr', bufferfmt='ubyte')
            # display image from the texture
            self.texture = image_texture
    def goToTrigger(self):
        if self.gotoOn == False:
            self.gotoOn = True
        else:
            self.gotoOn = False

    def goto(self,dt):
        if self.gotoOn == True:
            if self.bluecenter and self.redcenter:
                if self.xdist <= (-tol):
                    moveX = 10
                    data = "G0 X{0:d}\n".format(moveX)
                    print(data)
                    if testing:
                        s.write(data.encode())

                elif self.xdist >= (+tol):
                    moveX = 10
                    data = "G0 X-{0:d}\n".format(moveX)
                    print(data)
                    if testing:
                        s.write(data.encode())

                if self.ydist <= (-tol):
                    moveY = 10
                    data = "G0 Y{0:d}\n".format(moveY)
                    print(data)
                    if testing:
                        s.write(data.encode())

                elif self.ydist >= (+tol):
                    moveY = 10
                    data = "G0 Y-{0:d}\n".format(moveY)
                    print(data)
                    if testing:
                        s.write(data.encode())
                # if self.xdist <= tol and self.ydist <= tol:
                #     self.goToTrigger()


class mainLayout(GridLayout):
    def __init__(self):
        super(mainLayout, self).__init__()
        self.cols = 2
        self.cameraOn = False

        if os.path.isfile("prev_settings.txt"):
            with open("prev_settings.txt","r") as f:
                d = f.read().split(",")
                prev_port = d[0]
                prev_baudrate = d[1]
                prev_width= d[2]
                prev_steps = d[3]
                prev_mmpr = d[4]
                prev_speed = d[5]
        else:
            prev_port = ""
            prev_baudrate = ""
            prev_width=""
            prev_steps = ""
            prev_mmpr = ""
            prev_speed = ""

        port = self.ids['port']
        baudrate = self.ids['baudrate']
        machineWidth = self.ids['machineWidth']
        steps = self.ids['setStep']
        mmpr = self.ids['mmPerMin']
        speed = self.ids['speed']

        port.text= prev_port
        baudrate.text = prev_baudrate
        machineWidth.text = prev_width
        steps.text = prev_steps
        mmpr.text=prev_mmpr
        speed.text = prev_speed

    def camera(self):
        layout = self.ids['opencv']
        self.capture = cv2.VideoCapture(camera_index)
        self.my_camera = KivyCamera(capture=self.capture, fps=30)
        layout.add_widget(self.my_camera)

    def goTo(self):
            self.my_camera.goToTrigger()

    def on_stop(self):
        # without this, app will not exit even if the window is closed
        self.capture.release()

    def save(self):
        port = self.ids['port'].text
        baudrate = self.ids['baudrate'].text
        machineWidth = self.ids['machineWidth'].text
        steps = self.ids['setStep'].text
        mmpr = self.ids['mmPerMin'].text
        speed = self.ids['speed'].text
        toolWidth = "3"
        print("cake has been pressed")
        self.sendSpecs(machineWidth,toolWidth,steps,float(mmpr)*3.14159)
        self.setHome(machineWidth)
        self.sendSpeed(speed)
        with open("prev_settings.txt","w") as f:
             f.write(f"{port},{baudrate},{machineWidth},{steps},{mmpr},{speed}")

    def connect(self):
        time.sleep(1)
        port = self.ids['port'].text
        baudrate = self.ids['baudrate'].text
        machineWidth = self.ids['machineWidth'].text
        steps = self.ids['setStep'].text
        mmpr = self.ids['mmPerMin'].text
        speed = self.ids['speed'].text
        toolWidth = "3"
        s = serial.Serial('/dev/'+port, int(baudrate))
        time.sleep(1)
        print(f"cake has been pressed")
        self.sendSpecs(machineWidth,toolWidth,steps,float(mmpr)*3.14159)
        self.setHome(machineWidth)
        self.sendSpeed(speed)
        print('Opening Serial Port')

    def send(self,data):
        print("output = " + data)
        if testing:
            s.write(data.encode())

    def setHome(self,machineWidth):
        if machineWidth == False:
            machineWidth = self.ids['machineWidth'].text
        data = "M1 X00 Y"+ str(int(machineWidth)/2)+ "\n"
        self.send(data)


    def sendRelative(self):
        msg = "G91\n"
        self.send(msg)

    def sendSpecs(self,machineWidth,penWidth,stepsPerRev,mmPerRev):
        specs = "M4 X" + str(machineWidth) + " E" + str(penWidth) + " S" + str(stepsPerRev) + " P" + str(
            mmPerRev) + "\n"
        self.send(specs)

    def sendSpeed(self, speed):
        speed = "G0 F" + str(speed) + "\n"
        self.send(speed)

    def moveXY(self):
        self.sendRelative()
        time.sleep(1)
        moveX = int(self.ids['moveX'].text)
        moveY = int(self.ids['moveY'].text)
        data = "G0 X{0:d} Y{1:d}\n".format(moveX, moveY)
        self.send(data)

    def moveUP(self):
        self.sendRelative()
        myid = self.ids['step']
        moveY = int(myid.text)
        data = "G0 Y-{0:d}\n".format(moveY)
        self.send(data)

    def moveRIGHT(self):
        self.sendRelative()
        myid = self.ids['step']
        moveX = int(myid.text)
        data = "G0 X{0:d}\n".format(moveX)
        self.send(data)

    def moveLEFT(self):
        self.sendRelative()
        myid = self.ids['step']
        moveX = int(myid.text)
        data = "G0 X-{0:d}\n".format(moveX)
        self.send(data)


    def moveDOWN(self):
        self.sendRelative()
        myid = self.ids['step']
        moveY = int(myid.text)
        data = "G0 Y{0:d}\n".format(moveY)
        self.send(data)

    def sendMotorsOff(self):
        data = "M84\n"
        self.send(data)

    def emergencyStop(self):
        data = "M112\n"
        self.send(data)

    def drawQuad(self):
        self.sendRelative()
        moveX = int(self.ids['quadX'].text)
        moveY = int(self.ids['quadY'].text)
        data = "G0 X{0:d}\n".format(moveX)
        self.send(data)
        data = "G0 Y{0:d}\n".format(moveY)
        self.send(data)
        data = "G0 X-{0:d}\n".format(moveX)
        self.send(data)
        data = "G0 Y-{0:d}\n".format(moveY)
        self.send(data)
    # def drawOnTarget(self):
    #     if self.my_camera.xdist >= xdist or self.my_camera.ydist >= tol:
    #         self.goTo()
    #         if


class guiApp(App):
    def build(self):
        self.button_size = 150
        return mainLayout()

if __name__ == "__main__":
    if testing:
        try:
            s = serial.Serial('/dev/ttyACM0', 115200)
            #s = Serial(port=port, baudrate=self.baudrate, timeout=self.timeout)
        except:
            print("Failed to connect")
    guiApp().run()
    cv2.destroyAllWindows()

UI Functionality Description

Application

Left Panel

  1. Connect:
    • Initiates a connection to the cable robot. Typically, this button would open the specified serial port and establish communication.
  2. Set Home:
    • Sets the current position of the cable robot as the home position. This can be used as a reference point for subsequent movements.
  3. Turn Off Motors:
    • Disables the motors, stopping all movements of the cable robot. This is useful for emergency situations or when adjustments need to be made safely.
  4. CameraOn:
    • Activates the camera feed, allowing the user to visually track the position of the cable robot.
  5. Emergency STOP:
    • Immediately halts all operations and movements of the cable robot. This is a critical safety feature.

Middle Panel

  1. Set port:
    • Input field and button to specify and set the serial port used for communication with the cable robot.
  2. Set Baudrate:
    • Input field and button to set the baud rate for serial communication.
  3. Set Width:
    • Input field and button to set the width parameter for the cable robot’s movements.
  4. Steps per Rev.:
    • Input field and button to set the number of steps per revolution for the motor.
  5. Set mm per Rev.:
    • Input field and button to set the millimeters per revolution, which helps in calibrating the robot’s movements.
  6. Set Speed:
    • Input field and button to set the speed of the cable robot.
  7. Save parameters:
    • Button to save all the specified parameters for the cable robot.

Right Panel

  1. Directional Buttons (UP, DOWN, LEFT, RIGHT):
    • Buttons to manually jog the cable robot in the respective directions.
  2. Coordinate Input (X, Y):
    • Input fields to specify coordinates for the cable robot to move to.
  3. moveXY:
    • Button to execute a move command to the specified X and Y coordinates.
  4. Absolute / Relative Toggle:
    • Switch to toggle between absolute and relative positioning modes for the cable robot’s movements.

Distance test: Application

Conclusion

Creating a user interface with Kivy to control the Cable Robot was fairly quick. The combination of Kivy for the UI and Python seems like a very easy way to get started making UIs. The ability to send G-code commands and monitor the robot’s movements enhances the overall functionality and usability of the system.

Copyright By Luis Pacheco