Esempio n. 1
0
def pid_loop(state):
    i = 0
    pidhist = config.pid_hist_len * [0.]
    temphist = config.temp_hist_len * [0.]
    temperr = config.temp_hist_len * [0]
    temp = 25.
    lastsettemp = state['brewtemp']
    lasttime = time()

    sensor = MAX31855(SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO),
                      DigitalInOut(board.D5))
    pid = PID(Kp=config.pidc_kp,
              Ki=config.pidc_ki,
              Kd=config.pidc_kd,
              setpoint=state['brewtemp'],
              sample_time=config.time_sample,
              proportional_on_measurement=False,
              output_limits=(-config.boundary, config.boundary))

    while True:
        try:
            temp = sensor.temperature
            del temperr[0]
            temperr.append(0)
            del temphist[0]
            temphist.append(temp)
        except RuntimeError:
            del temperr[0]
            temperr.append(1)
        if sum(temperr) >= 5 * config.temp_hist_len:
            print("Temperature sensor error!")
            call(["killall", "python3"])

        avgtemp = sum(temphist) / config.temp_hist_len

        if avgtemp <= 0.9 * state['brewtemp']:
            pid.tunings = (config.pidc_kp, config.pidc_ki, config.pidc_kd)
        else:
            pid.tunings = (config.pidw_kp, config.pidw_ki, config.pidw_kd)

        if state['brewtemp'] != lastsettemp:
            pid.setpoint = state['brewtemp']
            lastsettemp = state['brewtemp']

        pidout = pid(avgtemp)
        pidhist.append(pidout)
        del pidhist[0]
        avgpid = sum(pidhist) / config.pid_hist_len

        state['i'] = i
        state['temp'] = temp
        state['pterm'], state['iterm'], state['dterm'] = pid.components
        state['avgtemp'] = round(avgtemp, 2)
        state['pidval'] = round(pidout, 2)
        state['avgpid'] = round(avgpid, 2)

        sleeptime = lasttime + config.time_sample - time()
        sleep(max(sleeptime, 0.))
        i += 1
        lasttime = time()
        with SyncCrazyflie(URI) as scf:
            # Define motion commander to crazyflie as mc:
            cf2 = CF(scf, available)
            cf2_psi = 0    # Get current yaw-angle of cf
            cf2_bat = cf2.vbat  # Get current battery level of cf
            cf2_blevel = cf2.blevel
            cf2_pwm = cf2.m1_pwm
            cf2_temp = cf2.temp
            cf2_phi = 0
            cf2_theta = 0
            marker_psi = 0  # Set marker angle to zero initially

            pid = PID(0, 0, 0, setpoint=marker_psi)  # Initialize PID
            pid_x = PID(0, 0, 0, setpoint=0)  # init PID for roll
            # Define pid parameters
            pid.tunings = (Kp_psi, Kd_psi, Ki_psi)
            pid_x.tunings = (Kp_x, Kd_x, Ki_x)
            pid_x.sample_time = 0.05    # update pid every 50 ms
            pid.output_limits = (-60, 60)
            pid_x.output_limits = (-20, 20)
            pid.proportional_on_measurment = False
            pid_x.proportional_on_measurment = False

            if cf2_bat >= low_battery:
                cf2.takeoff()    # CF takes off from ground
                print("Taking off ! Battery level: " + str(cf2_bat))
                time.sleep(1.5)   # Let the CF hover for 1.5s

            t_init = time.time()
            endTime = t_init + 30000   # Let the CF do its thing for some time
