How to run a script on boot

I have a script for a robot that follows tennis balls, which uses opencv, the Jetson.GPIOs, etc etc. But i dont want to connect a monitor, mouse, and keyboard to the robot everytime i want to use the robot just to open the terminal and run the program. Is there a way to make my script run after the jetson boots?

several ways to do it:

  1. you may consider generate/modify /etc/rc.local file. for ubuntu 18.04 this file is not existed by default.
  2. you can use systemd, write your own service script.

In this video it is explained and it works form me without keyboard and mouse, but with monitor.
I made a script autostart.sh, edited the command and set x-right.

How made you the input via GPIO?

That is the Canonical (haha) way of doing it now.

To start, create a limited user to run your service as by running the following commands:

sudo addgroup --system robot
sudo adduser --system --no-create-home --disabled-login --disabled-password --ingroup robot robot

if your script user needs to be in extra groups you can add them with

adduser robot GROUP

…where GROUP is the group you want to add the user to (video, for example, to access the GPU). You can see the groups a user such as your own is in with the groups USER command where USER is the user you want to check the groups for. If your script needs gpio, follow the instructions at the following link to create a gpio group if it doesn’t already exist rather than running your script as root (and then add robot to the gpio group with “sudo adduser robot gpio”).

Next, create a service unit file in /lib/systemd/system by doing:

sudo touch /lib/systemd/system/robot.service
sudo your_favorite_editor_here /lib/systemd/system/robot.service

The touch may not be necessary if your editor creates the file automatically. Then paste the below into the file and save it, modifying the ExecStart line with the path to your script. I recommended to sudo cp your script to /usr/local/bin and use that path so the script copy can only be changed by root. It’s mode should be 755, but chances are it already is if you’re executing it. You may wish to chmod 755 /usr/bin/your_script.whatever anyway after you copy it just to make sure it’s not something like 777.

[Unit]
After=network.target
Description="Robot Service"
[Service]
ExecStart=/usr/local/bin/your_script.whatever
User=robot
[Install]
WantedBy=multi-user.target

The “After” line is only needed if your robot needs network access. Otherwise it can be omitted. Other targets or specific services for “After” or “WantedBy” may be more appropriate depending on your needs.

Once that’s done you need do:

sudo systemctl daemon-reload
sudo systemctl start robot

so systemd sees the new .service file you’ve created and starts the service. “stop” instead of start will stop the service with SIGINT, and then (iirc) SIGKILL. To check on the status of the service and it’s output, if your script sends things to stderr/stdout, do:

sudo systemctl status robot

If there is an error in your script and the service fails, that’s how you will know.

Once you have tested that it works, to make the change peristent and load it every boot, simply do:

sudo systemctl enable robot

and every time your system boots to any runlevel past multi-user.target, your script will be run. Use “disable” instead to disable the service.

The final (optional) icing on the cake is for your script to handle SIGINT and cleanly shut down. This may or may not be necessary depending on what your script does. You may need to, for example, free some system resource. Otherwise you may have problems starting your script a second time after stopping it. In Python you can do this by catching KeyboardInterrupt (in a try block, for example) or with the signals module. In bash, it’s like this. Systemd can send other signals, like SIGHUP when a “reload” is requested with “systemctl reload robot”, so you can, for example, reload a configuration file without restarting your service. You can catch that in Python with the signal module. For bash, it’s the same link as above.

8 Likes

That works, but it requires X to be running, which consumes a lot of system resources and requires a monitor connected or automatic login configured. With the systemd solution your robot can be headless and you can turn off x (sudo systemctl enable multi-user.target && sudo systemctl set-default multi-user.target) … It’s the same command with “graphical.target” instead of “multi-user.target” to go back.

Thanks for all the answers, i’m going to try to make it happen, i’ll keep you updated

When you say: sudo your_favorite_editor_here /lib/systemd/system/robot.service
What do you mean by: your_favorite_editor_here
I am using Code-OSS (Visual studio) is that what you mean?, if so how should i type that
Thanks again for the support, i really appreciate it.

Look forward to it. FYI, I edited my post to add some information on a gpio group that you will probably need for your script to work properly.

Correct. “code” should be the command to launch visual studio code, so “sudo code /lib/systemd/system/robot.service”

If this doesn’t work for whatever reason, you can always “sudo apt install nano” to install the
“nano” editor and then “sudo nano /lib/systemd/system/robot.service”

