Compare commits

..

10 Commits

Author SHA1 Message Date
cc90a88276 README: Introduce Thermostat GUI
Co-authored-by: topquark12 <aw@m-labs.hk>
2024-11-04 18:27:30 +08:00
4618543916 pytec GUI: Set up packaging
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:26 +08:00
20cd69fff7 pytec GUI: Implement Control Panel
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:22 +08:00
e045b0288a pytec GUI: Implement PlotSettingsMenu
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:27:18 +08:00
ae244b66d1 pytec GUI: Implement plotting
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:27:14 +08:00
c77e6f73f1 pytec GUI: Incorporate autotuning
Co-authored-by: topquark12 <aw@m-labs.hk>
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:10 +08:00
0de3c9452e pytec GUI: Implement ThermostatSettingsMenu
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:27:06 +08:00
c95db11b62 pytec GUI: Implement status line
Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:26:54 +08:00
239b8d8791 pytec: Create GUI to Thermostat
- Add connection menu

- Add basic GUI layout skeleton

Co-authored-by: linuswck <linuswck@m-labs.hk>
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:26:01 +08:00
1d1500cc0f pytec: Create asyncio clients 2024-11-04 18:16:03 +08:00
7 changed files with 377 additions and 464 deletions

View File

@ -7,17 +7,9 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = outputs = { self, nixpkgs, rust-overlay }:
{
self,
nixpkgs,
rust-overlay,
}:
let let
pkgs = import nixpkgs { pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; };
system = "x86_64-linux";
overlays = [ (import rust-overlay) ];
};
rust = pkgs.rust-bin.stable."1.66.0".default.override { rust = pkgs.rust-bin.stable."1.66.0".default.override {
extensions = [ "rust-src" ]; extensions = [ "rust-src" ];
@ -94,8 +86,7 @@
wrapQtApp "$out/bin/tec_qt" wrapQtApp "$out/bin/tec_qt"
''; '';
}; };
in in {
{
packages.x86_64-linux = { packages.x86_64-linux = {
inherit thermostat thermostat_gui; inherit thermostat thermostat_gui;
default = thermostat; default = thermostat;
@ -112,26 +103,17 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = packages = with pkgs; [
with pkgs; rust llvm
[ openocd dfu-util rlwrap
rust qtcreator
llvm ] ++ (with python3Packages; [
openocd numpy matplotlib
dfu-util
rlwrap
qtcreator
]
++ (with python3Packages; [
numpy
matplotlib
pyqtgraph pyqtgraph
pyqt6 pyqt6
qasync qasync
pglive pglive
]); ]);
}; };
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style;
}; };
} }

View File

