Compare commits

..

12 Commits

Author SHA1 Message Date
953a48230c README: Introduce Thermostat GUI
Co-authored-by: topquark12 <aw@m-labs.hk>
2024-11-04 18:43:51 +08:00
0dbf154f82 pytec GUI: Set up packaging
Co-authored-by: Egor Savkin <es@m-labs.hk>
2024-11-04 18:43:49 +08:00
287186d030 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:43:27 +08:00
ef34d782e0 pytec GUI: Implement PlotSettingsMenu
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:43:27 +08:00
3d519826dd pytec GUI: Implement plotting
Co-authored-by: linuswck <linuswck@m-labs.hk>
2024-11-04 18:43:27 +08:00
50a69fbfeb 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:43:27 +08:00
3a45fdaaa6 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:43:27 +08:00
ebb240d7f3 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:43:27 +08:00
8daa40f62b 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:43:23 +08:00
8d2b7e69bb pytec: Create asyncio clients 2024-11-04 18:42:59 +08:00
52e35d2a98 flake: Format with nixfmt-rfc-style
Also set up formatter so that `nix fmt` formats.
2024-11-04 18:38:08 +08:00
f1da910c11 pytec: Complete client
Expose all available Thermostat commands to the pytec client, and add
disconnect functionality.
2024-11-04 18:03:50 +08:00
7 changed files with 464 additions and 377 deletions

View File