Esempio n. 3
0
def mainThread(debug=False):
    """ Controls the robot including joystick input, computer vision, line following, etc.

        Arguments:
            debug: (optional) log debugging data
    """

    DC = ControllerUtils.DriveController(flip=[1, 0, 1, 0, 0, 0, 0, 0])

    # Get Controller
    gamepad = None
    while (not gamepad) and execute['mainThread']:
        time.sleep(5)
        try:
            gamepad = ControllerUtils.identifyController()
        except Exception as e:
            print(e)

    newestImage = []
    newestSensorState = {
        'imu': {
            'calibration': {
                'sys': 2,
                'gyro': 3,
                'accel': 0,
                'mag': 0
            },
            'gyro': {
                'x': 0,
                'y': 0,
                'z': 0
            },
            'vel': {
                'x': -0.01,
                'y': 0.0,
                'z': -0.29
            }
        },
        'temp': 25
    }

    # Initalize PID controllers
    Kp = 1
    Kd = 0.1
    Ki = 0.05
    xRotPID = PID(Kp, Kd, Ki, setpoint=0)
    yRotPID = PID(Kp, Kd, Ki, setpoint=0)
    zRotPID = PID(Kp, Kd, Ki, setpoint=0)

    mode = "user-control"
    override = False
    print("mode:", mode)
    print("override:", override)

    lastMsgTime = time.time()
    minTime = 1.0 / 10.0
    while execute['mainThread']:
        while not mainQueue.empty():
            recvMsg = mainQueue.get()
            if recvMsg['tag'] == 'cam':
                #newestImage = CommunicationUtils.decodeImage(recvMsg['data'])
                pass
            elif recvMsg['tag'] == 'sensor':
                newestSensorState = recvMsg['data']

        # Get Joystick Input
        event = gamepad.read_one()
        if event:
            if (ControllerUtils.isOverrideCode(event, action="down")):
                override = True
                print("override:", override)
            elif (ControllerUtils.isOverrideCode(event, action="up")
                  and override):
                override = False
                print("override:", override)

            if (ControllerUtils.isStopCode(event)):
                handlePacket(CommunicationUtils.packet("stateChange", "close"))
                time.sleep(1)
                stopAllThreads()
            elif (ControllerUtils.isZeroMotorCode(event)):
                handlePacket(
                    CommunicationUtils.packet("motorData",
                                              DC.zeroMotors(),
                                              metadata="drivetrain"))
            elif (ControllerUtils.isStabilizeCode(event)):
                if (mode != "stabilize"):
                    # Reset PID controllers
                    xRotPID.reset()
                    yRotPID.reset()
                    zRotPID.reset()
                    xRotPID.tunings = (Kp, Ki, Kd)
                    yRotPID.tunings = (Kp, Ki, Kd)
                    zRotPID.tunings = (Kp, Ki, Kd)

                    # Assuming the robot has been correctly calibrated, (0,0,0) should be upright
                    xRotPID.setpoint = 0
                    yRotPID.setpoint = 0
                    zRotPID.setpoint = 0
                    mode = "stabilize"
                    print("mode:", mode)
                else:
                    mode = "user-control"
                    print("mode:", mode)
            if (mode == "user-control" or override):
                DC.updateState(event)
                speeds = DC.calcThrust()
                if (time.time() - lastMsgTime > minTime):
                    handlePacket(
                        CommunicationUtils.packet("motorData",
                                                  speeds,
                                                  metadata="drivetrain"))
                    lastMsgTime = time.time()
        elif (mode == "stabilize" and not override):
            xTgt = xRotPID(newestSensorState["imu"]["gyro"]["x"])
            yTgt = yRotPID(newestSensorState["imu"]["gyro"]["y"])
            zTgt = zRotPID(newestSensorState["imu"]["gyro"]["z"])
            speeds = DC.calcPIDRot(xTgt, yTgt, zTgt)
            if (time.time() - lastMsgTime > minTime):
                handlePacket(
                    CommunicationUtils.packet("motorData",
                                              speeds,
                                              metadata="drivetrain"))
                lastMsgTime = time.time()
        if (robot_heading_to_target - robot_heading
            ) < math.radians(-180):  # something might be wrong here
            robot_heading_to_target += math.radians(
                360)  # something might be wrong here
        else:
            if (robot_heading_to_target - robot_heading
                ) > math.radians(180):  # something might be wrong here
                robot_heading_to_target -= math.radians(
                    360)  # something might be wrong here

        robot_distance_to_target = distance(robot_center_position,
                                            current_target_position)

        pid_left_right.tunings = (
            0.3, 0.001, 0.01
        )  # tuning depends on the robot configuration, vision delay, etc
        if math.fabs(robot_heading_to_target -
                     robot_heading) > math.radians(5):  # I-term anti windup
            pid_left_right.Ki = 0
        pid_left_right.output_limits = (
            -0.3, 0.3
        )  # tuning depends on the robot configuration, vision delay, etc
        pid_left_right.sample_time = 0.01  # update every 0.01 seconds
        pid_left_right.setpoint = robot_heading_to_target
        ch1 = pid_left_right(robot_heading) * 100 + 100  # steering

        pid_speed.tunings = (0.01, 0.0001, 0
                             )  # depends on the robot configuration
        if robot_distance_to_target > 5:  # I-term anti windup
            pid_speed.Ki = 0