@ -12,10 +12,6 @@ class Client:
self._lines = [""] self._lines = [""]
self._check_zero_limits() self._check_zero_limits()
def disconnect(self):
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()
def _check_zero_limits(self): def _check_zero_limits(self):
output_report = self.get_output() output_report = self.get_output()
for output_channel in output_report: for output_channel in output_report:
@ -114,39 +110,6 @@ class Client:
""" """
return self._get_conf("postfilter") return self._get_conf("postfilter")
def get_report(self):
"""Obtain one-time report on measurement values
Example of yielded data::
{'channel': 0,
'time': 2302524,
'interval': 0.12
'adc': 0.6199188965423515,
'sens': 6138.519310282602,
'temperature': 36.87032392655527,
'pid_engaged': True,
'i_set': 2.0635816680889123,
'dac_value': 2.527790834044456,
'dac_feedback': 2.523,
'i_tec': 2.331,
'tec_i': 2.0925,
'tec_u_meas': 2.5340000000000003,
'pid_output': 2.067581958092247}
"""
return self._get_conf("report")
def get_ipv4(self):
"""Get the IPv4 settings of the Thermostat"""
return self._command("ipv4")
def get_fan(self):
"""Get Thermostat current fan settings"""
return self._command("fan")
def get_hwrev(self):
"""Get Thermostat hardware revision"""
return self._command("hwrev")
def report_mode(self): def report_mode(self):
"""Start reporting measurement values """Start reporting measurement values
@ -200,38 +163,10 @@ class Client:
self.set_param("pid", channel, "target", value=target) self.set_param("pid", channel, "target", value=target)
self.set_param("output", channel, "pid") self.set_param("output", channel, "pid")
def save_config(self, channel=""): def save_config(self):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""
self._command("save", channel) self._command("save")
if channel != "":
self._read_line() # read the extra {}
def load_config(self, channel=""): def load_config(self):
"""Load current configuration from EEPROM""" """Load current configuration from EEPROM"""
self._command("load", channel) self._command("load")
if channel != "":
self._read_line() # read the extra {}
def reset(self):
"""Reset the device"""
self._socket.sendall("reset".encode("utf-8"))
self.disconnect() # resetting ends the TCP session, disconnect anyway
def enter_dfu_mode(self):
"""Reset device and enters USB device firmware update (DFU) mode"""
self._socket.sendall("dfu".encode("utf-8"))
self.disconnect() # resetting ends the TCP session, disconnect anyway
def set_ipv4(self, address, netmask, gateway=""):
"""Configure IPv4 address, netmask length, and optional default gateway"""
self._command("ipv4", f"{address}/{netmask}", gateway)
def set_fan(self, power=None):
"""Set fan power with values from 1 to 100. If omitted, set according to fcurve"""
if power is None:
power = "auto"
self._command("fan", power)
def set_fcurve(self, a=1.0, b=0.0, c=0.0):
"""Set fan controller curve coefficients"""
self._command("fcurve", a, b, c)

View File