Middle mouse button usually pastes, and Ctrl+o will save. Ctrl+x will exit.

I tried, i think it didnt work because i couldnt make the adduser robot GROUP, but i dont know.
I had to get nano as you said because “Code” didnt work and added the lines you said.
I am really new to all this, i am sorry if i am slow.
I also added the screenshots of what i did and i’ll add my code just to be sure.

edit*******

I tried to redo from the step of: chmod 755/usr/bin/ctest2.py

and i got a different error(which is very long), the image is called latest error in attachements.

import cv2
import numpy as np
import math
import time
import Jetson.GPIO as GPIO
# MOTORS SETUP
# GPIO.cleanup() #clean trash from ports
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
# Motor left
GPIO.setup(18, GPIO.OUT)  # IN1
GPIO.setup(17, GPIO.OUT)  # IN2
# Motor right
GPIO.setup(23, GPIO.OUT)  # IN3
GPIO.setup(22, GPIO.OUT)  # IN4
# define color detection boundaries
lowerBound = np.array([29, 85, 6])
upperBound = np.array([64, 255, 255])
# camera setup
dispW=640
dispH=480
flip=2

camSet='nvarguscamerasrc !  video/x-raw(memory:NVMM), width=1980, height=1080, format=NV12, framerate=59/1 ! nvvidconv flip-method='+str(flip)+' ! video/x-raw, width='+str(dispW)+', height='+str(dispH)+', format=BGRx ! videoconvert ! video/x-raw, format=BGR ! appsink'
cam = cv2.VideoCapture(camSet)
# setting up kernel properties with matrices
kernelOpen = np.ones((5, 5))
kernelClose = np.ones((21, 21))
# camera position constants
xn = 0.112  # metros
yn = 0.088
zn = 0.23
alturaCam = 0.20
# lists for average function
xl = list()
yl = list()
rl = list()
# average function
def average(dispx, dispy, s):
    m = 10  # length of array accepted
    # appending values to each list
    xl.append(dispx)
    yl.append(dispy)
    # once the length of the generated lists equals m enter
    if len(xl) == m and len(yl) == m:
        # calculating the average measurements
        xm = sum(xl) / len(xl)
        ym = sum(yl) / len(yl)
        # cleaning up the lists
        del yl[0:m]
        del xl[0:m]
        # print results
        print("ball# " + str(s) + " x= " + str(round(xm, 4)) + " y= " + str(round(ym, 4)))
        return
    return