Esempio n. 5
0
    sub_thread = threading.Thread(target=controller.thread_function,
                                  daemon=True)
    sub_thread.start()

    pid = PID(controller.p, controller.i, controller.d, controller.
              desired_pitch)  #create the pid according to boat desired pitch
    pid.sample_time = 0.05
    pid.output_limits = (
        parameters.PID_PITCH_MIN_CORRECTION(),
        parameters.PID_PITCH_MAX_CORRECTION()
    )  #limit the range of action to aprox (-25,25) degrees

    while True:  #EXECUTE CONSTANTLY BUT PID ONLY ACTS IF pid.sample_time SECONDS HAVE PASSED

        try:
            pid.tunings = (controller.p, controller.i, controller.d)

            print("Current ctes: ", controller.p, "#", controller.i, "#",
                  controller.d)

            controller.pitch = controller.get_pitch_info()
            # if controller.pitch != controller.desired_pitch:
            print("Current pitch: ", controller.pitch)

            current_time = time.time()
            dt = current_time - last_time
            change = pid(controller.pitch)
            pitch = controller.send_pitch_corrections(change)
            last_time = current_time
            #plot_debugging_data(x,y,desired_value)
Esempio n. 6
0
import json
import pyvesc
from pyvesc.messages import GetValues, SetRPM, SetCurrent, SetRotorPositionMode, GetRotorPosition,SetDutyCycle,GetValues
import serial
import time
from simple_pid import PID
import time
import paho.mqtt.client as mqtt


pid = PID(1, 0.1, 0.05, setpoint=1)
pid.sample_time = 0.001  # update every 0.01 seconds
pid.setpoint = 0
normal_tuning=(700,1000,0.000000001)
#break_tuning=(100,500,0)
pid.tunings = normal_tuning #(700, 1000, 0.000000001)

pid.output_limits = (-100000, 100000)    # output value will be between 0 and 10
aca=0
motor_speed=0
motor_tach=0
force_break=0
# Set your serial port here (either /dev/ttyX or COMX)
serialport = '/dev/ttyACM2'
out_json={"MOTOR":"RIGTH","SPEED":0,"TACHOMETER":0}

def on_connect(client, userdata, flags, rc):
	print("Connected with result code {0}".format(str(rc)))  # Print result of connection attempt
	client.subscribe("actions")  # Subscribe to the topic “digitest/test1”, receive any messages published on it

Esempio n. 7
0
print("setting up Serial")
time.sleep(1)
print("Getting Data")
while noData == True:
    Serial()
print("DATA", dataList)

# PID Gubbins
LTPI, RTPI = 50, 50  # Ticks per Interval, initial setpoint
#tunings = (2, 1.0, 0.01) # Fast but permissibly erratic
#tunings = (1.1, 0.5, 0.5) # makes a bit of a waddle
#tunings = (1.3, 0.7, 0.075) # a little slower to converge but steadiest
tunings = (1.7, 0.4, 0.05)  # Default
leftMotor_PID = PID(1.0, 0.5, 0.05, setpoint=LTPI)
rightMotor_PID = PID(1.0, 0.5, 0.05, setpoint=RTPI)
leftMotor_PID.tunings = tunings
rightMotor_PID.tunings = tunings
#leftMotor_PID.sample_time = 0.01  # update every 0.01 seconds
#rightMotor_PID.sample_time = 0.01
leftMotor_PID.output_limits = (-100, 100
                               )  # output value will be between 0 and 100
rightMotor_PID.output_limits = (-100, 100)
prev_leftEnc = 0
prev_rightEnc = 0

while True:

    Pause()
    if pause == 1:
        pass
    else:
velocity = 60
bearing = 0
rotation = 0
rotationAccuracy = 1