@ -40,7 +40,7 @@ class PIDAutoTuner(QObject):
self.autotuners[ch].setOff() self.autotuners[ch].setOff()
self.autotune_state_changed.emit(ch, self.autotuners[ch].state()) self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
if self._thermostat.connected(): if self._thermostat.connected():
await self._thermostat.set_param("output", ch, "i_set", 0) await self._thermostat.set_param("pwm", ch, "i_set", 0)
@asyncSlot(list) @asyncSlot(list)
async def tick(self, report): async def tick(self, report):
@ -63,7 +63,7 @@ class PIDAutoTuner(QObject):
channel_report["temperature"], channel_report["time"] channel_report["temperature"], channel_report["time"]
) )
await self._thermostat.set_param( await self._thermostat.set_param(
"output", ch, "i_set", self.autotuners[ch].output() "pwm", ch, "i_set", self.autotuners[ch].output()
) )
case PIDAutotuneState.STATE_SUCCEEDED: case PIDAutotuneState.STATE_SUCCEEDED:
kp, ki, kd = self.autotuners[ch].get_tec_pid() kp, ki, kd = self.autotuners[ch].get_tec_pid()
@ -73,7 +73,7 @@ class PIDAutoTuner(QObject):
await self._thermostat.set_param("pid", ch, "kp", kp) await self._thermostat.set_param("pid", ch, "kp", kp)
await self._thermostat.set_param("pid", ch, "ki", ki) await self._thermostat.set_param("pid", ch, "ki", ki)
await self._thermostat.set_param("pid", ch, "kd", kd) await self._thermostat.set_param("pid", ch, "kd", kd)
await self._thermostat.set_param("output", ch, "pid") await self._thermostat.set_param("pwm", ch, "pid")
await self._thermostat.set_param( await self._thermostat.set_param(
"pid", ch, "target", self.target_temp[ch] "pid", ch, "target", self.target_temp[ch]
@ -81,4 +81,4 @@ class PIDAutoTuner(QObject):
case PIDAutotuneState.STATE_FAILED: case PIDAutotuneState.STATE_FAILED:
self.autotuners[ch].setOff() self.autotuners[ch].setOff()
self.autotune_state_changed.emit(ch, self.autotuners[ch].state()) self.autotune_state_changed.emit(ch, self.autotuners[ch].state())
await self._thermostat.set_param("output", ch, "i_set", 0) await self._thermostat.set_param("pwm", ch, "i_set", 0)

View File

@ -19,7 +19,7 @@ class Thermostat(QObject, metaclass=PropertyMeta):
fan = Property(dict) fan = Property(dict)
thermistor = Property(list) thermistor = Property(list)
pid = Property(list) pid = Property(list)
output = Property(list) pwm = Property(list)
postfilter = Property(list) postfilter = Property(list)
report = Property(list) report = Property(list)
@ -82,20 +82,15 @@ class Thermostat(QObject, metaclass=PropertyMeta):
await asyncio.sleep(self._update_s) await asyncio.sleep(self._update_s)
async def update_params(self): async def update_params(self):
( self.fan, self.pwm, self.report, self.pid, self.thermistor, self.postfilter = (
self.fan, await asyncio.gather(
self.output, self._client.get_fan(),
self.report, self._client.get_output(),
self.pid, self._client.report(),
self.thermistor, self._client.get_pid(),
self.postfilter, self._client.get_b_parameter(),
) = await asyncio.gather( self._client.get_postfilter(),
self._client.get_fan(), )
self._client.get_output(),
self._client.report(),
self._client.get_pid(),
self._client.get_b_parameter(),
self._client.get_postfilter(),
) )
def connected(self): def connected(self):

View File

@ -97,7 +97,7 @@ class CtrlPanel(QObject):
self.thermostat.pid_update.connect(self.update_pid) self.thermostat.pid_update.connect(self.update_pid)
self.thermostat.report_update.connect(self.update_report) self.thermostat.report_update.connect(self.update_report)
self.thermostat.thermistor_update.connect(self.update_thermistor) self.thermostat.thermistor_update.connect(self.update_thermistor)
self.thermostat.output_update.connect(self.update_output) self.thermostat.pwm_update.connect(self.update_pwm)
self.thermostat.postfilter_update.connect(self.update_postfilter) self.thermostat.postfilter_update.connect(self.update_postfilter)
self.autotuners.autotune_state_changed.connect(self.update_pid_autotune) self.autotuners.autotune_state_changed.connect(self.update_pid_autotune)
@ -225,19 +225,19 @@ class CtrlPanel(QObject):
) )
@pyqtSlot(list) @pyqtSlot(list)
def update_output(self, output_data): def update_pwm(self, pwm_data):
for output_params in output_data: for pwm_params in pwm_data:
channel = output_params["channel"] channel = pwm_params["channel"]
with QSignalBlocker(self.params[channel]): with QSignalBlocker(self.params[channel]):
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Voltage Difference" "Output Config", "Limits", "Max Voltage Difference"
).setValue(output_params["max_v"]) ).setValue(pwm_params["max_v"])
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Cooling Current" "Output Config", "Limits", "Max Cooling Current"
).setValue(output_params["max_i_pos"] * 1000) ).setValue(pwm_params["max_i_pos"] * 1000)
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Heating Current" "Output Config", "Limits", "Max Heating Current"
).setValue(output_params["max_i_neg"] * 1000) ).setValue(pwm_params["max_i_neg"] * 1000)
@pyqtSlot(list) @pyqtSlot(list)
def update_postfilter(self, postfilter_data): def update_postfilter(self, postfilter_data):
@ -304,3 +304,4 @@ class CtrlPanel(QObject):
| PIDAutotuneState.STATE_RELAY_STEP_DOWN | PIDAutotuneState.STATE_RELAY_STEP_DOWN
): ):
await self.autotuners.stop_pid_from_running(ch) await self.autotuners.stop_pid_from_running(ch)

View File