@ -7,9 +7,17 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, nixpkgs, rust-overlay }: outputs =
{
self,
nixpkgs,
rust-overlay,
}:
let let
pkgs = import nixpkgs { system = "x86_64-linux"; overlays = [ (import rust-overlay) ]; }; pkgs = import nixpkgs {
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" ];
@ -86,7 +94,8 @@
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;
@ -103,17 +112,26 @@
devShells.x86_64-linux.default = pkgs.mkShellNoCC { devShells.x86_64-linux.default = pkgs.mkShellNoCC {
name = "thermostat-dev-shell"; name = "thermostat-dev-shell";
packages = with pkgs; [ packages =
rust llvm with pkgs;
openocd dfu-util rlwrap [
rust
llvm
openocd
dfu-util
rlwrap
qtcreator qtcreator
] ++ (with python3Packages; [ ]
numpy matplotlib ++ (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,6 +12,10 @@ 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:
@ -110,6 +114,39 @@ 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
@ -163,10 +200,38 @@ 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): def save_config(self, channel=""):
"""Save current configuration to EEPROM""" """Save current configuration to EEPROM"""
self._command("save") self._command("save", channel)
if channel != "":
self._read_line() # read the extra {}
def load_config(self): def load_config(self, channel=""):
"""Load current configuration from EEPROM""" """Load current configuration from EEPROM"""
self._command("load") self._command("load", channel)
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("pwm", ch, "i_set", 0) await self._thermostat.set_param("output", 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(
"pwm", ch, "i_set", self.autotuners[ch].output() "output", 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("pwm", ch, "pid") await self._thermostat.set_param("output", 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("pwm", ch, "i_set", 0) await self._thermostat.set_param("output", 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)
pwm = Property(list) output = Property(list)
postfilter = Property(list) postfilter = Property(list)
report = Property(list) report = Property(list)
@ -82,8 +82,14 @@ 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 = ( (
await asyncio.gather( self.fan,
self.output,
self.report,
self.pid,
self.thermistor,
self.postfilter,
) = await asyncio.gather(
self._client.get_fan(), self._client.get_fan(),
self._client.get_output(), self._client.get_output(),
self._client.report(), self._client.report(),
@ -91,7 +97,6 @@ class Thermostat(QObject, metaclass=PropertyMeta):
self._client.get_b_parameter(), self._client.get_b_parameter(),
self._client.get_postfilter(), self._client.get_postfilter(),
) )
)
def connected(self): def connected(self):
return self._client.connected() return self._client.connected()

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.pwm_update.connect(self.update_pwm) self.thermostat.output_update.connect(self.update_output)
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_pwm(self, pwm_data): def update_output(self, output_data):
for pwm_params in pwm_data: for output_params in output_data:
channel = pwm_params["channel"] channel = output_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(pwm_params["max_v"]) ).setValue(output_params["max_v"])
self.params[channel].child( self.params[channel].child(
"Output Config", "Limits", "Max Cooling Current" "Output Config", "Limits", "Max Cooling Current"
).setValue(pwm_params["max_i_pos"] * 1000) ).setValue(output_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(pwm_params["max_i_neg"] * 1000) ).setValue(output_params["max_i_neg"] * 1000)
@pyqtSlot(list) @pyqtSlot(list)
def update_postfilter(self, postfilter_data): def update_postfilter(self, postfilter_data):
@ -304,4 +304,3 @@ 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":"pwm", "topic": "output",
"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":"pwm", "topic": "output",
"field":"i_set" "field": "i_set"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Set Temperature", "name": "Set Temperature",
"type":"float", "type": "float",
"value":25, "value": 25,
"step":0.1, "step": 0.1,
"limits":[ "limits": [
-273, -273,
300 300
], ],
"format":"{value:.4f} °C", "format": "{value:.4f} °C",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pid", "topic": "pid",
"field":"target" "field": "target"
}, },
"lock":false "lock": false
} }
] ]
}, },
{ {
"name":"Limits", "name": "Limits",
"expanded":true, "expanded": true,
"type":"group", "type": "group",
"children":[ "children": [
{ {
"name":"Max Cooling Current", "name": "Max Cooling Current",
"type":"float", "type": "float",
"value":0, "value": 0,
"step":100, "step": 100,
"decimals":6, "decimals": 6,
"limits":[ "limits": [
0, 0,
2000 2000
], ],
"suffix":"mA", "suffix": "mA",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pwm", "topic": "output",
"field":"max_i_pos" "field": "max_i_pos"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Max Heating Current", "name": "Max Heating Current",
"type":"float", "type": "float",
"value":0, "value": 0,
"step":100, "step": 100,
"decimals":6, "decimals": 6,
"limits":[ "limits": [
0, 0,
2000 2000
], ],
"suffix":"mA", "suffix": "mA",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pwm", "topic": "output",
"field":"max_i_neg" "field": "max_i_neg"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Max Voltage Difference", "name": "Max Voltage Difference",
"type":"float", "type": "float",
"value":0, "value": 0,
"step":0.1, "step": 0.1,
"limits":[ "limits": [
0, 0,
5 5
], ],
"siPrefix":true, "siPrefix": true,
"suffix":"V", "suffix": "V",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pwm", "topic": "output",
"field":"max_v" "field": "max_v"
}, },
"lock":false "lock": false
} }
] ]
} }
] ]
}, },
{ {
"name":"Thermistor Config", "name": "Thermistor Config",
"expanded":true, "expanded": true,
"type":"group", "type": "group",
"children":[ "children": [
{ {
"name":"T₀", "name": "T₀",
"type":"float", "type": "float",
"value":25, "value": 25,
"step":0.1, "step": 0.1,
"limits":[ "limits": [
-100, -100,
100 100
], ],
"format":"{value:.4f} °C", "format": "{value:.4f} °C",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"s-h", "topic": "s-h",
"field":"t0" "field": "t0"
}, },
"lock":false "lock": false
}, },
{ {
"name":"R₀", "name": "R₀",
"type":"float", "type": "float",
"value":10000, "value": 10000,
"step":1, "step": 1,
"siPrefix":true, "siPrefix": true,
"suffix":"Ω", "suffix": "Ω",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"s-h", "topic": "s-h",
"field":"r0" "field": "r0"
}, },
"lock":false "lock": false
}, },
{ {
"name":"B", "name": "B",
"type":"float", "type": "float",
"value":3950, "value": 3950,
"step":1, "step": 1,
"suffix":"K", "suffix": "K",
"decimals":4, "decimals": 4,
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"s-h", "topic": "s-h",
"field":"b" "field": "b"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Postfilter Rate", "name": "Postfilter Rate",
"type":"list", "type": "list",
"value":16.67, "value": 16.67,
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"postfilter", "topic": "postfilter",
"field":"rate" "field": "rate"
}, },
"limits":{ "limits": {
"Off":null, "Off": null,
"16.67 Hz":16.67, "16.67 Hz": 16.67,
"20 Hz":20.0, "20 Hz": 20.0,
"21.25 Hz":21.25, "21.25 Hz": 21.25,
"27 Hz":27.0 "27 Hz": 27.0
}, },
"lock":false "lock": false
} }
] ]
}, },
{ {
"name":"PID Config", "name": "PID Config",
"expanded":true, "expanded": true,
"type":"group", "type": "group",
"children":[ "children": [
{ {
"name":"Kp", "name": "Kp",
"type":"float", "type": "float",
"step":0.1, "step": 0.1,
"suffix":"", "suffix": "",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pid", "topic": "pid",
"field":"kp" "field": "kp"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Ki", "name": "Ki",
"type":"float", "type": "float",
"step":0.1, "step": 0.1,
"suffix":"Hz", "suffix": "Hz",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pid", "topic": "pid",
"field":"ki" "field": "ki"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Kd", "name": "Kd",
"type":"float", "type": "float",
"step":0.1, "step": 0.1,
"suffix":"s", "suffix": "s",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pid", "topic": "pid",
"field":"kd" "field": "kd"
}, },
"lock":false "lock": false
}, },
{ {
"name":"PID Output Clamping", "name": "PID Output Clamping",
"expanded":true, "expanded": true,
"type":"group", "type": "group",
"children":[ "children": [
{ {
"name":"Minimum", "name": "Minimum",
"type":"float", "type": "float",
"step":100, "step": 100,
"limits":[ "limits": [
-2000, -2000,
2000 2000
], ],
"decimals":6, "decimals": 6,
"suffix":"mA", "suffix": "mA",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pid", "topic": "pid",
"field":"output_min" "field": "output_min"
}, },
"lock":false "lock": false
}, },
{ {
"name":"Maximum", "name": "Maximum",
"type":"float", "type": "float",
"step":100, "step": 100,
"limits":[ "limits": [
-2000, -2000,
2000 2000
], ],
"decimals":6, "decimals": 6,
"suffix":"mA", "suffix": "mA",
"thermostat:set_param":{ "thermostat:set_param": {
"topic":"pid", "topic": "pid",
"field":"output_max" "field": "output_max"
}, },
"lock":false "lock": false
} }
] ]
}, },
{ {
"name":"PID Auto Tune", "name": "PID Auto Tune",
"expanded":false, "expanded": false,
"type":"group", "type": "group",
"children":[ "children": [
{ {
"name":"Target Temperature", "name": "Target Temperature",
"type":"float", "type": "float",
"value":20, "value": 20,
"step":0.1, "step": 0.1,
"format":"{value:.4f} °C", "format": "{value:.4f} °C",
"pid_autotune":"target_temp" "pid_autotune": "target_temp"
}, },
{ {
"name":"Test Current", "name": "Test Current",
"type":"float", "type": "float",
"value":0, "value": 0,
"decimals":6, "decimals": 6,
"step":100, "step": 100,
"limits":[ "limits": [
-2000, -2000,
2000 2000
], ],
"suffix":"mA", "suffix": "mA",
"pid_autotune":"test_current" "pid_autotune": "test_current"
}, },
{ {
"name":"Temperature Swing", "name": "Temperature Swing",
"type":"float", "type": "float",
"value":1.5, "value": 1.5,
"step":0.1, "step": 0.1,
"prefix":"±", "prefix": "±",
"format":"{value:.4f} °C", "format": "{value:.4f} °C",
"pid_autotune":"temp_swing" "pid_autotune": "temp_swing"
}, },
{ {
"name":"Lookback", "name": "Lookback",
"type":"float", "type": "float",
"value":3.0, "value": 3.0,
"step":0.1, "step": 0.1,
"format":"{value:.4f} s", "format": "{value:.4f} s",
"pid_autotune":"lookback" "pid_autotune": "lookback"
}, },
{ {
"name":"Run", "name": "Run",
"type":"action", "type": "action",
"tip":"Run" "tip": "Run"
} }
] ]
} }
] ]
}, },
{ {
"name":"Save to flash", "name": "Save to flash",
"type":"action", "type": "action",
"tip":"Save config to thermostat, applies on reset" "tip": "Save config to thermostat, applies on reset"
}, },
{ {
"name":"Load from flash", "name": "Load from flash",
"type":"action", "type": "action",
"tip":"Load config from flash" "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.pwm_update.connect(self.set_limits_warning) self._thermostat.output_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, pwm_data: list): def set_limits_warning(self, output_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 pwm_params in pwm_data: for output_params in output_data:
channel = pwm_params["channel"] channel = output_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 pwm_params[limit] == 0.0: if output_params[limit] == 0.0:
channels_zeroed_limits[channel].add(limit) channels_zeroed_limits[channel].add(limit)
channel_disabled = [False, False] channel_disabled = [False, False]