# PID Gubbins
LTPI, RTPI = velocity, velocity # Ticks per Interval, initial setpoint
#tunings = (2, 1.0, 0.01) # Fast but permissibly erratic
#tunings = (1.1, 0.5, 0.5) # makes a bit of a waddle
#tunings = (1.3, 0.7, 0.075) # a little slower to converge but steadiest
tunings = (1.0, 0.5, 0.05) # Default
rotationalTunings = (0.2, 0.1, 0.06)
leftMotor_PID = PID(1.0, 0.5, 0.05, setpoint=LTPI)
rightMotor_PID = PID(1.0, 0.5, 0.05, setpoint=RTPI)
rotational_PID = PID(1.0, 0.5, 0.05, setpoint=0)
leftMotor_PID.tunings = tunings
rightMotor_PID.tunings = tunings
rotational_PID.tunings = rotationalTunings
#leftMotor_PID.sample_time = 0.01  # update every 0.01 seconds
#rightMotor_PID.sample_time = 0.01
#rotational_PID.sample_time = 0.01
leftMotor_PID.output_limits = (-100, 100)    # output value (Duty Cycle)
rightMotor_PID.output_limits = (-100, 100)
rotational_PID.output_limits = (-100, 100)

# Odometry variables
prev_leftEncF, prev_leftEncB = 0, 0
prev_rightEncF, prev_rightEncB = 0, 0

#Rotational PID will need some odoemtry in order to work out rotation
travel, TotalTravel, thetaRad, theta = 0.0, 0.0, 0.0, 0.0
Esempio n. 9
0
        accelZ = accel_data['z']
        gyroX = gyro_data['x']
        gyroY = gyro_data['y']
        gyroZ = gyro_data['z']
        gyroX -= gyro_offset_x
        gyroY -= gyro_offset_y
        gyro_x_delta = (gyroX * time_diff)
        gyro_y_delta = (gyroY * time_diff)
        gyro_total_x += gyro_x_delta
        gyro_total_y += gyro_y_delta
        rotation_x = x_rotation(accelX, accelY, accelZ)
        rotation_y = y_rotation(accelX, accelY, accelZ)
        last_x = K * (last_x + gyro_x_delta) + (K1 * rotation_x)
        inputSudut = round(last_x, decimalDigitSudut) + round(calibrationValue, decimalDigitSudut)
        inputSudut = round(inputSudut, decimalDigit)
        pid.tunings = (P, I, D)
        PIDx = pid(inputSudut)
#         output = computePID(inputSudut)
#         PIDx = output
        if inputSudut == batas:
            equilibrium()
#             print("berhenti")
        elif PIDx < 0.0:
            backward(-int(PIDx))
#             print("back")
            #StepperFor(-PIDx)
        #if the PIDx data is higher than 0.0 than move appropriately forward
        elif PIDx > 0.0:
            forward(int(PIDx))
#             print("forw")
            #StepperBACK(PIDx)
Esempio n. 10
0
import time
from subprocess import call
from simple_pid import PID
mutex = Lock()
mutex_pid = Lock()

logging.basicConfig(level=logging.DEBUG)

vazao_in = parametros['start_vazao_in']
vazao_out = parametros['start_vazao_out']
nivel_ref = parametros['start_nivel_ref']  # setpoint
nivel_atual = parametros['start_nivel_atual']

pid = PID(1, 0.1, 0.05, setpoint=nivel_ref)
pid.output_limits = (0, 10)
pid.tunings = (1.0, 0.2, 0.4)


class ProcessThread(Thread):
    def __init__(self, cv, raio_inf, raio_sup, altura):
        Thread.__init__(self)
        self.cv = (
            cv
        )  # Coeficiente de descarga do tanque -> relacionado com o tamanho do furo no tanque
        self.raio_inf = raio_inf  # Raio inferior do tanque
        self.raio_sup = raio_sup  # Raio superior do tanque
        self.altura_tanque = altura
        self.alfa = (raio_sup - raio_inf) / altura

    @staticmethod
    def dhdt(self, t, h, u):
Esempio n. 11
0

def read_settings(fn):
    with open(fn) as f:
        params = json.load(f)
    return params  # (tunings["Kp"], tunings["Ki"], tunings["Kd"])