# main
while True:
    # camera setup
    ret, img = cam.read()
    #img = cv2.resize(img, (600, 500))  # resolution size
    # native camera resolution
    xr = img.shape[1]
    yr = img.shape[0]
    # first blur
    median = cv2.medianBlur(img, 7)
    # second blur
    blurred = cv2.GaussianBlur(img, (11, 11), 0)
    # convert BGR to HSV
    imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    # create the first mask filter
    mask = cv2.inRange(imgHSV, lowerBound, upperBound)
    # morphology, the second filter
    maskOpen = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernelOpen)
    # morphology, the third filter
    maskClose = cv2.morphologyEx(maskOpen, cv2.MORPH_CLOSE, kernelClose)
    maskFinal = maskClose
    # quantity of contours from the final mask filter
    im2,conts, h = cv2.findContours(maskFinal.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
    
    s = 0

    if (len(conts)==0):
                GPIO.output(18, GPIO.LOW)  # M1 in1
                GPIO.output(17, GPIO.LOW)  # M1 in2
                GPIO.output(23, GPIO.LOW)  # M2 in3
                GPIO.output(22, GPIO.LOW)
                print('stop')
    for i in range(len(conts)):
        s += 1  # ball counter
        # a = 1  # amount of balls permitted to see
        # drawing perceived contours
        cv2.drawContours(img, conts, -1, (255, 0, 0), 3)
        # drawing the minimum possible rectangle
        x, y, w, h = cv2.boundingRect(conts[i])
        # proceed only if minimum ball size threshold is met
        if w > 21 and h > 21:
            # draw a rectangle in the img to show it to the user
            cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
            # math for determining irl distance
            r = math.sqrt(w * h) / 2  # radius
            pry = (h / 2) * 2 * yn / yr  # radius in metres in the plane
            xp = x + w / 2  # center in x
            yp = y + h / 2  # center in y
            xi = xn * (2 * xp / xr - 1)  # vector pointed at the plane in x
            yi = yn * (1 - 2 * yp / yr)  # vector pointed at the plane in y
            zi = zn  # constant of z pointed at the plane
            m = math.sqrt(math.pow(xi, 2) + math.pow(yi, 2) + math.pow(zi, 2))
            xi = xi / m  # vector direction en x (sin mag)
            yi = yi / m  # vector direction en y
            zi = zi / m  # vector direction en z
            # Diameter approximation
            if pry * zi != 0:
                k1 = zn * 0.033 / (pry * zi)  # .33=r of tennis ball, calculus of distance from the ball in function of the diameter
            else:
                k1 = 0
                xi = xi * k1
                yi = yi * k1
                zi = zi * k1
            # rounding up final distances
            dispx = round(xi * 1000) / 1000
            dispy = round(yi * 1000) / 1000
            dispz = round(zi * 1000) / 1000
            # making a string that includes coordinates to reduce clutter
            dataStr = str(s) + "( " + str(dispx) + ", " + str(dispy) + ", " + str(dispz) + ")"
            # draw the coordinates in img to show it to the user
            cv2.putText(img, dataStr, (x, y), 0, 1, (0, 0, 0), 2, cv2.LINE_AA)
            # call average function to smooth out calculated coordinates
            average(dispx, dispy, s)
            # movement of the robot
            
            # if ball detected within a 2 metres range proceed
            if (k1 <= 2):
                if (dispx <= .2 and dispx >= -.2):
                    #foward
                    GPIO.output(18, GPIO.HIGH)  # M1 in1
                    GPIO.output(17, GPIO.LOW)  # M1 in2
                    GPIO.output(23, GPIO.HIGH)  # M2 in3
                    GPIO.output(22, GPIO.LOW)  # M2 in4
                    #time.sleep(.1)
                    print('foward')
                    
                # if the ball is in x positive, rotate to the right
                if (dispx > .2):
                    GPIO.output(18, GPIO.LOW)  # M1 in1
                    GPIO.output(17, GPIO.HIGH)  # M1 in2
                    GPIO.output(23, GPIO.HIGH)  # M2 in3
                    GPIO.output(22, GPIO.LOW)  # M2 in4
                    print('right')
                # if the ball is in x negative, rotate to the left
                if (dispx < -.2):
                    GPIO.output(18, GPIO.HIGH)   # M1 in1
                    GPIO.output(17, GPIO.LOW)  # M1 in2
                    GPIO.output(23, GPIO.LOW)  # M2 in3
                    GPIO.output(22, GPIO.HIGH)   # M2 in4
                    print('left')
                    # if the ball is in y positive but closer to the center, rotate slower to the right
                
        if cv2.waitKey(1)==ord('q'):
            GPIO.output(18, GPIO.LOW)  # M1 in1
            GPIO.output(17, GPIO.LOW)  # M1 in2
            GPIO.output(23, GPIO.LOW)  # M2 in3
            GPIO.output(22, GPIO.LOW)
            GPIO.cleanup()
            break
    if cv2.waitKey(1)==ord('q'):
        GPIO.output(18, GPIO.LOW)  # M1 in1
        GPIO.output(17, GPIO.LOW)  # M1 in2
        GPIO.output(23, GPIO.LOW)  # M2 in3
        GPIO.output(22, GPIO.LOW)
        GPIO.cleanup()
        break
    cv2.imshow("maskClose", maskClose)
    # cv2.imshow("maskOpen",maskOpen)
    # cv2.imshow("mask",mask)
    cv2.imshow("cam", img)
    cv2.waitKey(10)



There is no need to apologise and you are not slow:) I will help you tomorrow with the errors. It is my fault for being unclear.


That looks fine except the GROUP part. I should have been clearer. Where I wrote “where GROUP is the group you want to add the user to”, i meant that you replace GROUP with whatever gruop you need. In your case you should probably add your user to the “video” group and the “gpio” group.

Example output from my terminal:

$ sudo adduser robot video
Adding user `robot' to group `video' ...
Adding user robot to group video
Done.
 $ sudo adduser robot gpio
