I've found the following a good way to visualize what a PID controller does, because it uses a situation that is easy to do a mental simulation of.

Imagine you have a straight line painted on the ground in a large open area, and you are trying to program a self-driving car that moves at constant speed to follow the line.

Your only input is how far to the side you are from the line. This is a signed number, with negative meaning you are to the left of the line and positive meaning you are to the right.

Your only output is the angle to set the steering wheel to (in degrees). Positive angles turn to the right, negative to the left.

The car starts out on the line and pointed in the right direction, with the steering wheel at 0.

So how would you keep the car on the line?

Some possible problems you have to deal with: the line might not be perfectly straight--it could wobble a little or occasionally slightly turn; there might be an occasional gust of wind the knocks you off course; the wheels may be slightly out of alignment so that the car pulls to one side, so setting the wheel to 0 doesn't necessarily mean you will go straight.

The simplest thing that comes to mind is probably something like this, where "err" is how far off you are from the line, and "wheel" is the angle to set the wheel to:

if err < 0:
wheel = 15
elif err > 0:
wheel = -15
else
wheel = 0

As soon as that gets off the line for whatever reason, it ends up oscillating around the line in circular arcs that alternate direction.

You can reduce the magnitude and frequency of oscillation by using a smaller steering angle, but then it takes you longer to get back to the line when you get off.

We can address taking too long to get back to the line by replacing a constant steering angle with one that is small when we are close to the line and big when farther away.

Let's make it proportional, so now our code is this:

wheel = -err * Kp

where Kp is a non-negative constant. In other words, we've made our steering proportional to the error. "Proportional" is the "P" in PID.

Now when we get a big error, we respond big, so that does deal with the problem of taking a long time to correct a big error, but that introduces a couple new problems.

First, when we reach the line we will be moving at a steep angle. We'll cross to the other side and rapidly accumulate error in the other direction. We'll strongly correct that, and so on, so we still have oscillation--and it is no longer shallow low frequency oscillation. It is now big and high frequency. Oops.

Second, if we somehow get a big error, we could end up setting the steering angle high enough that we actually turn around enough to before reaching the line that we end up going the wrong way. Double oops!

We can address this by taking into account how rapidly we are approaching the line. If we are approaching rapidly, we can back off on the wheel.

We can do this by figuring out the rate of change of the error. In mathematical terms, we want the derivative of the error function.

We can approximate that quite well by keeping track of past samples of the error. If we sample at times t1 and t2, and get e1 and e2, the derivative is approximately (e2-e1)/(t2-t1).

So let's say we have the current derivative in variable D. Our code for updating the wheel after each sample is now:

wheel = -err * Kp - D * Kd

where Kd is a non-negative constant. "Derivative" is "D" in PID.

Note that when we are to the right (positive error) and approaching the line, error is decreasing so D is negative, so the D term will be trying to lessen the correction from the P term.

If we fiddle with Kp and Kd we hopefully can find some values so that as we get close to the line, the D term gets large enough to completely overcome the P term and more, so that instead of overshooting and oscillating we smoothly rejoin the line.

If our steering is not aligned perfectly, so that wheel at 0 does not result in us going straight, the above PD controller does not end up following the line. The best you get is that it follows a straight line but displaced to the side from the real line.

To fix that, we can keep track of the net error over time. In math terms, we want the integral of the error. This can be approximated by just keeping a running sum of all error samples (if we sample at a constant rate...if sampling at a variable rate you need to weigh them by the time since the prior sample). Let's say we have this available in a variable I. Incorporate this:

wheel = -err * Kp - D * Kd - I * Ki

where Ki is a non-negative constant. If our steering is biased to the right, that will tend to make I positive, so the I term will move the wheel to the left to counter the bias. "Integral" is the "I" in PID.

So now we have a PID controller:

while True:
err = read_sensor()
D = update_derivative(err)
I = update_integral(err)
set_wheel(-err * Kp - D * Kd - I * Ki)

where update_derivative and update_integral are functions that take the current sample and based on it and past history estimate the current derivative and integral of the error function.

If the sensor data becomes available at a fixed rate, and read_sensor() blocks until the next reading is available, then you can approximate the derivative simply by the difference between the current reading and the last reading, giving:

last_err = 0
I = 0
while True:
err = read_sensor()
I += err
D = err - last_err
last_err = err
set_wheel(-err * Kp - D * Kd - I * Ki)

Normally you approximate a derivative by dividing the difference in readings by the time between the readings, but with the sensor being read at a fixed rate, the time difference is constant and so it can be rolled into Kd. Similar with Ki for the integral.