@ -1,336 +1,336 @@
{ {
"ctrl_panel": [ "ctrl_panel":[
{ {
"name": "Temperature", "name":"Temperature",
"type": "float", "type":"float",
"format": "{value:.4f} °C", "format":"{value:.4f} °C",
"readonly": true "readonly":true
}, },
{ {
"name": "Current through TEC", "name":"Current through TEC",
"type": "float", "type":"float",
"suffix": "mA", "suffix":"mA",
"decimals": 6, "decimals":6,
"readonly": true "readonly":true
}, },
{ {
"name": "Output Config", "name":"Output Config",
"expanded": true, "expanded":true,
"type": "group", "type":"group",
"children": [ "children":[
{ {
"name": "Control Method", "name":"Control Method",
"type": "mutex", "type":"mutex",
"limits": [ "limits":[
"Constant Current", "Constant Current",
"Temperature PID" "Temperature PID"
], ],
"value": "Constant Current", "value": "Constant Current",
"thermostat:set_param": { "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field": "pid" "field":"pid"
}, },
"children": [ "children":[
{ {
"name": "Set Current", "name":"Set Current",
"type": "float", "type":"float",
"value": 0, "value":0,
"step": 100, "step":100,
"limits": [ "limits":[
-2000, -2000,
2000 2000
], ],
"triggerOnShow": true, "triggerOnShow":true,
"decimals": 6, "decimals":6,
"suffix": "mA", "suffix":"mA",
"thermostat:set_param": { "thermostat:set_param":{
"topic": "output", "topic":"pwm",
"field": "i_set" "field":"i_set"
}, },
"lock": false "lock":false
},
{
"name":"Set Temperature",
"type":"float",
"value":25,
"step":0.1,
"limits":[
-273,
300
],
"format":"{value:.4f} °C",
"thermostat:set_param":{
"topic":"pid",
"field":"target"
},
"lock":false
}
]
},
{
"name":"Limits",
"expanded":true,
"type":"group",
"children":[
{
"name":"Max Cooling Current",
"type":"float",
"value":0,
"step":100,
"decimals":6,
"limits":[
0,
2000
],
"suffix":"mA",
"thermostat:set_param":{
"topic":"pwm",
"field":"max_i_pos"
},
"lock":false
},
{
"name":"Max Heating Current",
"type":"float",
"value":0,
"step":100,
"decimals":6,
"limits":[
0,
2000
],
"suffix":"mA",
"thermostat:set_param":{
"topic":"pwm",
"field":"max_i_neg"
},
"lock":false
},
{
"name":"Max Voltage Difference",
"type":"float",
"value":0,
"step":0.1,
"limits":[
0,
5
],
"siPrefix":true,
"suffix":"V",
"thermostat:set_param":{
"topic":"pwm",
"field":"max_v"
},
"lock":false
}
]
}
]
},
{
"name":"Thermistor Config",
"expanded":true,
"type":"group",
"children":[
{
"name":"T₀",
"type":"float",
"value":25,
"step":0.1,
"limits":[
-100,
100
],
"format":"{value:.4f} °C",
"thermostat:set_param":{
"topic":"s-h",
"field":"t0"
},
"lock":false
},
{
"name":"R₀",
"type":"float",
"value":10000,
"step":1,
"siPrefix":true,
"suffix":"Ω",
"thermostat:set_param":{
"topic":"s-h",
"field":"r0"
},
"lock":false
},
{
"name":"B",
"type":"float",
"value":3950,
"step":1,
"suffix":"K",
"decimals":4,
"thermostat:set_param":{
"topic":"s-h",
"field":"b"
},
"lock":false
},
{
"name":"Postfilter Rate",
"type":"list",
"value":16.67,
"thermostat:set_param":{
"topic":"postfilter",
"field":"rate"
},
"limits":{
"Off":null,
"16.67 Hz":16.67,
"20 Hz":20.0,
"21.25 Hz":21.25,
"27 Hz":27.0
},
"lock":false
}
]
},
{
"name":"PID Config",
"expanded":true,
"type":"group",
"children":[
{
"name":"Kp",
"type":"float",
"step":0.1,
"suffix":"",
"thermostat:set_param":{
"topic":"pid",
"field":"kp"
},
"lock":false
},
{
"name":"Ki",
"type":"float",
"step":0.1,
"suffix":"Hz",
"thermostat:set_param":{
"topic":"pid",
"field":"ki"
},
"lock":false
},
{
"name":"Kd",
"type":"float",
"step":0.1,
"suffix":"s",
"thermostat:set_param":{
"topic":"pid",
"field":"kd"
},
"lock":false
},
{
"name":"PID Output Clamping",
"expanded":true,
"type":"group",
"children":[
{
"name":"Minimum",
"type":"float",
"step":100,
"limits":[
-2000,
2000
],
"decimals":6,
"suffix":"mA",
"thermostat:set_param":{
"topic":"pid",
"field":"output_min"
},
"lock":false
},
{
"name":"Maximum",
"type":"float",
"step":100,
"limits":[
-2000,
2000
],
"decimals":6,
"suffix":"mA",
"thermostat:set_param":{
"topic":"pid",
"field":"output_max"
},
"lock":false
}
]
},
{
"name":"PID Auto Tune",
"expanded":false,
"type":"group",
"children":[
{
"name":"Target Temperature",
"type":"float",
"value":20,
"step":0.1,
"format":"{value:.4f} °C",
"pid_autotune":"target_temp"
},
{
"name":"Test Current",
"type":"float",
"value":0,
"decimals":6,
"step":100,
"limits":[
-2000,
2000
],
"suffix":"mA",
"pid_autotune":"test_current"
},
{
"name":"Temperature Swing",
"type":"float",
"value":1.5,
"step":0.1,
"prefix":"±",
"format":"{value:.4f} °C",
"pid_autotune":"temp_swing"
},
{
"name":"Lookback",
"type":"float",
"value":3.0,
"step":0.1,
"format":"{value:.4f} s",
"pid_autotune":"lookback"
}, },
{ {
"name": "Set Temperature", "name":"Run",
"type": "float", "type":"action",
"value": 25, "tip":"Run"
"step": 0.1, }
"limits": [ ]
-273, }
300 ]
], },
"format": "{value:.4f} °C", {
"thermostat:set_param": { "name":"Save to flash",
"topic": "pid", "type":"action",
"field": "target" "tip":"Save config to thermostat, applies on reset"
}, },
"lock": false {
} "name":"Load from flash",
] "type":"action",
}, "tip":"Load config from flash"
{ }
"name": "Limits", ]
"expanded": true, }
"type": "group",
"children": [
{
"name": "Max Cooling Current",
"type": "float",
"value": 0,
"step": 100,
"decimals": 6,
"limits": [
0,
2000
],
"suffix": "mA",
"thermostat:set_param": {
"topic": "output",
"field": "max_i_pos"
},
"lock": false
},
{
"name": "Max Heating Current",
"type": "float",
"value": 0,
"step": 100,
"decimals": 6,
"limits": [
0,
2000
],
"suffix": "mA",
"thermostat:set_param": {
"topic": "output",
"field": "max_i_neg"
},
"lock": false
},
{
"name": "Max Voltage Difference",
"type": "float",
"value": 0,
"step": 0.1,
"limits": [
0,
5
],
"siPrefix": true,
"suffix": "V",
"thermostat:set_param": {
"topic": "output",
"field": "max_v"
},
"lock": false
}
]
}
]
},
{
"name": "Thermistor Config",
"expanded": true,
"type": "group",
"children": [
{
"name": "T₀",
"type": "float",
"value": 25,
"step": 0.1,
"limits": [
-100,
100
],
"format": "{value:.4f} °C",
"thermostat:set_param": {
"topic": "s-h",
"field": "t0"
},
"lock": false
},
{
"name": "R₀",
"type": "float",
"value": 10000,
"step": 1,
"siPrefix": true,
"suffix": "Ω",
"thermostat:set_param": {
"topic": "s-h",
"field": "r0"
},
"lock": false
},
{
"name": "B",
"type": "float",
"value": 3950,
"step": 1,
"suffix": "K",
"decimals": 4,
"thermostat:set_param": {
"topic": "s-h",
"field": "b"
},
"lock": false
},
{
"name": "Postfilter Rate",
"type": "list",
"value": 16.67,
"thermostat:set_param": {
"topic": "postfilter",
"field": "rate"
},
"limits": {
"Off": null,
"16.67 Hz": 16.67,
"20 Hz": 20.0,
"21.25 Hz": 21.25,
"27 Hz": 27.0
},
"lock": false
}
]
},
{
"name": "PID Config",
"expanded": true,
"type": "group",
"children": [
{
"name": "Kp",
"type": "float",
"step": 0.1,
"suffix": "",
"thermostat:set_param": {
"topic": "pid",
"field": "kp"
},
"lock": false
},
{
"name": "Ki",
"type": "float",
"step": 0.1,
"suffix": "Hz",
"thermostat:set_param": {
"topic": "pid",
"field": "ki"
},
"lock": false
},
{
"name": "Kd",
"type": "float",
"step": 0.1,
"suffix": "s",
"thermostat:set_param": {
"topic": "pid",
"field": "kd"
},
"lock": false
},
{
"name": "PID Output Clamping",
"expanded": true,
"type": "group",
"children": [
{
"name": "Minimum",
"type": "float",
"step": 100,
"limits": [
-2000,
2000
],
"decimals": 6,
"suffix": "mA",
"thermostat:set_param": {
"topic": "pid",
"field": "output_min"
},
"lock": false
},
{
"name": "Maximum",
"type": "float",
"step": 100,
"limits": [
-2000,
2000
],
"decimals": 6,
"suffix": "mA",
"thermostat:set_param": {
"topic": "pid",
"field": "output_max"
},
"lock": false
}
]
},
{
"name": "PID Auto Tune",
"expanded": false,
"type": "group",
"children": [
{
"name": "Target Temperature",
"type": "float",
"value": 20,
"step": 0.1,
"format": "{value:.4f} °C",
"pid_autotune": "target_temp"
},
{
"name": "Test Current",
"type": "float",
"value": 0,
"decimals": 6,
"step": 100,
"limits": [
-2000,
2000
],
"suffix": "mA",
"pid_autotune": "test_current"
},
{
"name": "Temperature Swing",
"type": "float",
"value": 1.5,
"step": 0.1,
"prefix": "±",
"format": "{value:.4f} °C",
"pid_autotune": "temp_swing"
},
{
"name": "Lookback",
"type": "float",
"value": 3.0,
"step": 0.1,
"format": "{value:.4f} s",
"pid_autotune": "lookback"
},
{
"name": "Run",
"type": "action",
"tip": "Run"
}
]
}
]
},
{
"name": "Save to flash",
"type": "action",
"tip": "Save config to thermostat, applies on reset"
},
{
"name": "Load from flash",
"type": "action",
"tip": "Load config from flash"
}
]
}

