Skip to content

Commit

Permalink
Added support for the secondary PID controller
Browse files Browse the repository at this point in the history
  • Loading branch information
PatrickBaus committed Sep 19, 2022
1 parent bc2f085 commit 7031852
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ keywords = ["IoT", "PID", "PID controller",]
dependencies = [
"typing-extensions; python_version <'3.11'",
"aiostream ~= 0.4.4",
"labnode_async ~= 0.15.2",
"labnode_async ~= 0.16.0",
"python-decouple ~= 3.5",
"tinkerforge_async ~= 1.4.1",
]
Expand Down
60 changes: 46 additions & 14 deletions temperature_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,24 @@
from _version import __version__


class PidConfig(TypedDict):
"""The PID controller configuration as read from the os environment."""
class PidParameters(TypedDict):
"""The PID controller parameters for each of the integrated PID controllers."""

kp: float
ki: float
kd: float
setpoint: float


class PidConfig(TypedDict):
"""The PID controller configuration as read from the os environment."""

primary_pid: PidParameters
secondary_pid: PidParameters
timeout: float
enable_gain: bool
enable_autoresume: bool
update_interval: float # in s


class Controller:
Expand Down Expand Up @@ -126,13 +135,15 @@ async def labnode_consumer(
controller.set_lower_output_limit(0),
controller.set_upper_output_limit(0xFFF), # 12-bit DAC
controller.set_dac_gain(pid_config["enable_gain"]), # Enable 10 V output (Gain x2)
controller.set_timeout(int(pid_config["timeout"] * 1000)), # time in ms
controller.set_timeout(pid_config["timeout"]), # time in seconds
controller.set_pid_feedback_direction(FeedbackDirection.NEGATIVE),
controller.set_auto_resume(pid_config["enable_autoresume"]),
controller.set_fallback_update_interval(pid_config["update_interval"]), # time in seconds
# Those values need some explanation:
# The target is an unsigned Qm.n 32-bit number, which is positive (>=0) See this link
# for details: https://en.wikipedia.org/wiki/Q_%28number_format%29
# To determine m and n, we need to look at the desired output. The output is a 12 bit DAC.
# The pid basically does input * kp -> output, which should be a 12 bit number (Q12.20)
# The PID basically does input * kp -> output, which should be a 12 bit number (Q12.20)
# The source sensor (Tinkerforge) has a temperature range of -40 °C to 125 °C
# (Temperature Bricklet v1), so this makes 165 K, we normalize this to 1 (/165).
# The input sensor (e.g. STS3x, Temperature Bricklet v2) has 16 bits of resolution (/2**16)
Expand All @@ -143,12 +154,22 @@ async def labnode_consumer(
# kp: dac_bit_values / K * 165 / 2**16 (adc_bit_values / K) in Q12.20 notation
# ki: dac_bit_values / (K s) * 165 / 2**16 (adc_bit_values / K) in Q12.20 notation
# kd: dac_bit_values * s / K * 165 / 2**16 (adc_bit_values / K) in Q12.20 notation
controller.set_kp(pid_config["kp"] * 165 / 2**16 * 2**20),
controller.set_ki(pid_config["ki"] * 165 / 2**16 * 2**20),
controller.set_kd(pid_config["kd"] * 165 / 2**16 * 2**20),
controller.set_kp(pid_config["primary_pid"]["kp"] * 165 / 2**16 * 2**20, config_id=0),
controller.set_ki(pid_config["primary_pid"]["ki"] * 165 / 2**16 * 2**20, config_id=0),
controller.set_kd(pid_config["primary_pid"]["kd"] * 165 / 2**16 * 2**20, config_id=0),
# To ensure the input is always positive, we add 40 K
controller.set_setpoint(max(int((float(pid_config["setpoint"]) + 40) / 165 * 2**16), 0)),
controller.set_setpoint(
max(int((pid_config["primary_pid"]["setpoint"] + 40) / 165 * 2**16), 0), config_id=0
),
# And the configuration for the secondary PID
controller.set_kp(pid_config["secondary_pid"]["kp"] * 165 / 2**16 * 2**20, config_id=1),
controller.set_ki(pid_config["secondary_pid"]["ki"] * 165 / 2**16 * 2**20, config_id=1),
controller.set_kd(pid_config["secondary_pid"]["kd"] * 165 / 2**16 * 2**20, config_id=1),
controller.set_setpoint(
max(int((pid_config["secondary_pid"]["setpoint"] + 40) / 165 * 2**16), 0), config_id=1
),
controller.set_enabled(True),
controller.set_secondary_config(1), # Set the secondary PID controller to use config #1
)
# Now pull the data from the input queue and feed it to the PID controller
data_stream = stream.call(input_queue.get) | pipe.cycle()
Expand Down Expand Up @@ -179,20 +200,29 @@ async def run(self) -> None: # pylint: disable=too-many-locals
sensor_host = config("SENSOR_IP")
sensor_port = config("SENSOR_PORT", cast=int, default=4223)
sensor_uid = config("SENSOR_UID")
interval = config("PID_INTERVAL", cast=float)
try:
sensor_uid = int(sensor_uid)
except ValueError:
# We need to convert the value from base58 encoding to an integer
sensor_uid = base58decode(sensor_uid)

pid_config: PidConfig = {
"kp": config("PID_KP", cast=float),
"ki": config("PID_KI", cast=float),
"kd": config("PID_KD", cast=float),
"setpoint": config("PID_SETPOINT", cast=float),
"primary_pid": {
"kp": config("PRIMARY_PID_KP", cast=float),
"ki": config("PRIMARY_PID_KI", cast=float),
"kd": config("PRIMARY_PID_KD", cast=float),
"setpoint": config("PRIMARY_PID_SETPOINT", cast=float),
},
"secondary_pid": {
"kp": config("SECONDARY_PID_KP", cast=float),
"ki": config("SECONDARY_PID_KI", cast=float),
"kd": config("SECONDARY_PID_KD", cast=float),
"setpoint": config("SECONDARY_PID_SETPOINT", cast=float),
},
"update_interval": config("PID_INTERVAL", cast=float),
"timeout": config("PID_TIMEOUT", cast=float),
"enable_gain": config("OUTPUT_ENABLE_GAIN", cast=bool, default=True),
"enable_autoresume": config("AUTO_RESUME", cast=bool, default=True),
}
except UndefinedValueError as exc:
self.__logger.error("Environment variable undefined: %s", exc)
Expand All @@ -217,7 +247,9 @@ async def run(self) -> None: # pylint: disable=too-many-locals
self.__logger.info("Connecting producer to Tinkerforge brick at '%s:%i", sensor_host, sensor_port)
ipcon = await stack.enter_async_context(IPConnectionAsync(host=sensor_host, port=sensor_port))
self.__logger.info("Connected to Tinkerforge brick at '%s:%i", sensor_host, sensor_port)
task = asyncio.create_task(self.tinkerforge_producer(ipcon, sensor_uid, interval, message_queue))
task = asyncio.create_task(
self.tinkerforge_producer(ipcon, sensor_uid, pid_config["update_interval"], message_queue)
)
tasks.add(task)

await asyncio.gather(*tasks)
Expand Down

0 comments on commit 7031852

Please sign in to comment.