if __name__ == '__main__':

    log = tl.LogFile()

    settings_fn = "settings.json"
    settings = read_settings(settings_fn)
    settings_1 = settings["PID_1"]
    settings_2 = settings["PID_2"]

    pid_tunings = (settings_1["Kp"], settings_1["Ki"], settings_1["Kd"])

    pid = PID()
    pid.sample_time = updatePIDInt  # /100 # settings["sample_time"]
    pid.output_limits = (0, 100)
    pid.tunings = pid_tunings
    pid.setpoint = 25.0
    pid.auto_mode = True
    pid.proportional_on_measurement = False

    print(pid.tunings)

    HomebrewApp().run()
Esempio n. 12
0
def main():
    pygame.init()
    pygame.font.init()
    screen = pygame.display.set_mode((width, height))
    clock = pygame.time.Clock()

    tunings = (27, 2, 10)
    pid = PID(*tunings,
              output_limits=(-8000, 8000),
              proportional_on_measurement=True)
    pid.sample_time = 1 / REFRESH

    draw_options = DrawOptions(screen)
    pymunk.pygame_util.positive_y_is_up = True

    font = pygame.font.SysFont('Lucida Console', 11)

    space = pymunk.Space()
    space.gravity = 0, -100
    x = random.randint(120, 380)
    drone = Drone((x, 450), space)
    ground = Ground(space)

    setpoint = 450
    ticks = 0
    while True:
        for event in pygame.event.get():
            # print(event)
            if event.type == pygame.QUIT:
                sys.exit(0)
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_UP:
                    setpoint += 5
                elif event.key == pygame.K_DOWN:
                    setpoint -= 5
                elif event.key == pygame.K_ESCAPE:
                    sys.exit(0)
                elif event.key == pygame.K_1:
                    p, i, d = pid.tunings
                    pid.tunings = (p + 0.5, i, d)
                elif event.key == pygame.K_q:
                    p, i, d = pid.tunings
                    pid.tunings = (p - 0.5, i, d)
                elif event.key == pygame.K_2:
                    p, i, d = pid.tunings
                    pid.tunings = (p, i + 0.5, d)
                elif event.key == pygame.K_w:
                    p, i, d = pid.tunings
                    pid.tunings = (p, i - 0.5, d)
                elif event.key == pygame.K_3:
                    p, i, d = pid.tunings
                    pid.tunings = (p, i, d + 0.5)
                elif event.key == pygame.K_e:
                    p, i, d = pid.tunings
                    pid.tunings = (p, i, d - 0.5)
            elif event.type == pygame.MOUSEBUTTONUP:
                setpoint = 600 - event.pos[1]
                # ticks=0

        screen.fill((0, 0, 0))

        error = drone.body.position.y - setpoint
        output = pid(error)

        space.debug_draw(draw_options)

        # if drone.shape.body.position.y < setpoint:
        if True:  # output > 0:
            # drone.shape.body.apply_force_at_local_point((0, 340), (0, 0))
            drone.shape.body.apply_force_at_local_point((0, output), (0, 0))
            c = to_pygame(drone.body.position, screen)
            pygame.draw.aaline(screen, (0, 128, 255, .75), c,
                               (c[0], c[1] - output // 10))

        pygame.draw.aaline(screen, (128, 255, 128, .125),
                           to_pygame((50, setpoint), screen),
                           to_pygame((600 - 50, setpoint), screen))

        # pygame.draw.aaline(screen, (255, 255, 255, .5), to_pygame((0,0), screen), to_pygame((100,100), screen))

        textsurface = font.render(
            f"pos:{drone.body.position.y:7.1f} set:{setpoint} error:{error:7.1f} o:{output:7.1f}",
            False, WHITE)
        screen.blit(textsurface, (10, 10))
        textsurface = font.render(
            f"pid:{['{:7.1f}'.format(p) for p in pid.components]}", False,
            WHITE)
        screen.blit(textsurface, (10, 25))
        textsurface = font.render(
            f"tun:{['{:7.1f}'.format(p) for p in pid.tunings]}", False, WHITE)
        screen.blit(textsurface, (10, 40))
        textsurface = font.render(f"ticks:{ticks}", False, WHITE)
        screen.blit(textsurface, (10, 55))

        space.step(1 / REFRESH)
        pygame.display.update()
        clock.tick(REFRESH)

        pygame.display.set_caption(f"{drone.body.position.y:.1f} {setpoint}")

        ticks += 1
    gray = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)

    dst = cv2.filter2D(gray, -1, kernel)
    faces = face_cascade.detectMultiScale(dst, 1.3, 5)

    for (x, y, w, h) in faces:
        cv2.rectangle(frame1, (x, y), (x + w, y + h), (0, 255, 0), 1)
        centerFace = (int(x + w / 2), int(y + h / 2))
        cv2.circle(frame1, centerFace, 5, (0, 255, 0), 1)
        roi_gray = gray[y:y + h, x:x + w]
        roi_color = frame1[y:y + h, x:x + w]
        faceDetected = True

    if faceDetected:
        pidFaceLR.tunings = (0.5, 0.000, 0.001)
        pidFaceLR.output_limits = (-0.5, 0.5)
        pidFaceLR.sample_time = 0.01  # update every 0.01 seconds
        pidFaceLR.setpoint = 0
        ch1 = pidFaceLR(
            (centerFace[0] - frame1Center[0]) / 100) * 100 + 100  # left right

        pidFaceUD.tunings = (0.5, 0.000, 0.001)
        pidFaceUD.output_limits = (-1, 1)
        pidFaceUD.sample_time = 0.01  # update every 0.01 seconds
        pidFaceUD.setpoint = 0
        ch4 = pidFaceUD(
            (centerFace[1] - frame1Center[1]) / 100) * 100 + 100  # up down

        if 110 < w < 300:
            ch2 = 80
Esempio n. 14
0
            #PID_Parameters[0] = cv2.getTrackbarPos('kP', 'CV PID Settings')
            #PID_Parameters[1]= cv2.getTrackbarPos('ki', 'CV PID Settings')
            #PID_Parameters[2]	= cv2.getTrackbarPos('kd', 'CV PID Settings')
    else:  #Not Found
        angle = 0
        pidout = 0
        speed = 0
        dir = 0
    fps.update()
    fps.stop()
    print(
        "Angle(deg): {:+.2f}, PID: Kp{}, Ki{:.3f}, Kd{:.3f}, PIDOut{:+3.3f}, Speed_Command: {:+5}, Direction: {}, fps:{:.2f} , time:{:.2f}\r"
        .format(angle, pid.Kp, pid.Ki, pid.Kd, pidout, speed, dir, fps.fps(),
                fps.elapsed()),
        end="")
    pid.tunings = (PID_Parameters[0], PID_Parameters[1], PID_Parameters[2])
    cv2.imshow("CV PID Pendulum", frame)

    key = cv2.waitKey(1) & 0xFF

    # if the 'q' key is pressed, stop the loop
    if key == ord("q"):
        s.write("0\n".encode())
        s.flush()
        s.close()
        f.close()
        fps.stop()
        break
    if key == ord("s") and enable == False:
        f = open("data.txt", "w")
        f.write("angle,pidout,speed,fps,elapsed\n")
Esempio n. 15
0
import pyvesc
from pyvesc.messages import GetValues, SetRPM, SetCurrent, SetRotorPositionMode, GetRotorPosition, SetDutyCycle, GetValues
import serial
import time
from simple_pid import PID
import time

# Set your serial port here (either /dev/ttyX or COMX)
serialport = '/dev/ttyACM1'

pid = PID(1, 0.1, 0.05, setpoint=1)
pid.sample_time = 0.001  # update every 0.01 seconds
pid.setpoint = 10
pid.tunings = (700, 2000, 0.000000001)
pid.output_limits = (-100000, 100000)  # output value will be between 0 and 10
print("la concha de tu puta madre")


def get_values_example():
    print("running")
    input_buffer = b""
    output = 0
    t = 0
    old_tach = 0
    with serial.Serial(serialport, baudrate=115200, timeout=0.05) as ser:
        try:
            # Optional: Turn on rotor position reading if an encoder is installed
            ser.write(
                pyvesc.encode(
                    SetRotorPositionMode(
                        SetRotorPositionMode.DISP_POS_MODE_ENCODER)))
Esempio n. 16
0
from simple_pid import PID

pid = PID(1, 0.1, 0.05, setpoint=1)
pid.sample_time = 0.01
pid.setpoint = 10
pid.tunings = (1.0, 0.2, 0.4)  #ki,Ti,Td
pid.output_limits = (0, 10)

while True:

    control = pid(
        9
    )  # The PID class implements __call__(), which means that to compute a new output value