View File

@ -6,18 +6,18 @@ class ZeroLimitsWarningView(QObject):
def __init__(self, thermostat, style, limit_warning): def __init__(self, thermostat, style, limit_warning):
super().__init__() super().__init__()
self._thermostat = thermostat self._thermostat = thermostat
self._thermostat.output_update.connect(self.set_limits_warning) self._thermostat.pwm_update.connect(self.set_limits_warning)
self._lbl = limit_warning self._lbl = limit_warning
self._style = style self._style = style
@pyqtSlot(list) @pyqtSlot(list)
def set_limits_warning(self, output_data: list): def set_limits_warning(self, pwm_data: list):
channels_zeroed_limits = [set() for i in range(self._thermostat.NUM_CHANNELS)] channels_zeroed_limits = [set() for i in range(self._thermostat.NUM_CHANNELS)]
for output_params in output_data: for pwm_params in pwm_data:
channel = output_params["channel"] channel = pwm_params["channel"]
for limit in "max_i_pos", "max_i_neg", "max_v": for limit in "max_i_pos", "max_i_neg", "max_v":
if output_params[limit] == 0.0: if pwm_params[limit] == 0.0:
channels_zeroed_limits[channel].add(limit) channels_zeroed_limits[channel].add(limit)
channel_disabled = [False, False] channel_disabled = [False, False]