adduser: The group `gpio' does not exist.

The gpio group should exist on your system since you have jetson.GPIO installed but if it’s not, and you get the same error as above, please follow the instructions here to add it, and then re-run “sudo adduser robot gpio”.


The above looks correct. Just save it with ctrl+O and you should be fine.

This one, it looks fine until the end where the service failed to start. To find out why it failed, systemctl is telling you to run “systemctl status robot” (you can leave off the .service) to check the status. I can see you did that here:

Unfortunately the terminal output is clipped, so I can’t see all of it.
You can check the full log of your program with “journalctl -u robot”
Please run that command and provide the output.

Aha. The problem may be an incorrect path. If your script is located in /usr/local/bin, the commands to ensure proper permissions should be:

sudo chown root:root /usr/local/bin/ctest2.py
sudo chmod 755 /usr/local/bin/ctest2.py

If the script is not there, that could be the cause of why it’s not running.

Your code looks fine to me. I have added and removed some things so it should run better headless, and so it will shut down cleanly on SIGINT. Please review my comments within the code for details.

import cv2
import numpy as np
import math
import time
import Jetson.GPIO as GPIO

# MOTORS SETUP
# GPIO.cleanup() #clean trash from ports
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
# Motor left
GPIO.setup(18, GPIO.OUT)  # IN1
GPIO.setup(17, GPIO.OUT)  # IN2
# Motor right
GPIO.setup(23, GPIO.OUT)  # IN3
GPIO.setup(22, GPIO.OUT)  # IN4
# define color detection boundaries
lowerBound = np.array([29, 85, 6])
upperBound = np.array([64, 255, 255])
# camera setup
dispW = 640
dispH = 480
flip = 2

camSet = 'nvarguscamerasrc !  video/x-raw(memory:NVMM), width=1980, height=1080, format=NV12, framerate=59/1 ! nvvidconv flip-method=' + str(
    flip) + ' ! video/x-raw, width=' + str(dispW) + ', height=' + str(
    dispH) + ', format=BGRx ! videoconvert ! video/x-raw, format=BGR ! appsink'
cam = cv2.VideoCapture(camSet)
# setting up kernel properties with matrices
kernelOpen = np.ones((5, 5))
kernelClose = np.ones((21, 21))
# camera position constants
xn = 0.112  # metros
yn = 0.088
zn = 0.23
alturaCam = 0.20
# lists for average function
xl = list()
yl = list()
rl = list()

# average function
def average(dispx, dispy, s):
    m = 10  # length of array accepted
    # appending values to each list
    xl.append(dispx)
    yl.append(dispy)
    # once the length of the generated lists equals m enter
    if len(xl) == m and len(yl) == m:
        # calculating the average measurements
        xm = sum(xl) / len(xl)
        ym = sum(yl) / len(yl)
        # cleaning up the lists
        del yl[0:m]
        del xl[0:m]
        # print results
        print("ball# " + str(s) + " x= " + str(round(xm, 4)) + " y= " + str(round(ym, 4)))
        return
    return

def cleanup():
    GPIO.output(18, GPIO.LOW)  # M1 in1
    GPIO.output(17, GPIO.LOW)  # M1 in2
    GPIO.output(23, GPIO.LOW)  # M2 in3
    GPIO.output(22, GPIO.LOW)
    GPIO.cleanup()

#(mdegans) wrapping this so that interrupt can be caught (see bottom of file)
def main():
    while True:
        # camera setup
        ret, img = cam.read()
        # img = cv2.resize(img, (600, 500))  # resolution size
        # native camera resolution
        xr = img.shape[1]
        yr = img.shape[0]
        #(mdegans): it doesn't look like these two blurred images are used, so commenting them out
        #(mdegans): you probably want to edit the code below to fix it
        # first blur
        # median = cv2.medianBlur(img, 7)
        # second blur
        # blurred = cv2.GaussianBlur(img, (11, 11), 0)
        # convert BGR to HSV
        imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        # create the first mask filter
        mask = cv2.inRange(imgHSV, lowerBound, upperBound)
        # morphology, the second filter
        maskOpen = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernelOpen)
        # morphology, the third filter
        maskClose = cv2.morphologyEx(maskOpen, cv2.MORPH_CLOSE, kernelClose)
        maskFinal = maskClose
        # quantity of contours from the final mask filter
        im2, conts, h = cv2.findContours(maskFinal.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

        s = 0

        if (len(conts) == 0):
            GPIO.output(18, GPIO.LOW)  # M1 in1
            GPIO.output(17, GPIO.LOW)  # M1 in2
            GPIO.output(23, GPIO.LOW)  # M2 in3
            GPIO.output(22, GPIO.LOW)
            print('stop')
        for i in range(len(conts)):
            s += 1  # ball counter
            # a = 1  # amount of balls permitted to see
            # drawing perceived contours
            cv2.drawContours(img, conts, -1, (255, 0, 0), 3)
            # drawing the minimum possible rectangle
            x, y, w, h = cv2.boundingRect(conts[i])
            # proceed only if minimum ball size threshold is met
            if w > 21 and h > 21:
                # draw a rectangle in the img to show it to the user
                cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
                # math for determining irl distance
                r = math.sqrt(w * h) / 2  # radius
                pry = (h / 2) * 2 * yn / yr  # radius in metres in the plane
                xp = x + w / 2  # center in x
                yp = y + h / 2  # center in y
                xi = xn * (2 * xp / xr - 1)  # vector pointed at the plane in x
                yi = yn * (1 - 2 * yp / yr)  # vector pointed at the plane in y
                zi = zn  # constant of z pointed at the plane
                m = math.sqrt(math.pow(xi, 2) + math.pow(yi, 2) + math.pow(zi, 2))
                xi = xi / m  # vector direction en x (sin mag)
                yi = yi / m  # vector direction en y
                zi = zi / m  # vector direction en z
                # Diameter approximation
                if pry * zi != 0:
                    k1 = zn * 0.033 / (
                            pry * zi)  # .33=r of tennis ball, calculus of distance from the ball in function of the diameter
                else:
                    k1 = 0
                    xi = xi * k1
                    yi = yi * k1
                    zi = zi * k1
                # rounding up final distances
                dispx = round(xi * 1000) / 1000
                dispy = round(yi * 1000) / 1000
                dispz = round(zi * 1000) / 1000
                # (mdegans): commented out display code because we are running headless now
                # making a string that includes coordinates to reduce clutter
                # dataStr = str(s) + "( " + str(dispx) + ", " + str(dispy) + ", " + str(dispz) + ")"
                # draw the coordinates in img to show it to the user
                # cv2.putText(img, dataStr, (x, y), 0, 1, (0, 0, 0), 2, cv2.LINE_AA)
                # call average function to smooth out calculated coordinates
                average(dispx, dispy, s)
                # movement of the robot

                # if ball detected within a 2 metres range proceed
                if (k1 <= 2):
                    if (dispx <= .2 and dispx >= -.2):
                        # foward
                        GPIO.output(18, GPIO.HIGH)  # M1 in1
                        GPIO.output(17, GPIO.LOW)  # M1 in2
                        GPIO.output(23, GPIO.HIGH)  # M2 in3
                        GPIO.output(22, GPIO.LOW)  # M2 in4
                        # time.sleep(.1)
                        print('foward')

                    # if the ball is in x positive, rotate to the right
                    if (dispx > .2):
                        GPIO.output(18, GPIO.LOW)  # M1 in1
                        GPIO.output(17, GPIO.HIGH)  # M1 in2
                        GPIO.output(23, GPIO.HIGH)  # M2 in3
                        GPIO.output(22, GPIO.LOW)  # M2 in4
                        print('right')
                    # if the ball is in x negative, rotate to the left
                    if (dispx < -.2):
                        GPIO.output(18, GPIO.HIGH)  # M1 in1
                        GPIO.output(17, GPIO.LOW)  # M1 in2
                        GPIO.output(23, GPIO.LOW)  # M2 in3
                        GPIO.output(22, GPIO.HIGH)  # M2 in4
                        print('left')
                        # if the ball is in y positive but closer to the center, rotate slower to the right
        # (mdegans): this part probably needs X to run and quitting is handled
        # below
        #     if cv2.waitKey(1) == ord('q'):
        #         cleanup()
        #         break
        # if cv2.waitKey(1) == ord('q'):
        #     cleanup()
        #     break
        # cv2.imshow("maskClose", maskClose)
        # # cv2.imshow("maskOpen",maskOpen)
        # # cv2.imshow("mask",mask)
        # cv2.imshow("cam", img)
        # cv2.waitKey(10)

if __name__ == '__main__':
    # (mdegans): I have wrapped your main() function in a try block to catch a KeyboardInterrupt (ctrl+c) or SIGINT
    # (mdegans): this ensures your script will close cleanly when you run "systemctl stop robot"
    try:
        main()
    except KeyboardInterrupt:
        print("Got SIGINT. Shutting down.")
        cleanup()

Thanks for arriving!

I read the changes and comments of the code and i think that i understood everything, but what does SIGINT means?
I made a new file named ctest3.py that has all your changes.

I could succesfully add the groups.
I edited the nano script for the new file as:

[Unit]
Description="Robot Service"
[Service]
ExecStart=/usr/local/bin/ctest3.py
User=robot
[Install]
WantedBy=multi-user.target

I moved the new file to the correct directory /usr/local/bin/ and deleted the previous one
And added the correct permissions
the new error seems much shorter and it fits in the screen haha, i think we are moving foward


terminal1day2.png

Also a doubt that i’ve had for a while, why if i am the only user in my jetson nano, and i am the administrator, sometimes when i run a command it asks me for my password? is there any way to grant access to myself in all commands?

Yesterday I just happened to publish a post that includes Python Packaging and Startup:

The article talks about taking a Python 3 script and turning it into a system module to be launched at startup. It might be too early in your development cycle to do all of that, so looking at the above issue …

I think that one of the issues above is the following line:

ExecStart=/usr/local/bin/ctest3.py

ctest3.py is not a binary file, and the OS doesn’t know how to execute it. Typically at the top of your Python 3 file you would place a shebang line:

#!/usr/bin/env python3

which indicates to the OS that the file is an executable script, and to use Python 3 to launch it. The /usr/bin/env indicates where to look for the python3 executable. You must mark the .py file as executable (as you did above with the chmod). I’m not sure that’s adequate for the ExecStart line in the .service file, but it’s something to try. If that’s not enough, you could try something more formal like:

ExecStart=/bin/sh -c “python3 /usr/local/bin/ctest3.py”

Everything looks good in the screenshots. The execformat error is due to not having a shebang at the top of your file. The first line should be

#!/usr/bin/python3

Sorry for missing that. It should execute with python3 now.
If it doesn’t, you should be able to just do:

ExecStart=/usr/bin/python3 /usr/local/bin/ctest3.py

Kangalow’s line should also work. Just make sure you test the service with “start” before you “enable” it on every boot, and make sure you have a way to remotely access your nano so you can turn off the service just in case you need to. Above all, have fun!

You can:

  • put all the commands in a script (a regular text file beginning with “#!/bin/sh” or “#!/bin/bash”), chmod +x it to make it executable, and run the script with sudo (preferable). You may also wish to add “set -e” to the script as the second line so that if one command fails, the entire script fails. This is useful for things where step 2 absoutely depends on step 1 and should never be executed without step 1.
    or
  • increase the timeout from 15 minutes to something more

When you are running a program at the command line and you press Ctrl+c, you are sending a SIGINT (interrupt) signal, to tell the program to quit. A python script can catch and handle this signal by catching KeyboardInterrupt as shown in the modification. It’s often necessary do this if your script needs to clean up after itself. Systemd, which manages background services and is controlled by the systemctl command, will send various signals to your app which you can use to handle events. Any of these can be caught and handled by the “signal” python module linked to on the previous page.

Thanks for all the help!

now i can do it in two different ways, the .service way that you thought me, and i found that

gedit ~/.bashrc

then at the end of the all

echo Running at boot
sudo python3 /home/benja/Desktop/project/ctest2.py

adding the terminal as a start up program it also works fine

That will work, but it requires you to log in. It also will have the side effect of running your script every time you open a terminal. You may not want that. .bashrc executes every time a bash instance is launched.

Also, if your user is in the appropriate groups you may wish to leave off the “sudo” so your script doesn’t run as root. There seems to be nothing dangerous in there now, but it’s good practice. Especially with Python it’s easy to accidentally install malware since there is plenty in PyPi.

Malicious hackers use “typo squatting” to substitute bad packages for good ones. If you make a typo while doing a ‘pip install’, for example, you could end up installing malware. Running a process as a regular user, or better, a user dedicated to just that one thing (eg. robot) can limit the damage malware can do to the rest of your system.

In related news, I am working on some software to automate a lot of what’s been done in this thread. I will probably post it by Christmas time in the Jetson projects section. If I remember I will also post a link here.