From be2e32aedcc3878138dc5c9515756150989d2ef6 Mon Sep 17 00:00:00 2001 From: Elias Bakken Date: Thu, 4 Jan 2018 23:35:05 +0100 Subject: [PATCH 01/27] Update index.rst --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index bc04fe8e..312f6bd8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,7 +44,7 @@ There are several different projects that contribute to this platform. **Kamikaze** the predecessor to Umikaze, based on Debian -**MagnaScreen** a compact, high-definition touch screen +**Manga Screen** a compact, high-definition touch screen **Toggle** software to build interactive applications with MagnaScreen From 88ea35d5a0c2f96549472b92617aa59fe859a58f Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 5 Jan 2018 07:17:45 +0000 Subject: [PATCH 02/27] added import of Printer to PruFirmware.py to allow indexing of axes --- redeem/PruFirmware.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/redeem/PruFirmware.py b/redeem/PruFirmware.py index a5eb8704..e5f64446 100644 --- a/redeem/PruFirmware.py +++ b/redeem/PruFirmware.py @@ -30,6 +30,8 @@ import re from six import iteritems +from Printer import Printer + class PruFirmware: def __init__(self, firmware_source_file0, binary_filename0, firmware_source_file1, binary_filename1, From b35fd56dbf77e475a4bb3c27fe85fb81bc0198a0 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 5 Jan 2018 11:48:12 +0000 Subject: [PATCH 03/27] modify SD printing to operate without buffering of the file before feeding it to the path planner --- redeem/SDCardManager.py | 117 ++++++++++++++++++++++++---------------- redeem/gcodes/M2x.py | 12 +++-- 2 files changed, 80 insertions(+), 49 deletions(-) diff --git a/redeem/SDCardManager.py b/redeem/SDCardManager.py index 47c2cc87..2c663063 100755 --- a/redeem/SDCardManager.py +++ b/redeem/SDCardManager.py @@ -1,10 +1,15 @@ from multiprocessing import Lock +import os +def blocks(files, size=65536): + while True: + b = files.read(size) + if not b: break + yield b class SDCardManager(object): file_name = None - lines = [] - lines_size = [] + gcode_file = None byte_count = None line_count = None file_byte_size = None @@ -20,58 +25,69 @@ def __iter__(self): def load_file(self, f): """ - read the file in as a list of lines + open the file, remains open until another file is opened """ - - with open(f, 'r') as gcode_file: - - self.lock.acquire() - self.file_name = f - self.line_count = 0 - self.byte_count = 0 - self.file_byte_size = 0 - self.file_line_size = 0 - self.lines = [] - self.lines_size = [] - self.lock.release() - gcode_file.seek(0) - - for line in gcode_file: - self.lock.acquire() - self.file_line_size += 1 - self.file_byte_size += len(line.encode('utf-8')) - self.lines.append(line) - self.lines_size.append(self.file_byte_size) # cumulative sum of bytes - self.lock.release() + self.lock.acquire() + + self.file_name = f + + if self.gcode_file: + self.gcode_file.close() + + self.gcode_file = open(self.file_name, 'r') + + self.line_count = 0 + self.byte_count = 0 + + # file size in bytes + self.file_byte_size = os.path.getsize(self.file_name) + + # file size in lines + self.file_line_size = sum(bl.count("\n") for bl in blocks(self.gcode_file)) + self.gcode_file.seek(-1, 2) + end = self.gcode_file.read() + if end != "\n": + self.file_line_size += 1 + + #reset file position + self.gcode_file.seek(0) + + self.lock.release() - return True + return True def next(self): """ - return the next line in the list and increment counters + return the next line in the file and increment counters """ self.lock.acquire() lc = self.line_count N = self.file_line_size - status = self.active + active = self.active self.lock.release() - if (lc < N) and status: + if not active: + raise StopIteration() + return + + if (lc < N): self.lock.acquire() - out = self.lines[self.line_count] - self.byte_count = self.lines_size[self.line_count] + line = self.gcode_file.readline() + self.byte_count += len(line.encode('utf-8')) self.line_count += 1 self.lock.release() - return out + return line else: self.lock.acquire() - self.byte_count = self.lines_size[-1] + self.byte_count = self.file_byte_size self.line_count = N self.lock.release() raise StopIteration() + return + def get_file_size(self): """ return the size of the file @@ -119,9 +135,9 @@ def get_status(self): return st - def set_active(self, status): + def set_status(self, status): """ - get the status of the current file + set the status of the current file """ self.lock.acquire() @@ -139,17 +155,30 @@ def set_position(self, byte_position=0, line_position=0): will be converted to a line position """ - if byte_position > 0: - self.lock.acquire() - szs = self.lines_size - self.lock.release() - for i, b in enumerate(szs): - if byte_position < b: - line_position = i - byte_position = b + self.lock.acquire() + + # reset file object + self.gcode_file.seek(0) + + # walk through the file line by line until we find a location that + # matches either line position or byte count + if (byte_position > 0) or (line_position > 0): + + i = 0; b = 0 + for line in self.gcode_file: + ls = len(line.encode('utf-8')) + if (byte_position > 0) and (byte_position < b+ls): break + elif (line_position > 0) and (line_position == i): + break + + i += 1 + b += ls + + line_position = i + byte_position = b - self.lock.acquire() + self.line_count = line_position self.byte_count = byte_position self.lock.release() @@ -163,8 +192,6 @@ def reset(self): """ self.file_name = None - self.lines = [] - self.lines_size = [] self.byte_count = None self.line_count = None self.file_byte_size = None diff --git a/redeem/gcodes/M2x.py b/redeem/gcodes/M2x.py index fd69053b..257216f8 100755 --- a/redeem/gcodes/M2x.py +++ b/redeem/gcodes/M2x.py @@ -281,6 +281,7 @@ def execute(self, g): self.printer.send_message(g.prot, "File opened:{} Lines:{} Size:{}B".format(fn, nl, nb)) self.printer.send_message(g.prot, "File selected") + logging.info("M23: finished gcode file processing") def get_description(self): @@ -302,7 +303,7 @@ class M24(GCodeCommand): def process_gcode(self, g): - self.printer.sd_card_manager.set_active(True) + self.printer.sd_card_manager.set_status(True) for line in self.printer.sd_card_manager: line = line.strip() @@ -313,7 +314,7 @@ def process_gcode(self, g): if self.printer.sd_card_manager.get_status(): logging.info("M24: file complete") - self.printer.sd_card_manager.set_active(False) + self.printer.sd_card_manager.set_status(False) self.printer.send_message(g.prot, "Done printing file") @@ -327,7 +328,10 @@ def execute(self, g): start_new_thread(self.process_gcode, (g, )) # allow some time for the new thread to start before we proceed - sleep(0.1) + counter = 0 + while (not active) and (counter < 10): + sleep(0.1) + counter += 1 self.printer.path_planner.resume() @@ -348,7 +352,7 @@ def is_buffered(self): class M25(GCodeCommand): def execute(self, g): - self.printer.sd_card_manager.set_active(False) + self.printer.sd_card_manager.set_status(False) def get_description(self): return "Pause the current SD print." From 6847a09eacecd7129a909b990a37494bd98f48fb Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sat, 6 Jan 2018 04:09:51 +0000 Subject: [PATCH 04/27] add ok to end of heating routine --- redeem/gcodes/M116.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redeem/gcodes/M116.py b/redeem/gcodes/M116.py index 9048197c..3425cf83 100644 --- a/redeem/gcodes/M116.py +++ b/redeem/gcodes/M116.py @@ -60,7 +60,7 @@ def execute(self, g): self.printer.processor.execute(m105) if False not in all_ok or not self.printer.running_M116: logging.info("Heating done.") - self.printer.send_message(g.prot, "Heating done.") + self.printer.send_message(g.prot, "ok Heating done.") self.printer.running_M116 = False return else: From dada4f0b53023cf1b37b62dd2c4ab5823537234a Mon Sep 17 00:00:00 2001 From: Elias Bakken Date: Thu, 4 Jan 2018 23:35:05 +0100 Subject: [PATCH 05/27] Update index.rst --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index bc04fe8e..312f6bd8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,7 +44,7 @@ There are several different projects that contribute to this platform. **Kamikaze** the predecessor to Umikaze, based on Debian -**MagnaScreen** a compact, high-definition touch screen +**Manga Screen** a compact, high-definition touch screen **Toggle** software to build interactive applications with MagnaScreen From 43d1a048b4ee4e9af6e15ac9045fa79d57711db0 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 5 Jan 2018 07:17:45 +0000 Subject: [PATCH 06/27] added import of Printer to PruFirmware.py to allow indexing of axes --- redeem/PruFirmware.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/redeem/PruFirmware.py b/redeem/PruFirmware.py index a5eb8704..e5f64446 100644 --- a/redeem/PruFirmware.py +++ b/redeem/PruFirmware.py @@ -30,6 +30,8 @@ import re from six import iteritems +from Printer import Printer + class PruFirmware: def __init__(self, firmware_source_file0, binary_filename0, firmware_source_file1, binary_filename1, From 92789eae4c3677fa46e0446fa0a967308e8dbd8c Mon Sep 17 00:00:00 2001 From: Jens Chr Brynildsen Date: Sun, 7 Jan 2018 23:54:05 +0100 Subject: [PATCH 07/27] Better instructions for Fan settings --- docs/replicape/configuration.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/replicape/configuration.rst b/docs/replicape/configuration.rst index e13441fc..a5c85322 100644 --- a/docs/replicape/configuration.rst +++ b/docs/replicape/configuration.rst @@ -634,8 +634,9 @@ code, so yours will be different than what you see here. ... connect-therm-H-fan-1 = False ... - - add-fan-0-to-M106 = False + # For your part cooling fan, you'll want to set this to True for the correct Fan-input so your slicer can control it. + # If your part-cooling fan is connected to the Fan0 input, use this: + add-fan-0-to-M106 = True ... # If you want coolers to @@ -645,6 +646,12 @@ code, so yours will be different than what you see here. # If you want the fan-thermistor connections to have a # different temperature: # therm-e-fan-0-target_temp = 70 + + # if you have a Titan Aero (or any other all-metal) hotend, you'll want the fan on the hotend to turn on + # automatically above 50C. To make the fan connected to the Fan1 input turn on when the hotend reaches 60C + # (provided that the hotend is connected to "thermistor extruder 1", also referred to as E). + connect-therm-E-fan-1 = True + therm-e-fan-1-target_temp = 50 .. _ConfigHeaters: From e812decbdf872c1fe299b1b20f1efb621e721f2e Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 10 Jan 2018 16:15:48 +1000 Subject: [PATCH 08/27] Fan control scheme based on operational units defined in the config. Inspired by Richard Wackerbarth. --- configs/default.cfg | 156 ++++++++++++++-------- redeem/FanControl.py | 310 +++++++++++++++++++++++++++++++++++++++++++ redeem/Redeem.py | 80 +++-------- 3 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 redeem/FanControl.py diff --git a/configs/default.cfg b/configs/default.cfg index c51838dc..3dd1246a 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -213,65 +213,103 @@ arc_segment_length = 0.001 # such movements will only apply to the E axis. e_axis_active = True -[Cold-ends] -# To use the DS18B20 temp sensors, connect them like this. -# Enable by setting to True -connect-ds18b20-0-fan-0 = False -connect-ds18b20-1-fan-0 = False -connect-ds18b20-0-fan-1 = False - -# This list is for connecting thermistors to fans, -# so they are controlled automatically when reaching 60 degrees. -connect-therm-E-fan-0 = False -connect-therm-E-fan-1 = False -connect-therm-E-fan-2 = False -connect-therm-E-fan-3 = False -connect-therm-H-fan-0 = False -connect-therm-H-fan-1 = False -connect-therm-H-fan-2 = False -connect-therm-H-fan-3 = False -connect-therm-A-fan-0 = False -connect-therm-A-fan-1 = False -connect-therm-A-fan-2 = False -connect-therm-A-fan-3 = False -connect-therm-B-fan-0 = False -connect-therm-B-fan-1 = False -connect-therm-B-fan-2 = False -connect-therm-B-fan-3 = False -connect-therm-C-fan-0 = False -connect-therm-C-fan-1 = False -connect-therm-C-fan-2 = False -connect-therm-C-fan-3 = False -connect-therm-HBP-fan-0 = False -connect-therm-HBP-fan-1 = False -connect-therm-HBP-fan-2 = False -connect-therm-HBP-fan-3 = False - -add-fan-0-to-M106 = False -add-fan-1-to-M106 = False -add-fan-2-to-M106 = False -add-fan-3-to-M106 = False - -# If you want coolers to -# have a different 'keep' temp, list it here. -cooler_0_target_temp = 60 - -# If you want the fan-thermitor connetions to have a -# different temperature: -# therm-e-fan-0-target_temp = 70 - -[Fans] -default-fan-0-value = 0.0 -default-fan-1-value = 0.0 -default-fan-2-value = 0.0 -default-fan-3-value = 0.0 -default-fan-3-value = 0.0 -default-fan-4-value = 0.0 -default-fan-5-value = 0.0 -default-fan-6-value = 0.0 -default-fan-7-value = 0.0 -default-fan-8-value = 0.0 -default-fan-9-value = 0.0 +# Fans allow for the connection of many different signals to determine the +# power level of that fan. The underlying idea of this approach is that we have +# sensors that measure temperature that feed into control units or comparator +# units. Available units are as follows: +# +# alias : name a unit something else, can make it easier to keep track +# difference : subtract input value 1 from input value 0 i.e. = (input-0) - (input-1) +# maximum : return the maximum value of two inputs +# minimum : return the minimum value of two inputs +# on-off-control: implement on-off control, returns a signal in the range 0..1 +# proportinal-control : implement proportional control, returns a signal in the range 0..1 +# +# A unit is defined as a section in the config, given by [UnitName], while the +# type and other options are specified as part of that section. To use the +# output of one unit in another, simply use the desired UnitName for the input +# value. +# +# allowed temperature inputs are: +# thermistor-E, thermistor-H, thermistor-HBP, and ds18b20-* (where * is an integer) +# possibly also: thermistor-A, thermistor-B, thermistor-C +# +# EXAMPLE: +# +#[CoolantTemperature] +#type = alias +#input = ds18b20-1 +# +#[CoolantWarmup] +#type = difference +#input-0 = CoolantTemperature +#input-1 = ds18b20-0 +# +#[CoolantFan] +#type = proportional-control +#input = CoolantWarmup +#target_temperature = 0 +#proportional = 0.1 +#max_speed = 255 +#min_speed = 50 +#ok_range = 2 +# +#[EitherHotend] +#type = maximum +#input-0 = thermistor-A +#input-1 = thermistor-B +# +#[CoolantPump] +#type = on-off-control +#input = EitherHotend +#on_temperature = 80 +#off_temperature = 60 +#on_power = 100 +#off_power = 0 +# +#[Fan-0] +#value = CoolantPump +# +#[Fan-1] +#value = CoolantFan +# +#[Fan-2] +#value = 127 +# +#[Fan-3] +#value = 0 +#add-to-M106 = True +# +# END OF EXAMPLE +# In this example Fan-0 controls a water cooling pump which only turns on at +# about 39% power (100/255=0.392) when either of two hot-ends (A or B) get +# above 80deg. It turns off when the temperature drops below 60deg. +# Fan-1 controls a fan that only turns on when the coolant temperature is above +# ambient temperature where the temperatures are given by two DS18B20 sensors. +# In this case the control scheme is proportional and so we would see a ramp-up +# in fan speed starting at ~20% when the temperature difference was above 2 +# degrees. Note that in this case the target temperature is 0 as we are +# supplying the control unit with a difference between two temperature sensors +# and we would like to see a difference of zero. +# Fan-2 and Fan-3 are both set to have a default starting value. Use this if +# that fan is to be attached to M106. + +[Fan-0] +value = 0 +add-to-M106 = False + +[Fan-1] +value = 0 +add-to-M106 = False + +[Fan-2] +value = 0 +add-to-M106 = False + +[Fan-3] +value = 0 +add-to-M106 = False + [Heaters] # For list of available temp charts, look in temp_chart.py diff --git a/redeem/FanControl.py b/redeem/FanControl.py new file mode 100644 index 00000000..f9ef169e --- /dev/null +++ b/redeem/FanControl.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +""" +A fan is for blowing stuff away. This one is for Replicape. + +Author: Daryl Bond +email: daryl(dot)bond(at)hotmail(dot)com +Website: http://www.thing-printer.com +License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html + + Redeem is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Redeem is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Redeem. If not, see . +""" + +import time +from builtins import range +from PWM import PWM +import logging + +#============================================================================== +# +#============================================================================== + +class Unit: + printer = None + def get_input(self, get): + """ + get the correct input + """ + # try to get it from the config + if self.printer.config.has_section(get): + input_type = self.printer.config.get(get, "type") + return control_units[input_type](get, self.printer) + + # check thermistors and cold ends + if "thermistor-" in get: + g = get.replace("thermistor-","") + if g in self.printer.thermistors: + return self.printer.thermistors[g] + elif "ds18b20" in get: + for sensor in self.printer.cold_ends: + if get == sensor.name: + return sensor + + # can't find it, assume it is a number + logging.info("Setting up fan controller. Cannot find {}. Assume it is a number".format(get)) + + try: + value = float(get) + except: + msg = "Setting up fan controller. Cannot convert '{}' to float".format(get) + raise RuntimeError(msg) + + + return value + +class Alias(Unit): + + def __init__(self, name, printer): + + self.name = name + self.printer = printer + input_name = printer.config.get(name, "input") + self.alias_input = self.get_input(input_name) + + return + + def get_temperature(self): + return self.alias_input.get_temperature() + +class Compare(Unit): + def __init__(self, name, printer): + self.printer = printer + self.name = name + self.inputs = [] + for i in range(2): + input_name = printer.config.get(name, "input-{}".format(i)) + self.inputs.append(self.get_input(input_name)) + return + +class Difference(Compare): + def get_temperature(self): + return self.inputs[0].get_temperature() - self.inputs[1].get_temperature() + +class Maximum(Compare): + def get_temperature(self): + return max(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + +class Minimum(Compare): + def get_temperature(self): + return min(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + +class Control(Unit): + + def __init__(self, name, printer): + self.name = name + self.printer = printer + input_name = printer.config.get(name, "input") + self.control_input = self.get_input(input_name) + + self.get_options() + + return + +class OnOffControl(Control): + + def get_options(self): + + # options + self.on_temperature = self.printer.config.getfloat(self.name, 'on_temperature') + self.off_temperature = self.printer.config.getfloat(self.name, 'off_temperature') + self.on_power = self.printer.config.getint(self.name, 'on_power')/255.0 + self.off_power = self.printer.config.getint(self.name, 'off_power')/255.0 + + self.power = self.off_power + + return + + def get_power(self): + + temp = self.control_input.get_temperature() + + if temp >= self.on_temperature: + self.power = self.on_power + elif temp <= self.off_temperature: + self.power = self.off_power + + return self.power + +class ProportionalControl(Control): + + def get_options(self): + """ Init """ + self.current_temp = 0.0 + self.target_temp = self.printer.config.getfloat(self.name, 'target_temperature') # Target temperature (Ts). Start off. + self.P = self.printer.config.getfloat(self.name, 'proportional') # Proportional + self.max_speed = self.printer.config.getfloat(self.name, 'max_speed')/255.0 + self.min_speed = self.printer.config.getfloat(self.name, 'min_speed')/255.0 + self.ok_range = self.printer.config.getfloat(self.name, 'ok_range') + + def get_power(self): + """ PID Thread that keeps the temperature stable """ + self.current_temp = self.control_input.get_temperature() + error = self.target_temp-self.current_temp + + print "error = ",error + + if error <= self.ok_range: + return 0.0 + + power = self.P*error # The formula for the PID (only P) + power = max(min(power, 1.0), 0.0) # Normalize to 0,1 + + # Invert the control since it'a a cooler + power = 1.0 - power + + # Clamp the max speed + power = min(power, self.max_speed) + # Clamp min speed + power = max(power, self.min_speed) + + return power + +### + +class Fan(Unit): #class Fan(PWM): + + def __init__(self, channel, fan_id, printer): + """ + channel : channel that this fan is on + fan_id : number of the fan + printer : description of this printer + """ + + self.channel = channel + self.printer = printer + + config = printer.config + + self.name = "Fan-{}".format(fan_id) + input_name = config.get(self.name, "value") + + self.fan_input = self.get_input(input_name) + + if isinstance(self.fan_input, float): + value = min(abs(float(self.fan_input))/255.0, 1.0) + self.set_value(value) + elif isinstance(self.fan_input, Control): + self.enable() # start the fan controller + else: + msg = "Fan input {} is not a number [0..255] or a control unit".format(self.fan_input) + logging.error(msg) + raise RuntimeError(msg) + + return + + def set_PWM_frequency(self, value): + """ Set the amount of on-time from 0..1 """ + self.pwm_frequency = int(value) + PWM.set_frequency(value) + + def set_value(self, value): + """ Set the amount of on-time from 0..1 """ + self.value = value + PWM.set_value(value, self.channel) + return + + + def ramp_to(self, value, delay=0.01): + ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' + for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): + logging.debug("Fan value: "+str(w)) + self.set_value(w/255.0) + time.sleep(delay) + self.set_value(value) + + def run_controller(self): + """ follow a target PWM value 0..1""" + + while self.enabled: + self.set_value(self.fan_input.get_power()) + time.sleep(1) + self.disabled = True + + def disable(self): + """ stops the controller """ + self.enabled = False + # Wait for controller to stop + while self.disabled == False: + time.sleep(0.2) + # The controller loop has finished + self.set_value(0.0) + + def enable(self): + """ starts the controller """ + self.enabled = True + self.disabled = False + self.t = Thread(target=self.run_controller, name=self.name) + self.t.daemon = True + self.t.start() + return + +### + +control_units = {"alias":Alias, "difference":Difference, + "maximum":Maximum, "minimum":Minimum, + "on-off-control":OnOffControl, + "proportional-control":ProportionalControl} + +### + +#============================================================================== +# EXAMPLE +#============================================================================== + +#[AmbientTemperature] +#type = alias +#input = ds18b20-1 +# +#[CoolantTemperature] +#type = alias +#input = ds18b20-0 +# +#[CoolantWarmup] +#type = difference +#input-0 = AmbientTemperature +#input-1 = CoolantTemperature +# +#[CoolantFan] +#type = proportional-control +#input = CoolantWarmup +#target_temperature = 0 +#proportional = 0.1 +#max_speed = 255 +#min_speed = 50 +#ok_range = 2 +# +#[EitherHotend] +#type = maximum +#input-0 = thermistor-A +#input-1 = thermistor-B +# +#[CoolantPump] +#type = on-off-control +#input = EitherHotend +#on_temperature = 80 +#off_temperature = 60 +#on_power = 100 +#off_power = 0 +# +#[Fan-0] +#value = CoolantPump +# +#[Fan-1] +#value = CoolantFan +# +#[Fan-2] +#value = 127 +# +#[Fan-3] +#value = 255 diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 4901e53b..133d50ff 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -39,7 +39,7 @@ from Mosfet import Mosfet from Stepper import * from TemperatureSensor import * -from Fan import Fan +from FanControl import Fan from Servo import Servo from EndStop import EndStop from USB import USB @@ -310,29 +310,25 @@ def __init__(self, config_location="/etc/redeem"): self.printer.heaters[e].max_temp_fall = self.printer.config.getfloat('Heaters', 'max_fall_temp_'+e) self.printer.heaters[e].max_power = self.printer.config.getfloat('Heaters', 'max_power_'+e) - # Init the three fans. Argument is PWM channel number + # Init the three fans. Arguments: PWM channel number, fan number, printer self.printer.fans = [] if self.revision == "00A3": - self.printer.fans.append(Fan(0)) - self.printer.fans.append(Fan(1)) - self.printer.fans.append(Fan(2)) + self.printer.fans.append(Fan(0,0,self.printer)) + self.printer.fans.append(Fan(1,1,self.printer)) + self.printer.fans.append(Fan(2,2,self.printer)) elif self.revision == "0A4A": - self.printer.fans.append(Fan(8)) - self.printer.fans.append(Fan(9)) - self.printer.fans.append(Fan(10)) + self.printer.fans.append(Fan(8,0,self.printer)) + self.printer.fans.append(Fan(9,1,self.printer)) + self.printer.fans.append(Fan(10,2,self.printer)) elif self.revision in ["00B1", "00B2", "00B3", "0B3A"]: - self.printer.fans.append(Fan(7)) - self.printer.fans.append(Fan(8)) - self.printer.fans.append(Fan(9)) - self.printer.fans.append(Fan(10)) + self.printer.fans.append(Fan(7,0,self.printer)) + self.printer.fans.append(Fan(8,1,self.printer)) + self.printer.fans.append(Fan(9,2,self.printer)) + self.printer.fans.append(Fan(10,3,self.printer)) if printer.config.reach_revision == "00A0": - self.printer.fans.append(Fan(14)) - self.printer.fans.append(Fan(15)) - self.printer.fans.append(Fan(7)) - - # Set default value for all fans - for i, f in enumerate(self.printer.fans): - f.set_value(self.printer.config.getfloat('Fans', "default-fan-{}-value".format(i))) + self.printer.fans.append(Fan(14,0,self.printer)) + self.printer.fans.append(Fan(15,1,self.printer)) + self.printer.fans.append(Fan(7,2,self.printer)) # Init the servos printer.servos = [] @@ -350,57 +346,13 @@ def __init__(self, config_location="/etc/redeem"): logging.info("Added servo "+str(servo_nr)) servo_nr += 1 - # Connect thermitors to fans - for t, therm in iteritems(self.printer.heaters): - for f, fan in enumerate(self.printer.fans): - if not self.printer.config.has_option('Cold-ends', "connect-therm-{}-fan-{}".format(t, f)): - continue - if printer.config.getboolean('Cold-ends', "connect-therm-{}-fan-{}".format(t, f)): - c = Cooler(therm, fan, "Cooler-{}-{}".format(t, f), True) # Use ON/OFF on these. - c.ok_range = 4 - opt_temp = "therm-{}-fan-{}-target_temp".format(t, f) - if printer.config.has_option('Cold-ends', opt_temp): - target_temp = printer.config.getfloat('Cold-ends', opt_temp) - else: - target_temp = 60 - c.set_target_temperature(target_temp) - max_speed = "therm-{}-fan-{}-max_speed".format(t, f) - if printer.config.has_option('Cold-ends', max_speed): - target_speed = printer.config.getfloat('Cold-ends', max_speed) - else: - target_speed = 1.0 - c.set_max_speed(target_speed) - c.enable() - printer.coolers.append(c) - logging.info("Cooler connects therm {} with fan {}".format(t, f)) - # Connect fans to M106 printer.controlled_fans = [] for i, fan in enumerate(self.printer.fans): - if not self.printer.config.has_option('Cold-ends', "add-fan-{}-to-M106".format(i)): - continue - if self.printer.config.getboolean('Cold-ends', "add-fan-{}-to-M106".format(i)): + if self.printer.config.getboolean('Fan-{}'.format(i), "add-to-M106"): printer.controlled_fans.append(self.printer.fans[i]) logging.info("Added fan {} to M106/M107".format(i)) - # Connect the colds to fans - for ce, cold_end in enumerate(self.printer.cold_ends): - for f, fan in enumerate(self.printer.fans): - option = "connect-ds18b20-{}-fan-{}".format(ce, f) - if self.printer.config.has_option('Cold-ends', option): - if self.printer.config.getboolean('Cold-ends', option): - c = Cooler(cold_end, fan, "Cooler-ds18b20-{}-{}".format(ce, f), False) - c.ok_range = 4 - opt_temp = "cooler_{}_target_temp".format(ce) - if printer.config.has_option('Cold-ends', opt_temp): - target_temp = printer.config.getfloat('Cold-ends', opt_temp) - else: - target_temp = 60 - c.set_target_temperature(target_temp) - c.enable() - printer.coolers.append(c) - logging.info("Cooler connects temp sensor ds18b20 {} with fan {}".format(ce, f)) - # Init roatray encs. printer.filament_sensors = [] From fc6f82ebd7a6753e814b8d423b3ea1dfd48b6c99 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 10 Jan 2018 16:15:48 +1000 Subject: [PATCH 09/27] Fan control scheme based on operational units defined in the config. Inspired by Richard Wackerbarth. --- configs/default.cfg | 156 ++++++++++++++-------- redeem/FanControl.py | 310 +++++++++++++++++++++++++++++++++++++++++++ redeem/Redeem.py | 80 +++-------- 3 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 redeem/FanControl.py diff --git a/configs/default.cfg b/configs/default.cfg index c51838dc..3dd1246a 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -213,65 +213,103 @@ arc_segment_length = 0.001 # such movements will only apply to the E axis. e_axis_active = True -[Cold-ends] -# To use the DS18B20 temp sensors, connect them like this. -# Enable by setting to True -connect-ds18b20-0-fan-0 = False -connect-ds18b20-1-fan-0 = False -connect-ds18b20-0-fan-1 = False - -# This list is for connecting thermistors to fans, -# so they are controlled automatically when reaching 60 degrees. -connect-therm-E-fan-0 = False -connect-therm-E-fan-1 = False -connect-therm-E-fan-2 = False -connect-therm-E-fan-3 = False -connect-therm-H-fan-0 = False -connect-therm-H-fan-1 = False -connect-therm-H-fan-2 = False -connect-therm-H-fan-3 = False -connect-therm-A-fan-0 = False -connect-therm-A-fan-1 = False -connect-therm-A-fan-2 = False -connect-therm-A-fan-3 = False -connect-therm-B-fan-0 = False -connect-therm-B-fan-1 = False -connect-therm-B-fan-2 = False -connect-therm-B-fan-3 = False -connect-therm-C-fan-0 = False -connect-therm-C-fan-1 = False -connect-therm-C-fan-2 = False -connect-therm-C-fan-3 = False -connect-therm-HBP-fan-0 = False -connect-therm-HBP-fan-1 = False -connect-therm-HBP-fan-2 = False -connect-therm-HBP-fan-3 = False - -add-fan-0-to-M106 = False -add-fan-1-to-M106 = False -add-fan-2-to-M106 = False -add-fan-3-to-M106 = False - -# If you want coolers to -# have a different 'keep' temp, list it here. -cooler_0_target_temp = 60 - -# If you want the fan-thermitor connetions to have a -# different temperature: -# therm-e-fan-0-target_temp = 70 - -[Fans] -default-fan-0-value = 0.0 -default-fan-1-value = 0.0 -default-fan-2-value = 0.0 -default-fan-3-value = 0.0 -default-fan-3-value = 0.0 -default-fan-4-value = 0.0 -default-fan-5-value = 0.0 -default-fan-6-value = 0.0 -default-fan-7-value = 0.0 -default-fan-8-value = 0.0 -default-fan-9-value = 0.0 +# Fans allow for the connection of many different signals to determine the +# power level of that fan. The underlying idea of this approach is that we have +# sensors that measure temperature that feed into control units or comparator +# units. Available units are as follows: +# +# alias : name a unit something else, can make it easier to keep track +# difference : subtract input value 1 from input value 0 i.e. = (input-0) - (input-1) +# maximum : return the maximum value of two inputs +# minimum : return the minimum value of two inputs +# on-off-control: implement on-off control, returns a signal in the range 0..1 +# proportinal-control : implement proportional control, returns a signal in the range 0..1 +# +# A unit is defined as a section in the config, given by [UnitName], while the +# type and other options are specified as part of that section. To use the +# output of one unit in another, simply use the desired UnitName for the input +# value. +# +# allowed temperature inputs are: +# thermistor-E, thermistor-H, thermistor-HBP, and ds18b20-* (where * is an integer) +# possibly also: thermistor-A, thermistor-B, thermistor-C +# +# EXAMPLE: +# +#[CoolantTemperature] +#type = alias +#input = ds18b20-1 +# +#[CoolantWarmup] +#type = difference +#input-0 = CoolantTemperature +#input-1 = ds18b20-0 +# +#[CoolantFan] +#type = proportional-control +#input = CoolantWarmup +#target_temperature = 0 +#proportional = 0.1 +#max_speed = 255 +#min_speed = 50 +#ok_range = 2 +# +#[EitherHotend] +#type = maximum +#input-0 = thermistor-A +#input-1 = thermistor-B +# +#[CoolantPump] +#type = on-off-control +#input = EitherHotend +#on_temperature = 80 +#off_temperature = 60 +#on_power = 100 +#off_power = 0 +# +#[Fan-0] +#value = CoolantPump +# +#[Fan-1] +#value = CoolantFan +# +#[Fan-2] +#value = 127 +# +#[Fan-3] +#value = 0 +#add-to-M106 = True +# +# END OF EXAMPLE +# In this example Fan-0 controls a water cooling pump which only turns on at +# about 39% power (100/255=0.392) when either of two hot-ends (A or B) get +# above 80deg. It turns off when the temperature drops below 60deg. +# Fan-1 controls a fan that only turns on when the coolant temperature is above +# ambient temperature where the temperatures are given by two DS18B20 sensors. +# In this case the control scheme is proportional and so we would see a ramp-up +# in fan speed starting at ~20% when the temperature difference was above 2 +# degrees. Note that in this case the target temperature is 0 as we are +# supplying the control unit with a difference between two temperature sensors +# and we would like to see a difference of zero. +# Fan-2 and Fan-3 are both set to have a default starting value. Use this if +# that fan is to be attached to M106. + +[Fan-0] +value = 0 +add-to-M106 = False + +[Fan-1] +value = 0 +add-to-M106 = False + +[Fan-2] +value = 0 +add-to-M106 = False + +[Fan-3] +value = 0 +add-to-M106 = False + [Heaters] # For list of available temp charts, look in temp_chart.py diff --git a/redeem/FanControl.py b/redeem/FanControl.py new file mode 100644 index 00000000..f9ef169e --- /dev/null +++ b/redeem/FanControl.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +""" +A fan is for blowing stuff away. This one is for Replicape. + +Author: Daryl Bond +email: daryl(dot)bond(at)hotmail(dot)com +Website: http://www.thing-printer.com +License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html + + Redeem is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Redeem is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Redeem. If not, see . +""" + +import time +from builtins import range +from PWM import PWM +import logging + +#============================================================================== +# +#============================================================================== + +class Unit: + printer = None + def get_input(self, get): + """ + get the correct input + """ + # try to get it from the config + if self.printer.config.has_section(get): + input_type = self.printer.config.get(get, "type") + return control_units[input_type](get, self.printer) + + # check thermistors and cold ends + if "thermistor-" in get: + g = get.replace("thermistor-","") + if g in self.printer.thermistors: + return self.printer.thermistors[g] + elif "ds18b20" in get: + for sensor in self.printer.cold_ends: + if get == sensor.name: + return sensor + + # can't find it, assume it is a number + logging.info("Setting up fan controller. Cannot find {}. Assume it is a number".format(get)) + + try: + value = float(get) + except: + msg = "Setting up fan controller. Cannot convert '{}' to float".format(get) + raise RuntimeError(msg) + + + return value + +class Alias(Unit): + + def __init__(self, name, printer): + + self.name = name + self.printer = printer + input_name = printer.config.get(name, "input") + self.alias_input = self.get_input(input_name) + + return + + def get_temperature(self): + return self.alias_input.get_temperature() + +class Compare(Unit): + def __init__(self, name, printer): + self.printer = printer + self.name = name + self.inputs = [] + for i in range(2): + input_name = printer.config.get(name, "input-{}".format(i)) + self.inputs.append(self.get_input(input_name)) + return + +class Difference(Compare): + def get_temperature(self): + return self.inputs[0].get_temperature() - self.inputs[1].get_temperature() + +class Maximum(Compare): + def get_temperature(self): + return max(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + +class Minimum(Compare): + def get_temperature(self): + return min(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + +class Control(Unit): + + def __init__(self, name, printer): + self.name = name + self.printer = printer + input_name = printer.config.get(name, "input") + self.control_input = self.get_input(input_name) + + self.get_options() + + return + +class OnOffControl(Control): + + def get_options(self): + + # options + self.on_temperature = self.printer.config.getfloat(self.name, 'on_temperature') + self.off_temperature = self.printer.config.getfloat(self.name, 'off_temperature') + self.on_power = self.printer.config.getint(self.name, 'on_power')/255.0 + self.off_power = self.printer.config.getint(self.name, 'off_power')/255.0 + + self.power = self.off_power + + return + + def get_power(self): + + temp = self.control_input.get_temperature() + + if temp >= self.on_temperature: + self.power = self.on_power + elif temp <= self.off_temperature: + self.power = self.off_power + + return self.power + +class ProportionalControl(Control): + + def get_options(self): + """ Init """ + self.current_temp = 0.0 + self.target_temp = self.printer.config.getfloat(self.name, 'target_temperature') # Target temperature (Ts). Start off. + self.P = self.printer.config.getfloat(self.name, 'proportional') # Proportional + self.max_speed = self.printer.config.getfloat(self.name, 'max_speed')/255.0 + self.min_speed = self.printer.config.getfloat(self.name, 'min_speed')/255.0 + self.ok_range = self.printer.config.getfloat(self.name, 'ok_range') + + def get_power(self): + """ PID Thread that keeps the temperature stable """ + self.current_temp = self.control_input.get_temperature() + error = self.target_temp-self.current_temp + + print "error = ",error + + if error <= self.ok_range: + return 0.0 + + power = self.P*error # The formula for the PID (only P) + power = max(min(power, 1.0), 0.0) # Normalize to 0,1 + + # Invert the control since it'a a cooler + power = 1.0 - power + + # Clamp the max speed + power = min(power, self.max_speed) + # Clamp min speed + power = max(power, self.min_speed) + + return power + +### + +class Fan(Unit): #class Fan(PWM): + + def __init__(self, channel, fan_id, printer): + """ + channel : channel that this fan is on + fan_id : number of the fan + printer : description of this printer + """ + + self.channel = channel + self.printer = printer + + config = printer.config + + self.name = "Fan-{}".format(fan_id) + input_name = config.get(self.name, "value") + + self.fan_input = self.get_input(input_name) + + if isinstance(self.fan_input, float): + value = min(abs(float(self.fan_input))/255.0, 1.0) + self.set_value(value) + elif isinstance(self.fan_input, Control): + self.enable() # start the fan controller + else: + msg = "Fan input {} is not a number [0..255] or a control unit".format(self.fan_input) + logging.error(msg) + raise RuntimeError(msg) + + return + + def set_PWM_frequency(self, value): + """ Set the amount of on-time from 0..1 """ + self.pwm_frequency = int(value) + PWM.set_frequency(value) + + def set_value(self, value): + """ Set the amount of on-time from 0..1 """ + self.value = value + PWM.set_value(value, self.channel) + return + + + def ramp_to(self, value, delay=0.01): + ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' + for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): + logging.debug("Fan value: "+str(w)) + self.set_value(w/255.0) + time.sleep(delay) + self.set_value(value) + + def run_controller(self): + """ follow a target PWM value 0..1""" + + while self.enabled: + self.set_value(self.fan_input.get_power()) + time.sleep(1) + self.disabled = True + + def disable(self): + """ stops the controller """ + self.enabled = False + # Wait for controller to stop + while self.disabled == False: + time.sleep(0.2) + # The controller loop has finished + self.set_value(0.0) + + def enable(self): + """ starts the controller """ + self.enabled = True + self.disabled = False + self.t = Thread(target=self.run_controller, name=self.name) + self.t.daemon = True + self.t.start() + return + +### + +control_units = {"alias":Alias, "difference":Difference, + "maximum":Maximum, "minimum":Minimum, + "on-off-control":OnOffControl, + "proportional-control":ProportionalControl} + +### + +#============================================================================== +# EXAMPLE +#============================================================================== + +#[AmbientTemperature] +#type = alias +#input = ds18b20-1 +# +#[CoolantTemperature] +#type = alias +#input = ds18b20-0 +# +#[CoolantWarmup] +#type = difference +#input-0 = AmbientTemperature +#input-1 = CoolantTemperature +# +#[CoolantFan] +#type = proportional-control +#input = CoolantWarmup +#target_temperature = 0 +#proportional = 0.1 +#max_speed = 255 +#min_speed = 50 +#ok_range = 2 +# +#[EitherHotend] +#type = maximum +#input-0 = thermistor-A +#input-1 = thermistor-B +# +#[CoolantPump] +#type = on-off-control +#input = EitherHotend +#on_temperature = 80 +#off_temperature = 60 +#on_power = 100 +#off_power = 0 +# +#[Fan-0] +#value = CoolantPump +# +#[Fan-1] +#value = CoolantFan +# +#[Fan-2] +#value = 127 +# +#[Fan-3] +#value = 255 diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 4901e53b..133d50ff 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -39,7 +39,7 @@ from Mosfet import Mosfet from Stepper import * from TemperatureSensor import * -from Fan import Fan +from FanControl import Fan from Servo import Servo from EndStop import EndStop from USB import USB @@ -310,29 +310,25 @@ def __init__(self, config_location="/etc/redeem"): self.printer.heaters[e].max_temp_fall = self.printer.config.getfloat('Heaters', 'max_fall_temp_'+e) self.printer.heaters[e].max_power = self.printer.config.getfloat('Heaters', 'max_power_'+e) - # Init the three fans. Argument is PWM channel number + # Init the three fans. Arguments: PWM channel number, fan number, printer self.printer.fans = [] if self.revision == "00A3": - self.printer.fans.append(Fan(0)) - self.printer.fans.append(Fan(1)) - self.printer.fans.append(Fan(2)) + self.printer.fans.append(Fan(0,0,self.printer)) + self.printer.fans.append(Fan(1,1,self.printer)) + self.printer.fans.append(Fan(2,2,self.printer)) elif self.revision == "0A4A": - self.printer.fans.append(Fan(8)) - self.printer.fans.append(Fan(9)) - self.printer.fans.append(Fan(10)) + self.printer.fans.append(Fan(8,0,self.printer)) + self.printer.fans.append(Fan(9,1,self.printer)) + self.printer.fans.append(Fan(10,2,self.printer)) elif self.revision in ["00B1", "00B2", "00B3", "0B3A"]: - self.printer.fans.append(Fan(7)) - self.printer.fans.append(Fan(8)) - self.printer.fans.append(Fan(9)) - self.printer.fans.append(Fan(10)) + self.printer.fans.append(Fan(7,0,self.printer)) + self.printer.fans.append(Fan(8,1,self.printer)) + self.printer.fans.append(Fan(9,2,self.printer)) + self.printer.fans.append(Fan(10,3,self.printer)) if printer.config.reach_revision == "00A0": - self.printer.fans.append(Fan(14)) - self.printer.fans.append(Fan(15)) - self.printer.fans.append(Fan(7)) - - # Set default value for all fans - for i, f in enumerate(self.printer.fans): - f.set_value(self.printer.config.getfloat('Fans', "default-fan-{}-value".format(i))) + self.printer.fans.append(Fan(14,0,self.printer)) + self.printer.fans.append(Fan(15,1,self.printer)) + self.printer.fans.append(Fan(7,2,self.printer)) # Init the servos printer.servos = [] @@ -350,57 +346,13 @@ def __init__(self, config_location="/etc/redeem"): logging.info("Added servo "+str(servo_nr)) servo_nr += 1 - # Connect thermitors to fans - for t, therm in iteritems(self.printer.heaters): - for f, fan in enumerate(self.printer.fans): - if not self.printer.config.has_option('Cold-ends', "connect-therm-{}-fan-{}".format(t, f)): - continue - if printer.config.getboolean('Cold-ends', "connect-therm-{}-fan-{}".format(t, f)): - c = Cooler(therm, fan, "Cooler-{}-{}".format(t, f), True) # Use ON/OFF on these. - c.ok_range = 4 - opt_temp = "therm-{}-fan-{}-target_temp".format(t, f) - if printer.config.has_option('Cold-ends', opt_temp): - target_temp = printer.config.getfloat('Cold-ends', opt_temp) - else: - target_temp = 60 - c.set_target_temperature(target_temp) - max_speed = "therm-{}-fan-{}-max_speed".format(t, f) - if printer.config.has_option('Cold-ends', max_speed): - target_speed = printer.config.getfloat('Cold-ends', max_speed) - else: - target_speed = 1.0 - c.set_max_speed(target_speed) - c.enable() - printer.coolers.append(c) - logging.info("Cooler connects therm {} with fan {}".format(t, f)) - # Connect fans to M106 printer.controlled_fans = [] for i, fan in enumerate(self.printer.fans): - if not self.printer.config.has_option('Cold-ends', "add-fan-{}-to-M106".format(i)): - continue - if self.printer.config.getboolean('Cold-ends', "add-fan-{}-to-M106".format(i)): + if self.printer.config.getboolean('Fan-{}'.format(i), "add-to-M106"): printer.controlled_fans.append(self.printer.fans[i]) logging.info("Added fan {} to M106/M107".format(i)) - # Connect the colds to fans - for ce, cold_end in enumerate(self.printer.cold_ends): - for f, fan in enumerate(self.printer.fans): - option = "connect-ds18b20-{}-fan-{}".format(ce, f) - if self.printer.config.has_option('Cold-ends', option): - if self.printer.config.getboolean('Cold-ends', option): - c = Cooler(cold_end, fan, "Cooler-ds18b20-{}-{}".format(ce, f), False) - c.ok_range = 4 - opt_temp = "cooler_{}_target_temp".format(ce) - if printer.config.has_option('Cold-ends', opt_temp): - target_temp = printer.config.getfloat('Cold-ends', opt_temp) - else: - target_temp = 60 - c.set_target_temperature(target_temp) - c.enable() - printer.coolers.append(c) - logging.info("Cooler connects temp sensor ds18b20 {} with fan {}".format(ce, f)) - # Init roatray encs. printer.filament_sensors = [] From c50f805998d6937806e6421b0e68a9167a2aef5c Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Thu, 11 Jan 2018 13:11:40 +0000 Subject: [PATCH 10/27] Working (minimal testing) implementation of nested config, with configurable allowed sandbox sections in which users can define custom sections. Fan control updated to allow non-linear initialisation as part of a multi-pass startup routine. --- configs/default.cfg | 125 +++--------- redeem/CascadingConfigParser.py | 269 +++++++++++++++++-------- redeem/Fan.py | 83 -------- redeem/FanControl.py | 310 ----------------------------- redeem/PluginsController.py | 4 +- redeem/PruFirmware.py | 2 +- redeem/Redeem.py | 92 +++++---- redeem/TemperatureControl.py | 340 ++++++++++++++++++++++++++++++++ requirements.txt | 1 + 9 files changed, 618 insertions(+), 608 deletions(-) delete mode 100644 redeem/Fan.py delete mode 100644 redeem/FanControl.py create mode 100644 redeem/TemperatureControl.py diff --git a/configs/default.cfg b/configs/default.cfg index 3dd1246a..585047ac 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -213,102 +213,39 @@ arc_segment_length = 0.001 # such movements will only apply to the E axis. e_axis_active = True -# Fans allow for the connection of many different signals to determine the -# power level of that fan. The underlying idea of this approach is that we have -# sensors that measure temperature that feed into control units or comparator -# units. Available units are as follows: -# -# alias : name a unit something else, can make it easier to keep track -# difference : subtract input value 1 from input value 0 i.e. = (input-0) - (input-1) -# maximum : return the maximum value of two inputs -# minimum : return the minimum value of two inputs -# on-off-control: implement on-off control, returns a signal in the range 0..1 -# proportinal-control : implement proportional control, returns a signal in the range 0..1 -# -# A unit is defined as a section in the config, given by [UnitName], while the -# type and other options are specified as part of that section. To use the -# output of one unit in another, simply use the desired UnitName for the input -# value. -# -# allowed temperature inputs are: -# thermistor-E, thermistor-H, thermistor-HBP, and ds18b20-* (where * is an integer) -# possibly also: thermistor-A, thermistor-B, thermistor-C -# -# EXAMPLE: -# -#[CoolantTemperature] -#type = alias -#input = ds18b20-1 -# -#[CoolantWarmup] -#type = difference -#input-0 = CoolantTemperature -#input-1 = ds18b20-0 -# -#[CoolantFan] -#type = proportional-control -#input = CoolantWarmup -#target_temperature = 0 -#proportional = 0.1 -#max_speed = 255 -#min_speed = 50 -#ok_range = 2 -# -#[EitherHotend] -#type = maximum -#input-0 = thermistor-A -#input-1 = thermistor-B -# -#[CoolantPump] -#type = on-off-control -#input = EitherHotend -#on_temperature = 80 -#off_temperature = 60 -#on_power = 100 -#off_power = 0 -# -#[Fan-0] -#value = CoolantPump -# -#[Fan-1] -#value = CoolantFan -# -#[Fan-2] -#value = 127 -# -#[Fan-3] -#value = 0 -#add-to-M106 = True -# -# END OF EXAMPLE -# In this example Fan-0 controls a water cooling pump which only turns on at -# about 39% power (100/255=0.392) when either of two hot-ends (A or B) get -# above 80deg. It turns off when the temperature drops below 60deg. -# Fan-1 controls a fan that only turns on when the coolant temperature is above -# ambient temperature where the temperatures are given by two DS18B20 sensors. -# In this case the control scheme is proportional and so we would see a ramp-up -# in fan speed starting at ~20% when the temperature difference was above 2 -# degrees. Note that in this case the target temperature is 0 as we are -# supplying the control unit with a difference between two temperature sensors -# and we would like to see a difference of zero. -# Fan-2 and Fan-3 are both set to have a default starting value. Use this if -# that fan is to be attached to M106. - -[Fan-0] -value = 0 +[Temperature Control] + +# put temperature control units in here, they won't be accepted anywhere else! + +[Fans] + +# control the fans, hook them up to a temperature control unit or simply set +# a constant value. Note that 'channel' is modified on startup according to +# your Replicape version. + +[[Fan-0]] +type = fan +input = 0 add-to-M106 = False +channel = 0 -[Fan-1] -value = 0 +[[Fan-1]] +type=fan +input = 0 add-to-M106 = False +channel = 0 -[Fan-2] -value = 0 +[[Fan-2]] +type = fan +input = 0 add-to-M106 = False +channel = 0 -[Fan-3] -value = 0 +[[Fan-3]] +type = fan +input = 0 add-to-M106 = False +channel = 0 [Heaters] @@ -586,7 +523,7 @@ enable_watchdog = True [Macros] -G29 = +G29 =""" M561 ; Reset the bed level matrix M558 P0 ; Set probe type to Servo with switch M557 P0 X10 Y20 ; Set probe point 0 @@ -618,13 +555,11 @@ G29 = G0 Z0 ; Move the Z up G31 ; Dock probe - G28 X0 Y0 ; Home X Y + G28 X0 Y0 ; Home X Y""" -G31 = - M280 P0 S320 F3000 ; Probe up (Dock sled) +G31 = M280 P0 S320 F3000 ; Probe up (Dock sled) -G32 = - M280 P0 S-60 F3000 ; Probe down (Undock sled) +G32 = M280 P0 S-60 F3000 ; Probe down (Undock sled) # Configuration for the HPX2Max plugin (if loaded) diff --git a/redeem/CascadingConfigParser.py b/redeem/CascadingConfigParser.py index 983f48c9..eb12198e 100644 --- a/redeem/CascadingConfigParser.py +++ b/redeem/CascadingConfigParser.py @@ -1,5 +1,5 @@ """ -Author: Elias Bakken +Author: Elias Bakken & Daryl Bond email: elias(dot)bakken(at)gmail(dot)com Website: http://www.thing-printer.com License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html @@ -18,19 +18,88 @@ along with Redeem. If not, see . """ -import ConfigParser +from configobj import OPTION_DEFAULTS, ConfigObj, Section import os import logging import struct +#============================================================================== +# Functions +#============================================================================== -class CascadingConfigParser(ConfigParser.SafeConfigParser): - def __init__(self, config_files): +def walk_up(section, lineage): + '''return the parentage of a section in list form''' + if section.depth == 1: + return lineage + else: + lineage.insert(0, section.parent.name) + walk_up(section.parent, lineage) + return lineage - ConfigParser.SafeConfigParser.__init__(self) +def walk_down(cfg, path, key, value, allow_new): + '''make/modify an entry of value in a dict given a list of sections and a + key. New sections only allowed in allow_new''' + + if allow_new == True: + allow = True + else: + allow = False + for p in reversed(path): + if p in allow_new: + allow = True + break + + if cfg.depth >= len(path): - # Write options in the case it was read. - # self.optionxform = str + # check if we can add a value + if key not in cfg: + if not allow: + return True + + cfg[key] = value + return False + else: + # check if we can add a section + if path[cfg.depth] not in cfg: + if allow: + cfg[path[cfg.depth]] = {} + else: + return True + + return walk_down(cfg[path[cfg.depth]], path, key, value, allow_new) + + +def check_modified(cfg, path, key, value): + '''check if value is the same as that in a dict given a list of + sections and a key''' + + if cfg.depth >= len(path): + if cfg[key] != value: + return True + return False + else: + if path[cfg.depth] not in cfg: + return True + else: + modified = check_modified(cfg[path[cfg.depth]], path, key, value) + return modified + +#============================================================================== +# Class +#============================================================================== + +class CascadingConfigParser(ConfigObj): + + parser_options = OPTION_DEFAULTS + + def __init__(self, config_files, allow_new=[]): + + self.parser_options["list_values"] = False + + ConfigObj.__init__(self) + + # sections which are allowed to have entries added by printer.cfg, or local.cfg + self.allow_new = allow_new # Parse to real path self.config_files = [] @@ -38,14 +107,15 @@ def __init__(self, config_files): self.config_files.append(os.path.realpath(config_file)) self.config_location = os.path.dirname(os.path.realpath(config_file)) - # Parse all config files in list + # check all config files in list for config_file in self.config_files: if os.path.isfile(config_file): logging.info("Using config file " + config_file) - self.readfp(open(config_file)) else: logging.warning("Missing config file " + config_file) - # Might also add command line options for overriding stuff + + # parse config files + self.load() def timestamp(self): """ Get the largest (newest) timestamp for all the config files. """ @@ -87,80 +157,64 @@ def parse_capes(self): pass return - def get_default_settings(self): - fs = [] - for config_file in self.config_files: + def load(self): + '''generate a config that combines all of the cascading configs in the + list. Config entry (i+1) entry overwrites entry (i). Entries may be + added at any level but only in allowed sections''' + + for i, config_file in enumerate(self.config_files): if os.path.isfile(config_file): c_file = os.path.basename(config_file) - cp = ConfigParser.SafeConfigParser() - cp.readfp(open(config_file)) - fs.append((c_file, cp)) - - lines = [] - for section in self.sections(): - for option in self.options(section): - for (name, cp) in fs: - if cp.has_option(section, option): - line = [name, section, option, cp.get(section, option)] - lines.append(line) - - return lines - + items = [] + if i == 0: # generate the base config + self._initialise(self.parser_options) + self._load(config_file, self._original_configspec) + else: + cfg = ConfigObj(config_file, **self.parser_options) + + # get a linear list of all items + items = [] + cfg.walk(lambda section, key : items.append( + (walk_up(section,[section.name]), key, section[key]))) + + # overwrite or add to the base config + for item in items: + if walk_down(self, item[0], item[1], item[2], self.allow_new): + path = '/'.join(item[0]+[item[1]]) + msg = "Config entry not permitted : {} = {} ".format(path, item[2]) + logging.warning(msg) + + + return def save(self, filename): """ Save the changed settings to local.cfg """ + + # get a copy of the currently hard-coded configs current = CascadingConfigParser(self.config_files) - - # Get list of changed values + + # get a flat list of all entries in the live config + items = [] + self.walk(lambda section, key : items.append( + (walk_up(section,[section.name]), key, section[key]))) + + # check for differences between the live config and the hard config to_save = [] - for section in self.sections(): - #logging.debug(section) - for option in self.options(section): - if self.get(section, option) != current.get(section, option): - old = current.get(section, option) - val = self.get(section, option) - to_save.append((section, option, val, old)) - - # Update local config with changed values - local = ConfigParser.SafeConfigParser() - local.readfp(open(filename, "r")) - for opt in to_save: - (section, option, value, old) = opt - if not local.has_section(section): - local.add_section(section) - local.set(section, option, value) - logging.info("Update setting: {} from {} to {} ".format(option, old, value)) - - + for item in items: + if check_modified(current, item[0], item[1], item[2]): + to_save.append(item) + + # make a new temporary config object and load in the stuff to be saved + local = ConfigObj(**self.parser_options) + for item in to_save: + walk_down(local, item[0], item[1], item[2], allow_new=True) + + path = '-'.join(item[0]+[item[1]]) + logging.info("Update local config: {} = {} ".format(path, item[2])) + # Save changed values to file local.write(open(filename, "w+")) - - def check(self, filename): - """ Check the settings currently set against default.cfg """ - default = ConfigParser.SafeConfigParser() - default.readfp(open(os.path.join(self.config_location, "default.cfg"))) - local = ConfigParser.SafeConfigParser() - local.readfp(open(filename)) - - local_ok = True - diff = set(local.sections())-set(default.sections()) - for section in diff: - logging.warning("Section {} does not exist in {}".format(section, "default.cfg")) - local_ok = False - for section in local.sections(): - if not default.has_section(section): - continue - diff = set(local.options(section))-set(default.options(section)) - for option in diff: - logging.warning("Option {} in section {} does not exist in {}".format(option, section, "default.cfg")) - local_ok = False - if local_ok: - logging.info("{} is OK".format(filename)) - else: - logging.warning("{} contains errors.".format(filename)) - return local_ok - def get_key(self): """ Get the generated key from the config or create one """ self.replicape_key = "".join(struct.unpack('20c', self.replicape_data[100:120])) @@ -179,11 +233,68 @@ def get_key(self): except IOError as e: logging.warning("Unable to write new key to EEPROM") return self.replicape_key + + def getint(self, *path): + ''' get integer ''' + cfg = self + for s in path[:-1]: + cfg = cfg[s] + return cfg.as_int(path[-1]) + + def getfloat(self, *path): + ''' get float ''' + cfg = self + for s in path[:-1]: + cfg = cfg[s] + return cfg.as_float(path[-1]) + + def getboolean(self, *path): + ''' get integer ''' + cfg = self + for s in path[:-1]: + cfg = cfg[s] + return cfg.as_bool(path[-1]) + + def get(self, *path): + ''' get whatever is there ''' + cfg = self + for s in path[:-1]: + cfg = cfg[s] + return cfg[path[-1]] + + def has_option(self, *path): + '''check if path exists''' + cfg = self + try: + for s in path: + cfg = cfg[s] + except: + return False + + if not isinstance(cfg, Section): + return True + return False + + def has_section(self, *path): + '''check for section''' + cfg = self + try: + for s in path: + cfg = cfg[s] + except: + return False + + if isinstance(cfg, Section): + return True + + return False if __name__ == '__main__': - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M') - c = CascadingConfigParser(["/etc/redeem/default.cfg", "/etc/redeem/printer.cfg", "/etc/redeem/local.cfg"]) - print(c.get_default_settings()) + c = CascadingConfigParser(["../configs/default.cfg", "../configs/test.cfg"], allow_new=["Temperature Control"]) + + print c["Fans"] + + #c.save("") + + #print c#["Temperature Control"] \ No newline at end of file diff --git a/redeem/Fan.py b/redeem/Fan.py deleted file mode 100644 index 1b3a0ead..00000000 --- a/redeem/Fan.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python -""" -A fan is for blowing stuff away. This one is for Replicape. - -Author: Elias Bakken -email: elias(dot)bakken(at)gmail(dot)com -Website: http://www.thing-printer.com -License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html - - Redeem is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Redeem is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Redeem. If not, see . -""" - -import time -from builtins import range -from PWM import PWM - - -class Fan(PWM): - - def __init__(self, channel): - """ Channel is the channel that the fan is on (0-7) """ - self.channel = channel - - def set_PWM_frequency(self, value): - """ Set the amount of on-time from 0..1 """ - self.pwm_frequency = int(value) - PWM.set_frequency(value) - - def set_value(self, value): - """ Set the amount of on-time from 0..1 """ - self.value = value - PWM.set_value(value, self.channel) - - - def ramp_to(self, value, delay=0.01): - ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' - for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): - logging.debug("Fan value: "+str(w)) - self.set_value(w/255.0) - time.sleep(delay) - self.set_value(value) - -if __name__ == '__main__': - import os - import logging - - logging.basicConfig(level=logging.DEBUG, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M') - - PWM.set_frequency(100) - - fan7 = Fan(7) - fan8 = Fan(8) - fan9 = Fan(9) - fan10 = Fan(10) - - while 1: - for i in range(1, 100): - fan7.set_value(i/100.0) - fan8.set_value(i/100.0) - fan9.set_value(i/100.0) - fan10.set_value(i/100.0) - time.sleep(0.01) - for i in range(100, 1, -1): - fan7.set_value(i/100.0) - fan8.set_value(i/100.0) - fan9.set_value(i/100.0) - fan10.set_value(i/100.0) - time.sleep(0.01) - - diff --git a/redeem/FanControl.py b/redeem/FanControl.py deleted file mode 100644 index f9ef169e..00000000 --- a/redeem/FanControl.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python -""" -A fan is for blowing stuff away. This one is for Replicape. - -Author: Daryl Bond -email: daryl(dot)bond(at)hotmail(dot)com -Website: http://www.thing-printer.com -License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html - - Redeem is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Redeem is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Redeem. If not, see . -""" - -import time -from builtins import range -from PWM import PWM -import logging - -#============================================================================== -# -#============================================================================== - -class Unit: - printer = None - def get_input(self, get): - """ - get the correct input - """ - # try to get it from the config - if self.printer.config.has_section(get): - input_type = self.printer.config.get(get, "type") - return control_units[input_type](get, self.printer) - - # check thermistors and cold ends - if "thermistor-" in get: - g = get.replace("thermistor-","") - if g in self.printer.thermistors: - return self.printer.thermistors[g] - elif "ds18b20" in get: - for sensor in self.printer.cold_ends: - if get == sensor.name: - return sensor - - # can't find it, assume it is a number - logging.info("Setting up fan controller. Cannot find {}. Assume it is a number".format(get)) - - try: - value = float(get) - except: - msg = "Setting up fan controller. Cannot convert '{}' to float".format(get) - raise RuntimeError(msg) - - - return value - -class Alias(Unit): - - def __init__(self, name, printer): - - self.name = name - self.printer = printer - input_name = printer.config.get(name, "input") - self.alias_input = self.get_input(input_name) - - return - - def get_temperature(self): - return self.alias_input.get_temperature() - -class Compare(Unit): - def __init__(self, name, printer): - self.printer = printer - self.name = name - self.inputs = [] - for i in range(2): - input_name = printer.config.get(name, "input-{}".format(i)) - self.inputs.append(self.get_input(input_name)) - return - -class Difference(Compare): - def get_temperature(self): - return self.inputs[0].get_temperature() - self.inputs[1].get_temperature() - -class Maximum(Compare): - def get_temperature(self): - return max(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) - -class Minimum(Compare): - def get_temperature(self): - return min(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) - -class Control(Unit): - - def __init__(self, name, printer): - self.name = name - self.printer = printer - input_name = printer.config.get(name, "input") - self.control_input = self.get_input(input_name) - - self.get_options() - - return - -class OnOffControl(Control): - - def get_options(self): - - # options - self.on_temperature = self.printer.config.getfloat(self.name, 'on_temperature') - self.off_temperature = self.printer.config.getfloat(self.name, 'off_temperature') - self.on_power = self.printer.config.getint(self.name, 'on_power')/255.0 - self.off_power = self.printer.config.getint(self.name, 'off_power')/255.0 - - self.power = self.off_power - - return - - def get_power(self): - - temp = self.control_input.get_temperature() - - if temp >= self.on_temperature: - self.power = self.on_power - elif temp <= self.off_temperature: - self.power = self.off_power - - return self.power - -class ProportionalControl(Control): - - def get_options(self): - """ Init """ - self.current_temp = 0.0 - self.target_temp = self.printer.config.getfloat(self.name, 'target_temperature') # Target temperature (Ts). Start off. - self.P = self.printer.config.getfloat(self.name, 'proportional') # Proportional - self.max_speed = self.printer.config.getfloat(self.name, 'max_speed')/255.0 - self.min_speed = self.printer.config.getfloat(self.name, 'min_speed')/255.0 - self.ok_range = self.printer.config.getfloat(self.name, 'ok_range') - - def get_power(self): - """ PID Thread that keeps the temperature stable """ - self.current_temp = self.control_input.get_temperature() - error = self.target_temp-self.current_temp - - print "error = ",error - - if error <= self.ok_range: - return 0.0 - - power = self.P*error # The formula for the PID (only P) - power = max(min(power, 1.0), 0.0) # Normalize to 0,1 - - # Invert the control since it'a a cooler - power = 1.0 - power - - # Clamp the max speed - power = min(power, self.max_speed) - # Clamp min speed - power = max(power, self.min_speed) - - return power - -### - -class Fan(Unit): #class Fan(PWM): - - def __init__(self, channel, fan_id, printer): - """ - channel : channel that this fan is on - fan_id : number of the fan - printer : description of this printer - """ - - self.channel = channel - self.printer = printer - - config = printer.config - - self.name = "Fan-{}".format(fan_id) - input_name = config.get(self.name, "value") - - self.fan_input = self.get_input(input_name) - - if isinstance(self.fan_input, float): - value = min(abs(float(self.fan_input))/255.0, 1.0) - self.set_value(value) - elif isinstance(self.fan_input, Control): - self.enable() # start the fan controller - else: - msg = "Fan input {} is not a number [0..255] or a control unit".format(self.fan_input) - logging.error(msg) - raise RuntimeError(msg) - - return - - def set_PWM_frequency(self, value): - """ Set the amount of on-time from 0..1 """ - self.pwm_frequency = int(value) - PWM.set_frequency(value) - - def set_value(self, value): - """ Set the amount of on-time from 0..1 """ - self.value = value - PWM.set_value(value, self.channel) - return - - - def ramp_to(self, value, delay=0.01): - ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' - for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): - logging.debug("Fan value: "+str(w)) - self.set_value(w/255.0) - time.sleep(delay) - self.set_value(value) - - def run_controller(self): - """ follow a target PWM value 0..1""" - - while self.enabled: - self.set_value(self.fan_input.get_power()) - time.sleep(1) - self.disabled = True - - def disable(self): - """ stops the controller """ - self.enabled = False - # Wait for controller to stop - while self.disabled == False: - time.sleep(0.2) - # The controller loop has finished - self.set_value(0.0) - - def enable(self): - """ starts the controller """ - self.enabled = True - self.disabled = False - self.t = Thread(target=self.run_controller, name=self.name) - self.t.daemon = True - self.t.start() - return - -### - -control_units = {"alias":Alias, "difference":Difference, - "maximum":Maximum, "minimum":Minimum, - "on-off-control":OnOffControl, - "proportional-control":ProportionalControl} - -### - -#============================================================================== -# EXAMPLE -#============================================================================== - -#[AmbientTemperature] -#type = alias -#input = ds18b20-1 -# -#[CoolantTemperature] -#type = alias -#input = ds18b20-0 -# -#[CoolantWarmup] -#type = difference -#input-0 = AmbientTemperature -#input-1 = CoolantTemperature -# -#[CoolantFan] -#type = proportional-control -#input = CoolantWarmup -#target_temperature = 0 -#proportional = 0.1 -#max_speed = 255 -#min_speed = 50 -#ok_range = 2 -# -#[EitherHotend] -#type = maximum -#input-0 = thermistor-A -#input-1 = thermistor-B -# -#[CoolantPump] -#type = on-off-control -#input = EitherHotend -#on_temperature = 80 -#off_temperature = 60 -#on_power = 100 -#off_power = 0 -# -#[Fan-0] -#value = CoolantPump -# -#[Fan-1] -#value = CoolantFan -# -#[Fan-2] -#value = 127 -# -#[Fan-3] -#value = 255 diff --git a/redeem/PluginsController.py b/redeem/PluginsController.py index ec502bd8..4b9a404c 100644 --- a/redeem/PluginsController.py +++ b/redeem/PluginsController.py @@ -36,7 +36,9 @@ def __init__(self, printer): self.plugins = {} # Load the plugins specified by the config - pluginsToLoad = [v.strip() for v in self.printer.config.get('System', 'plugins', '').split(',')] + pluginsToLoad = [] + if 'plugins' in self.printer.config["System"]: + pluginsToLoad = [v.strip() for v in self.printer.config.get('System', 'plugins').split(',')] pluginClasses = PluginsController.get_plugin_classes() for plugin in pluginsToLoad: diff --git a/redeem/PruFirmware.py b/redeem/PruFirmware.py index e5f64446..148fec58 100644 --- a/redeem/PruFirmware.py +++ b/redeem/PruFirmware.py @@ -256,7 +256,7 @@ def make_config_file(self): configFile.write('#define STEPPER_' + name + '_DIR_PIN\t\t' + dir_pin+'\n') # Define direction - direction = "0" if self.config.getint('Steppers', 'direction_' + name) > 0 else "1" + direction = "0" if self.config.getint('Steppers', 'direction_' + name.lower()) > 0 else "1" configFile.write('#define STEPPER_'+ name +'_DIRECTION\t\t'+ direction +'\n') index = Printer.axis_to_index(name) diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 133d50ff..0effb504 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -39,7 +39,7 @@ from Mosfet import Mosfet from Stepper import * from TemperatureSensor import * -from FanControl import Fan +import TemperatureControl from Servo import Servo from EndStop import EndStop from USB import USB @@ -117,13 +117,8 @@ def __init__(self, config_location="/etc/redeem"): printer.config = CascadingConfigParser( [os.path.join(config_location,'default.cfg'), os.path.join(config_location,'printer.cfg'), - os.path.join(config_location,'local.cfg')]) - - # Check the local and printer files - printer_path = os.path.join(config_location,"printer.cfg") - if os.path.exists(printer_path): - printer.config.check(printer_path) - printer.config.check(os.path.join(config_location,'local.cfg')) + os.path.join(config_location,'local.cfg')], + allow_new = ["Temperature Control"]) # <-- this is where users are allowed to add stuff to the config # Get the revision and loglevel from the Config file level = self.printer.config.getint('System', 'loglevel') @@ -189,6 +184,9 @@ def __init__(self, config_location="/etc/redeem"): # activate all the endstops self.printer.set_active_endstops() + + ####################################################################### + # STEPPER # Init the 5 Stepper motors (step, dir, fault, DAC channel, name) Stepper.printer = printer @@ -235,17 +233,17 @@ def __init__(self, config_location="/etc/redeem"): # Enable the steppers and set the current, steps pr mm and # microstepping for name, stepper in iteritems(self.printer.steppers): - stepper.in_use = printer.config.getboolean('Steppers', 'in_use_' + name) - stepper.direction = printer.config.getint('Steppers', 'direction_' + name) - stepper.has_endstop = printer.config.getboolean('Endstops', 'has_' + name) - stepper.set_current_value(printer.config.getfloat('Steppers', 'current_' + name)) - stepper.set_steps_pr_mm(printer.config.getfloat('Steppers', 'steps_pr_mm_' + name)) - stepper.set_microstepping(printer.config.getint('Steppers', 'microstepping_' + name)) - stepper.set_decay(printer.config.getint("Steppers", "slow_decay_" + name)) + stepper.in_use = printer.config.getboolean('Steppers', 'in_use_' + name.lower()) + stepper.direction = printer.config.getint('Steppers', 'direction_' + name.lower()) + stepper.has_endstop = printer.config.getboolean('Endstops', 'has_' + name.lower()) + stepper.set_current_value(printer.config.getfloat('Steppers', 'current_' + name.lower())) + stepper.set_steps_pr_mm(printer.config.getfloat('Steppers', 'steps_pr_mm_' + name.lower())) + stepper.set_microstepping(printer.config.getint('Steppers', 'microstepping_' + name.lower())) + stepper.set_decay(printer.config.getint("Steppers", "slow_decay_" + name.lower())) # Add soft end stops - printer.soft_min[Printer.axis_to_index(name)] = printer.config.getfloat('Endstops', 'soft_end_stop_min_' + name) - printer.soft_max[Printer.axis_to_index(name)] = printer.config.getfloat('Endstops', 'soft_end_stop_max_' + name) - slave = printer.config.get('Steppers', 'slave_' + name) + printer.soft_min[Printer.axis_to_index(name)] = printer.config.getfloat('Endstops', 'soft_end_stop_min_' + name.lower()) + printer.soft_max[Printer.axis_to_index(name)] = printer.config.getfloat('Endstops', 'soft_end_stop_max_' + name.lower()) + slave = printer.config.get('Steppers', 'slave_' + name.lower()) if slave: printer.add_slave(name, slave) logging.debug("Axis "+name+" has slave "+slave) @@ -260,6 +258,9 @@ def __init__(self, config_location="/etc/redeem"): opts = ["L", "r", "A_radial", "B_radial", "C_radial", "A_angular", "B_angular", "C_angular" ] for opt in opts: Delta.__dict__[opt] = printer.config.getfloat('Delta', opt) + + ####################################################################### + # TEMPERATURE CONTROL # Discover and add all DS18B20 cold ends. paths = glob.glob("/sys/bus/w1/devices/28-*/w1_slave") @@ -310,25 +311,34 @@ def __init__(self, config_location="/etc/redeem"): self.printer.heaters[e].max_temp_fall = self.printer.config.getfloat('Heaters', 'max_fall_temp_'+e) self.printer.heaters[e].max_power = self.printer.config.getfloat('Heaters', 'max_power_'+e) - # Init the three fans. Arguments: PWM channel number, fan number, printer - self.printer.fans = [] + + # update the channel information for fans if self.revision == "00A3": - self.printer.fans.append(Fan(0,0,self.printer)) - self.printer.fans.append(Fan(1,1,self.printer)) - self.printer.fans.append(Fan(2,2,self.printer)) + for i, c in enumerate([0,1,2]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c elif self.revision == "0A4A": - self.printer.fans.append(Fan(8,0,self.printer)) - self.printer.fans.append(Fan(9,1,self.printer)) - self.printer.fans.append(Fan(10,2,self.printer)) + for i, c in enumerate([8,9,10]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c elif self.revision in ["00B1", "00B2", "00B3", "0B3A"]: - self.printer.fans.append(Fan(7,0,self.printer)) - self.printer.fans.append(Fan(8,1,self.printer)) - self.printer.fans.append(Fan(9,2,self.printer)) - self.printer.fans.append(Fan(10,3,self.printer)) + for i, c in enumerate([7,8,9,10]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c if printer.config.reach_revision == "00A0": - self.printer.fans.append(Fan(14,0,self.printer)) - self.printer.fans.append(Fan(15,1,self.printer)) - self.printer.fans.append(Fan(7,2,self.printer)) + for i, c in enumerate([14,15,7]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c + + self.printer.controlled_fans = [] + self.printer.fans = [] + + # fans and ... generated and added to printer in build_temperature_control + TemperatureControl.build_temperature_control(self.printer) + + # turn on the fans and ... + + for fan in self.printer.fans: + fan.enable() + + ####################################################################### + # SERVO # Init the servos printer.servos = [] @@ -346,12 +356,8 @@ def __init__(self, config_location="/etc/redeem"): logging.info("Added servo "+str(servo_nr)) servo_nr += 1 - # Connect fans to M106 - printer.controlled_fans = [] - for i, fan in enumerate(self.printer.fans): - if self.printer.config.getboolean('Fan-{}'.format(i), "add-to-M106"): - printer.controlled_fans.append(self.printer.fans[i]) - logging.info("Added fan {} to M106/M107".format(i)) + ####################################################################### + # ROTARY ENCODER # Init roatray encs. printer.filament_sensors = [] @@ -376,6 +382,10 @@ def __init__(self, config_location="/etc/redeem"): sensor.alarm_level = alarm_level printer.filament_sensors.append(sensor) + + ####################################################################### + # PATH PLANNING + # Make a queue of commands self.printer.commands = JoinableQueue(10) @@ -586,6 +596,10 @@ def exit(self): for name, heater in iteritems(self.printer.heaters): logging.debug("closing "+name) heater.disable() + + for fan in self.printer.fans: + logging.debug("closing "+fan.name) + fan.disable() for name, comm in iteritems(self.printer.comms): logging.debug("closing "+name) diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py new file mode 100644 index 00000000..b917a516 --- /dev/null +++ b/redeem/TemperatureControl.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python +""" +Implementation of a system for controlling heating and cooling on the +replicape. Essentially consists of building blocks for creating a network of +functional units that connects temperature sensors to heating/cooling units. + +Author: Daryl Bond +email: daryl(dot)bond(at)hotmail(dot)com +Website: http://www.thing-printer.com +License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html + + Redeem is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Redeem is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Redeem. If not, see . +""" + +import time +from builtins import range +from PWM import PWM +from configobj import Section +import logging +from threading import Thread + +#============================================================================== +# CLASSES +#============================================================================== + +class Unit: + + printer = None + counter = 0 + + def get_unit(self, name, units): + """ retrieve a thermistor, cold_end, or unit""" + + # check units, thermistors, and cold ends + if name in units: + return units[name] + elif "thermistor-" in name: + g = name.replace("thermistor-","") + if g in self.printer.thermistors: + return self.printer.thermistors[g] + elif "ds18b20" in name: + for sensor in self.printer.cold_ends: + if name == sensor.name: + return sensor + else: #assume it is a constant + c_name = "Constant-{}".format(self.counter) + unit = ConstantControl(c_name, {"input":int(name)}, self.printer) + units[c_name] = unit + return unit + + + return + + +class Alias(Unit): + + def __init__(self, name, options, printer): + + self.name = name + self.options = options + self.printer = printer + self.input = options["input"] + + self.output = None + if "output" in options: + self.output = options["output"] + + self.counter += 1 + + return + + def connect(self, units): + self.input = self.get_unit(self.input, units) + if self.output: + self.output = self.get_unit(self.output, units) + self.output.input = self + + def get_temperature(self): + return self.input.get_temperature() + + +class Compare(Unit): + def __init__(self, name, options, printer): + self.name = name + self.options = options + self.printer = printer + self.inputs = [] + for i in range(2): + self.inputs.append(options["input-{}".format(i)]) + + self.output = None + if "output" in options: + self.output = options["output"] + + self.counter += 1 + + return + + def connect(self, units): + for i in range(2): + self.inputs[i] = self.get_unit(self.inputs[i], units) + if self.output: + self.output = self.get_unit(self.output, units) + self.output.input = self + + +class Difference(Compare): + def get_temperature(self): + return self.inputs[0].get_temperature() - self.inputs[1].get_temperature() + + +class Maximum(Compare): + def get_temperature(self): + return max(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + + +class Minimum(Compare): + def get_temperature(self): + return min(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + + +class Control(Unit): + + def __init__(self, name, options, printer): + self.name = name + self.options = options + self.printer = printer + self.input = options["input"] + + self.output = None + if "output" in options: + self.output = options["output"] + + self.get_options() + + self.counter += 1 + + return + + def connect(self, units): + self.input = self.get_unit(self.input, units) + if self.output: + self.output = self.get_unit(self.output, units) + self.output.input = self + + +class ConstantControl(Control): + + def get_options(self): + self.power = int(self.options['input'])/255.0 + return + + def get_power(self): + return self.power + + +class OnOffControl(Control): + + def get_options(self): + self.on_temperature = float(self.options['on_temperature']) + self.off_temperature = float(self.options['off_temperature']) + self.on_power = float(self.options['on_power'])/255.0 + self.off_power = float(self.options['off_power'])/255.0 + + self.power = self.off_power + + return + + def get_power(self): + + temp = self.input.get_temperature() + + if temp >= self.on_temperature: + self.power = self.on_power + elif temp <= self.off_temperature: + self.power = self.off_power + + return self.power + + +class ProportionalControl(Control): + + def get_options(self): + """ Init """ + self.current_temp = 0.0 + self.target_temp = float(self.options['target_temperature']) # Target temperature (Ts). Start off. + self.P = float(self.options['proportional']) # Proportional + self.max_speed = float(self.options['max_speed'])/255.0 + self.min_speed = float(self.options['min_speed'])/255.0 + self.ok_range = float(self.options['ok_range']) + + def get_power(self): + """ PID Thread that keeps the temperature stable """ + self.current_temp = self.input.get_temperature() + error = self.target_temp-self.current_temp + + if error <= self.ok_range: + return 0.0 + + power = self.P*error # The formula for the PID (only P) + power = max(min(power, 1.0), 0.0) # Normalize to 0,1 + + # Clamp the max speed + power = min(power, self.max_speed) + # Clamp min speed + power = max(power, self.min_speed) + + return power + +### + +class Fan(Unit): + + def __init__(self, name, options, printer): + """ + channel : channel that this fan is on + fan_id : number of the fan + printer : description of this printer + """ + + self.name = name + self.options = options + self.printer = printer + + self.input = self.options["input"] + self.channel = int(self.options["channel"]) + self.force_disable = False + + self.printer.fans.append(self) + + self.counter += 1 + + return + + def connect(self, units): + self.input = self.get_unit(self.input, units) + + if self.options["add-to-M106"] == "True": + self.force_disable = True + if not isinstance(self.input, ConstantControl): + msg = "{} has a non-constant controller attached. For control by M106/M107 set config 'input' as a constant".format(self.name) + logging.error(msg) + raise RuntimeError(msg) + + self.printer.controlled_fans.append(self) + logging.info("Added {} to M106/M107".format(self.name)) + + + def set_PWM_frequency(self, value): + """ Set the amount of on-time from 0..1 """ + self.pwm_frequency = int(value) + PWM.set_frequency(value) + + def set_value(self, value): + """ Set the amount of on-time from 0..1 """ + self.value = value + PWM.set_value(value, self.channel) + return + + + def ramp_to(self, value, delay=0.01): + ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' + for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): + logging.debug("Fan value: "+str(w)) + self.set_value(w/255.0) + time.sleep(delay) + self.set_value(value) + + def run_controller(self): + """ follow a target PWM value 0..1""" + + while self.enabled: + self.set_value(self.input.get_power()) + time.sleep(1) + self.disabled = True + + def disable(self): + """ stops the controller """ + self.enabled = False + # Wait for controller to stop + while self.disabled == False: + time.sleep(0.2) + # The controller loop has finished + self.set_value(0.0) + + def enable(self): + """ starts the controller """ + if self.force_disable: + self.disabled = True + self.enabled = False + return + self.enabled = True + self.disabled = False + self.t = Thread(target=self.run_controller, name=self.name) + self.t.daemon = True + self.t.start() + return + +#============================================================================== +# FUNCTIONS +#============================================================================== + +def build_temperature_control(printer): + """ build the network linking sensors to controllers """ + + control_units = {"alias":Alias, "difference":Difference, + "maximum":Maximum, "minimum":Minimum, + "constant-control":ConstantControl, + "on-off-control":OnOffControl, + "proportional-control":ProportionalControl, + "fan":Fan} + + units = {} + for section in ["Temperature Control", "Fans"]: #, Heaters]: TODO + cfg = printer.config[section] + + # generate units + for name, options in cfg.items(): + if not isinstance(options, Section): + continue + input_type = options["type"] + unit = control_units[input_type](name, options, printer) + units[name] = unit + + # connect units + for name, unit in units.items(): + unit.connect(units) + + return diff --git a/requirements.txt b/requirements.txt index 24e38125..f89b6e7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ six==1.10.0 sh==1.12.14 sympy==1.1.1 testfixtures==5.1.1 +configobj==5.0.6 From 2bf8b07a6117b703dd0b6eb5dd950f3b56ad3b97 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 12 Jan 2018 09:02:23 +0000 Subject: [PATCH 11/27] First pass at migrating heating control to the user defined control scheme. Runs with default settings, no further checks performed --- configs/default.cfg | 292 ++++++++++++++++++++++++----------- redeem/Alarm.py | 6 + redeem/Cooler.py | 102 ------------ redeem/Extruder.py | 260 ------------------------------- redeem/Fan.py | 119 ++++++++++++++ redeem/Heater.py | 270 ++++++++++++++++++++++++++++++++ redeem/Redeem.py | 106 +++++++------ redeem/TemperatureControl.py | 199 +++++++++++------------- redeem/TemperatureSensor.py | 10 +- redeem/gcodes/M105.py | 4 +- 10 files changed, 753 insertions(+), 615 deletions(-) delete mode 100644 redeem/Cooler.py delete mode 100644 redeem/Extruder.py create mode 100644 redeem/Fan.py create mode 100644 redeem/Heater.py diff --git a/configs/default.cfg b/configs/default.cfg index 585047ac..5faa782f 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -216,6 +216,109 @@ e_axis_active = True [Temperature Control] # put temperature control units in here, they won't be accepted anywhere else! +# Set 'active' = True for all units that are to be parsed. A False value means +# it will be ignored + +# default PID control for heaters +# When this config is parsed the 'active' flag is updated to reflect the +# available thermistors for your Replicape version +[[Control-E]] +type = pid-control +input = Thermistor-E +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +sleep = 0.25 +active = False + +[[Control-H]] +type = pid-control +input = Thermistor-H +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +sleep = 0.25 +active = False + +[[Control-A]] +type = pid-control +input = Thermistor-A +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +sleep = 0.25 +active = False + +[[Control-B]] +type = pid-control +input = Thermistor-B +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +sleep = 0.25 +active = False + +[[Control-C]] +type = pid-control +input = Thermistor-C +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +sleep = 0.25 +active = False + +[[Control-HBP]] +type = pid-control +input = Thermistor-HBP +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +sleep = 0.5 +active = False + +[Thermistors] + +# Thermistors for measuring temperature +# For list of available temp charts, look in temp_chart.py +# When this config is parsed the 'active' flag is updated to reflect the +# available thermistors for your Replicape version + +[[Thermistor-E]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage4_raw +active = False + +[[Thermistor-H]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage5_raw +active = False + +[[Thermistor-A]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage0_raw +active = False + +[[Thermistor-B]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage3_raw +active = False + +[[Thermistor-C]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage2_raw +active = False + +[[Thermistor-HBP]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage6_raw +active = False + [Fans] @@ -228,118 +331,127 @@ type = fan input = 0 add-to-M106 = False channel = 0 +active = True [[Fan-1]] type=fan input = 0 add-to-M106 = False channel = 0 +active = True [[Fan-2]] type = fan input = 0 add-to-M106 = False channel = 0 +active = True [[Fan-3]] type = fan input = 0 add-to-M106 = False channel = 0 +active = True [Heaters] -# For list of available temp charts, look in temp_chart.py -sensor_E = B57560G104F -pid_Kp_E = 0.1 -pid_Ti_E = 100.0 -pid_Td_E = 0.3 -ok_range_E = 4.0 -max_rise_temp_E = 10.0 -max_fall_temp_E = 10.0 -min_temp_E = 20.0 -max_temp_E = 250.0 -path_adc_E = /sys/bus/iio/devices/iio:device0/in_voltage4_raw -mosfet_E = 5 -onoff_E = False -prefix_E = T0 -max_power_E = 1.0 - -sensor_H = B57560G104F -pid_Kp_H = 0.1 -pid_Ti_H = 0.01 -pid_Td_H = 0.3 -ok_range_H = 4.0 -max_rise_temp_H = 10.0 -max_fall_temp_H = 10.0 -min_temp_H = 20.0 -max_temp_H = 250.0 -path_adc_H = /sys/bus/iio/devices/iio:device0/in_voltage5_raw -mosfet_H = 3 -onoff_H = False -prefix_H = T1 -max_power_H = 1.0 - -sensor_A = B57560G104F -pid_Kp_A = 0.1 -pid_Ti_A = 0.01 -pid_Td_A = 0.3 -ok_range_A = 4.0 -max_rise_temp_A = 10.0 -max_fall_temp_A = 10.0 -min_temp_A = 20.0 -max_temp_A = 250.0 -path_adc_A = /sys/bus/iio/devices/iio:device0/in_voltage0_raw -mosfet_A = 11 -onoff_A = False -prefix_A = T2 -max_power_A = 1.0 - -sensor_B = B57560G104F -pid_Kp_B = 0.1 -pid_Ti_B = 0.01 -pid_Td_B = 0.3 -ok_range_B = 4.0 -max_rise_temp_B = 10.0 -max_fall_temp_B = 10.0 -min_temp_B = 20.0 -max_temp_B = 250.0 -path_adc_B = /sys/bus/iio/devices/iio:device0/in_voltage3_raw -mosfet_B = 12 -onoff_B = False -prefix_B = T3 -max_power_B = 1.0 - -sensor_C = B57560G104F -pid_Kp_C = 0.1 -pid_Ti_C = 0.01 -pid_Td_C = 0.3 -ok_range_C = 4.0 -max_rise_temp_C = 10.0 -max_fall_temp_C = 10.0 -min_temp_C = 20.0 -max_temp_C = 250.0 -path_adc_C = /sys/bus/iio/devices/iio:device0/in_voltage2_raw -mosfet_C = 13 -onoff_C = False -prefix_C = T4 -max_power_C = 1.0 - -sensor_HBP = B57560G104F -pid_Kp_HBP = 0.1 -pid_Ti_HBP = 0.01 -pid_Td_HBP = 0.3 -ok_range_HBP = 4.0 -max_rise_temp_HBP = 10.0 -max_fall_temp_HBP = 10.0 -min_temp_HBP = 20.0 -max_temp_HBP = 250.0 -path_adc_HBP = /sys/bus/iio/devices/iio:device0/in_voltage6_raw -mosfet_HBP = 4 -onoff_HBP = False -prefix_HBP = B -max_power_HBP = 1.0 +# make things hot, can be connected to control units defined in [Temperature Control] +# NOTE: Safety parameters are important, make sure they are set correctly for your printer! +# When this config is parsed the 'active' flag is updated to reflect the +# available heaters for your Replicape version + +[[Heater-E]] +type = heater +mosfet = 5 +max_rise_temp = 10.0 +min_rise_temp = 0.1 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +max_power = 1.0 +sleep = 0.25 +prefix = T0 +temperature = Thermistor-E +#input = 0 +input = Control-E +active = False + +[[Heater-H]] +type = heater +mosfet = 3 +max_rise_temp = 10.0 +min_rise_temp = 0.1 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +max_power = 1.0 +sleep = 0.25 +prefix = T1 +temperature = Thermistor-H +input = Control-H +active = False + +[[Heater-A]] +type = heater +mosfet = 11 +max_rise_temp = 10.0 +min_rise_temp = 0.1 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +max_power = 1.0 +sleep = 0.25 +prefix = T2 +temperature = Thermistor-A +input = Control-A +active = False + +[[Heater-B]] +type = heater +mosfet = 12 +max_rise_temp = 10.0 +min_rise_temp = 0.1 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +max_power = 1.0 +sleep = 0.25 +prefix = T3 +temperature = Thermistor-B +input = Control-B +active = False + +[[Heater-C]] +type = heater +mosfet = 13 +max_rise_temp = 10.0 +min_rise_temp = 0.1 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +max_power = 1.0 +sleep = 0.25 +prefix = T4 +temperature = Thermistor-C +input = Control-C +active = False + +[[Heater-HBP]] +type = heater +mosfet = 4 +max_rise_temp = 10.0 +min_rise_temp = 0.1 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +max_power = 1.0 +sleep = 0.5 +prefix = B +temperature = Thermistor-HBP +input = Control-HBP +active = False [Endstops] # Which axis should be homed. diff --git a/redeem/Alarm.py b/redeem/Alarm.py index ed5dcd76..380aec95 100644 --- a/redeem/Alarm.py +++ b/redeem/Alarm.py @@ -39,6 +39,7 @@ class Alarm: IMPOSSIBLE_MOVE_ATTEMPTED = 9 STEPPER_FAULT = 10 # Error on a stepper ALARM_TEST = 11 # Testsignal, used during start-up + HEATER_RISING_SLOW = 12 # Temperture is rising too fast printer = None executor = None @@ -79,6 +80,11 @@ def execute(self): self.inform_listeners() Alarm.action_command("pause") Alarm.action_command("alarm_heater_rising_fast", self.message) + elif self.type == Alarm.HEATER_RISING_SLOW: + self.stop_print() + self.inform_listeners() + Alarm.action_command("pause") + Alarm.action_command("alarm_heater_rising_slow", self.message) elif self.type == Alarm.HEATER_FALLING_FAST: self.disable_heaters() self.inform_listeners() diff --git a/redeem/Cooler.py b/redeem/Cooler.py deleted file mode 100644 index 5c6a2dad..00000000 --- a/redeem/Cooler.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -A Cooler is a P controller -Author: Elias Bakken -email: elias(dot)bakken(at)gmail(dot)com -Website: http://www.thing-printer.com -License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html - - Redeem is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Redeem is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Redeem. If not, see . -""" - -from threading import Thread -import time -import logging - -class Cooler: - - def __init__(self, cold_end, fan, name, onoff_control): - """ Init """ - self.cold_end = cold_end - self.fan = fan - self.name = name # Name, used for debugging - self.current_temp = 0.0 - self.target_temp = 0.0 # Target temperature (Ts). Start off. - self.P = 1.0 # Proportional - self.onoff_control = onoff_control # If we use PID or ON/OFF control - self.max_speed = 1.0 - self.ok_range = 4.0 - - def set_max_speed(self, speed): - """ Set the desired max speed of the fan """ - self.max_speed = speed - - def set_target_temperature(self, temp): - """ Set the desired temperature of the extruder """ - self.target_temp = float(temp) - - def get_temperature(self): - """ get the temperature of the thermistor""" - return self.current_temp - - def is_target_temperature_reached(self): - """ Returns true if the target temperature is reached """ - if self.target_temp == 0: - return True - err = abs(self.current_temp - self.target_temp) - return err < self.ok_range - - def disable(self): - """ Stops the heater and the PID controller """ - self.enabled = False - # Wait for PID to stop - while self.disabled == False: - time.sleep(0.2) - # The PID loop has finished - self.fan.set_power(0.0) - - def enable(self): - """ Start the PID controller """ - self.enabled = True - self.disabled = False - self.t = Thread(target=self.keep_temperature, name=self.name) - self.t.daemon = True - self.t.start() - - def set_p_value(self, P): - """ Set values for Proportional, Integral, Derivative""" - self.P = P # Proportional - - def keep_temperature(self): - """ PID Thread that keeps the temperature stable """ - while self.enabled: - self.current_temp = self.cold_end.get_temperature() - error = self.target_temp-self.current_temp - - if self.onoff_control: - power = 1.0 if (self.P*error > 1.0) else 0.0 - else: - power = self.P*error # The formula for the PID (only P) - power = max(min(power, 1.0), 0.0) # Normalize to 0,1 - - # Invert the control since it'a a cooler - power = 1.0 - power - # Clamp the max speed - power = min(power, self.max_speed) - #logging.info("Err: {}, Pwr: {}".format(error, power)) - self.fan.set_value(power) - time.sleep(1) - self.disabled = True - - - diff --git a/redeem/Extruder.py b/redeem/Extruder.py deleted file mode 100644 index d07e48ee..00000000 --- a/redeem/Extruder.py +++ /dev/null @@ -1,260 +0,0 @@ -""" -Extruder file for Replicape. - -Author: Elias Bakken -email: elias(dot)bakken(at)gmail(dot)com -Website: http://www.thing-printer.com -License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html - - Redeem is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Redeem is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Redeem. If not, see . -""" - -from threading import Thread -import time -import logging -import numpy as np -from Alarm import Alarm - -class Heater(object): - """ - A heater element that must keep temperature, - either an extruder, a HBP or could even be a heated chamber - """ - def __init__(self, thermistor, mosfet, name, onoff_control): - """ Init """ - self.thermistor = thermistor - self.mosfet = mosfet - self.name = name # Name, used for debugging - self.current_temp = 0.0 - self.target_temp = 0.0 # Target temperature (Ts). Start off. - self.last_error = 0.0 # Previous error term, used in calculating the derivative - self.error_integral = 0.0 # Accumulated integral since the temperature came within the boudry - self.error_integral_limit = 100.0 # Integral temperature boundary - self.Kp = 0.1 - self.Ti = 100.0 - self.Td = 1.0 - self.onoff_control = onoff_control # If we use PID or ON/OFF control - self.ok_range = 4.0 - self.prefix = "" - self.sleep = 0.1 # Time to sleep between measurements - self.max_power = 1.0 # Maximum power - - self.min_temp_enabled = False # Temperature error limit - self.min_temp = 0 # If temperature falls below this point from the target, disable. - self.max_temp = 250.0 # Max temp that can be reached before disabling printer. - self.max_temp_rise = 4.0 # Fastest temp can rise pr measrement - self.max_temp_fall = 4.0 # Fastest temp can fall pr measurement - - self.extruder_error = False - if not thermistor.sensor: - logging.warning("Temperature sensor is not set, heater disabled") - self.extruder_error = True - - - - def set_target_temperature(self, temp): - """ Set the desired temperature of the extruder """ - self.min_temp_enabled = False - self.target_temp = float(temp) - - def get_temperature(self): - """ get the temperature of the thermistor""" - return np.average(self.temperatures[-self.avg:]) - - def get_temperature_raw(self): - """ Get unaveraged temp measurement """ - return self.temperatures[-1] - - def get_target_temperature(self): - """ get the temperature of the thermistor""" - return self.target_temp - - def is_target_temperature_reached(self): - """ Returns true if the target temperature is reached """ - if self.target_temp == 0: - return True - if self.current_temp == 0: - self.target_temp = 0 - err = abs(self.current_temp - self.target_temp) - reached = err < self.ok_range - return reached - - def is_temperature_stable(self, seconds=10): - """ Returns true if the temperature has been stable for n seconds """ - if len(self.temperatures) < int(seconds/self.sleep): - return False - if max(self.temperatures[-int(seconds/self.sleep):]) > (self.target_temp + self.ok_range): - return False - if min(self.temperatures[-int(seconds/self.sleep):]) < (self.target_temp - self.ok_range): - return False - return True - - def get_noise_magnitude(self, measurements=10): - """ Calculate and return the magnitude in the noise """ - measurements = min(measurements, len(self.temperatures)) - #logging.debug("Measurements: "+str(self.temperatures)) - avg = np.average(self.temperatures[-measurements:]) - mag = np.max(self.temperatures[-measurements:]) - #logging.debug("Avg: "+str(avg)) - #logging.debug("Mag: "+str(mag)) - return abs(mag-avg) - - def set_min_temp(self, min_temp): - """ Set the minimum temperature. If current temp goes below this, - sound the alarm """ - self.current_min_temp = min_temp - - def enable_min_temp(self): - """ Enable minimum temperature alarm """ - self.min_temp_enabled = True - logging.info("Min temp alarm enabled at {} for {}".format(self.min_temp, self.name)) - - def disable(self): - """ Stops the heater and the PID controller """ - self.target_temp = 0 - self.enabled = False - self.mosfet.set_power(0.0) - # Wait for PID to stop - self.t.join() - logging.debug("Heater {} disabled".format(self.name)) - self.mosfet.set_power(0.0) - self.last_error = 0.0 - self.error_integral = 0.0 - self.error_integral_limit = 100.0 - - def enable(self): - """ Start the PID controller """ - self.avg = max(int(1.0/self.sleep), 3) - self.error = 0 - self.errors = [0]*self.avg - self.average = 0 - self.averages = [0]*self.avg - self.prev_time = self.current_time = time.time() - self.current_temp = self.thermistor.get_temperature() - self.temperatures = [self.current_temp] - self.enabled = True - self.t = Thread(target=self.keep_temperature, name=self.name) - self.t.start() - - def keep_temperature(self): - """ PID Thread that keeps the temperature stable """ - try: - while self.enabled: - self.current_temp = self.thermistor.get_temperature() - self.temperatures.append(self.current_temp) - self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history - - self.error = self.target_temp-self.current_temp - self.errors.append(self.error) - self.errors.pop(0) - - if self.onoff_control: - if self.error > 0.0: - power = self.max_power - else: - power = 0.0 - else: - derivative = self.get_error_derivative() - integral = self.get_error_integral() - # The standard formula for the PID - power = self.Kp*(self.error + (1.0/self.Ti)*integral + self.Td*derivative) - power = max(min(power, self.max_power, 1.0), 0.0) # Normalize to 0, max - #if self.name =="E": - # logging.debug("Err: {0:.3f}, der: {1:.4f} int: {2:.2f}".format(self.error, derivative, integral)) - - # Run safety checks - self.time_diff = self.current_time-self.prev_time - self.prev_time = self.current_time - self.current_time = time.time() - - if not self.extruder_error: - self.check_temperature_error() - - # Set temp if temperature is OK - if not self.extruder_error and self.current_temp > 0: - self.mosfet.set_power(power) - else: - self.mosfet.set_power(0) - time.sleep(self.sleep) - finally: - # Disable this mosfet if anything goes wrong - self.mosfet.set_power(0) - - def get_error_derivative(self): - """ Get the derivative of the temperature""" - # Using temperature and not error for calculating derivative - # gets rid of the derivative kick. dT/dt - der = (self.temperatures[-2]-self.temperatures[-1])/self.sleep - self.averages.append(der) - if len(self.averages) > 11: - self.averages.pop(0) - #if self.name =="E": - # logging.debug(self.averages) - return np.average(self.averages) - - def get_error_integral(self): - """ Calculate and return the error integral """ - self.error_integral += self.error*self.sleep - # Avoid windup by clippping the integral part - # to teh reciprocal of the integral term - self.error_integral = np.clip(self.error_integral, 0, self.max_power*self.Ti/self.Kp) - return self.error_integral - - def check_temperature_error(self): - """ Check the temperatures, make sure they are sane. - Sound the alarm if something is wrong """ - if len(self.temperatures) < 2: - return - temp_delta = self.temperatures[-1]-self.temperatures[-2] - # Check that temperature is not rising too quickly - if temp_delta > self.max_temp_rise: - a = Alarm(Alarm.HEATER_RISING_FAST, - "Temperature rising too quickly ({} degrees) for {}".format(temp_delta, self.name)) - # Check that temperature is not falling too quickly - if temp_delta < -self.max_temp_fall: - a = Alarm(Alarm.HEATER_FALLING_FAST, - "Temperature falling too quickly ({} degrees) for {}".format(temp_delta, self.name)) - # Check that temperature has not fallen below a certain setpoint from target - if self.min_temp_enabled and self.current_temp < (self.target_temp - self.min_temp): - a = Alarm(Alarm.HEATER_TOO_COLD, - "Temperature below min set point ({} degrees) for {}".format(self.min_temp, self.name), - "Alarm: Heater {}".format(self.name)) - # Check if the temperature has gone beyond the max value - if self.current_temp > self.max_temp: - a = Alarm(Alarm.HEATER_TOO_HOT, - "Temperature beyond max ({} degrees) for {}".format(self.max_temp, self.name)) - # Check the time diff, only warn if something is off. - if self.time_diff > 4: - logging.warning("Heater time update large: " + - self.name + " temp: " + - str(self.current_temp) + " time delta: " + - str(self.current_time-self.prev_time)) - - - -class Extruder(Heater): - """ Subclass for Heater, this is an extruder """ - def __init__(self, smd, thermistor, mosfet, name, onoff_control): - Heater.__init__(self, thermistor, mosfet, name, onoff_control) - self.smd = smd - self.sleep = 0.25 - self.enable() - - -class HBP(Heater): - """ Subclass for heater, this is a Heated build platform """ - def __init__(self, thermistor, mosfet, onoff_control): - Heater.__init__(self, thermistor, mosfet, "HBP", onoff_control) - self.sleep = 0.5 # Heaters have more thermal mass - self.enable() diff --git a/redeem/Fan.py b/redeem/Fan.py new file mode 100644 index 00000000..5550815a --- /dev/null +++ b/redeem/Fan.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +""" +For running a fan. + +Author: Daryl Bond +email: daryl(dot)bond(at)hotmail(dot)com +Website: http://www.thing-printer.com +License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html + + Redeem is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Redeem is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Redeem. If not, see . +""" + +import time +from builtins import range +from PWM import PWM +from configobj import Section +import logging +from threading import Thread + +from TemperatureControl import Unit, ConstantControl + + +class Fan(Unit): + + def __init__(self, name, options, printer): + """ + channel : channel that this fan is on + fan_id : number of the fan + printer : description of this printer + """ + + self.name = name + self.options = options + self.printer = printer + + self.input = self.options["input"] + self.channel = int(self.options["channel"]) + self.force_disable = False + + self.printer.fans.append(self) + + self.counter += 1 + + return + + def connect(self, units): + self.input = self.get_unit(self.input, units) + + if self.options["add-to-M106"] == "True": + self.force_disable = True + if not isinstance(self.input, ConstantControl): + msg = "{} has a non-constant controller attached. For control by M106/M107 set config 'input' as a constant".format(self.name) + logging.error(msg) + raise RuntimeError(msg) + + self.printer.controlled_fans.append(self) + logging.info("Added {} to M106/M107".format(self.name)) + + + def set_PWM_frequency(self, value): + """ Set the amount of on-time from 0..1 """ + self.pwm_frequency = int(value) + PWM.set_frequency(value) + + def set_value(self, value): + """ Set the amount of on-time from 0..1 """ + self.value = value + PWM.set_value(value, self.channel) + return + + + def ramp_to(self, value, delay=0.01): + ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' + for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): + logging.debug("Fan value: "+str(w)) + self.set_value(w/255.0) + time.sleep(delay) + self.set_value(value) + + def run_controller(self): + """ follow a target PWM value 0..1""" + + while self.enabled: + self.set_value(self.input.get_power()) + time.sleep(1) + self.disabled = True + + def disable(self): + """ stops the controller """ + self.enabled = False + # Wait for controller to stop + while self.disabled == False: + time.sleep(0.2) + # The controller loop has finished + self.set_value(0.0) + + def enable(self): + """ starts the controller """ + if self.force_disable: + self.disabled = True + self.enabled = False + return + self.enabled = True + self.disabled = False + self.t = Thread(target=self.run_controller, name=self.name) + self.t.daemon = True + self.t.start() + return \ No newline at end of file diff --git a/redeem/Heater.py b/redeem/Heater.py new file mode 100644 index 00000000..dbdfc358 --- /dev/null +++ b/redeem/Heater.py @@ -0,0 +1,270 @@ +""" +Extruder file for Replicape. + +Author: Elias Bakken +email: elias(dot)bakken(at)gmail(dot)com +Website: http://www.thing-printer.com +License: GNU GPL v3: http://www.gnu.org/copyleft/gpl.html + + Redeem is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Redeem is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Redeem. If not, see . +""" + +from threading import Thread +import time +import logging +import numpy as np +from Alarm import Alarm + +from TemperatureControl import Unit, Control, PIDControl + +class Heater(Unit): + """ + A heater element that must keep temperature, + either an extruder, a HBP or could even be a heated chamber + """ + def __init__(self, name, options, printer): + """ Init """ + + self.name = name + self.options = options + self.printer = printer + + self.thermistor = self.options["temperature"] + self.mosfet = self.options["mosfet"] + self.prefix = self.options["prefix"] + self.sleep = float(self.options["sleep"]) # Time to sleep between measurements + + + self.max_power = float(self.options["max_power"]) # Maximum power + self.min_temp_enabled = False # Temperature error limit + self.min_temp = float(self.options["min_temp"]) # If temperature falls below this point from the target, disable. + self.max_temp = float(self.options["max_temp"]) # Max temp that can be reached before disabling printer. + self.max_temp_rise = float(self.options["max_rise_temp"]) # Fastest temp can rise pr measrement + self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor + self.max_temp_fall = float(self.options["max_fall_temp"]) # Fastest temp can fall pr measurement + + + self.input = self.options["input"] + + self.heater_error = False + + self.thermistor_temperatures = [] + self.control_temperatures = [] + + return + + def connect(self, units): + """ connect to sensors and control units""" + + # connect the thermistor + self.thermistor = self.get_unit(self.thermistor, units) + + # connect a MOSFET + self.mosfet = self.printer.mosfets[self.name.replace("Heater", "MOSFET")] + + # connect the controller + self.input = self.get_unit(self.input, units) + + return + + def initialise(self): + """ stuff to do after connecting""" + + if not self.thermistor.sensor: + logging.warning("{} temperature sensor is not set, heater disabled".format(self.name)) + self.heater_error = True + + # ensure the controller is one that allows feedback, i.e. not open loop + allow = False + if isinstance(self.input, Control): + if self.input.feedback_control: + allow = True + if not allow: + self.mosfet.set_power(0.0) + logging.error("{} has non-feedback control assigned, heater disabled".format(self.name)) + self.heater_error = True + + # if the thermistor is not the input to the controller driving this heater generate a warning + if self.thermistor != self.input.input: + logging.warning("{} control driven by {}".format(self.name, self.input.input.name)) + + # inherit the sleep timer from PID controller if that is what we are using + if isinstance(self.input, PIDControl): + self.sleep = self.input.sleep + + return + + def set_target_temperature(self, temp): + """ Set the target temperature of the controller """ + self.min_temp_enabled = False + self.input.target_temperature = float(temp) + + def get_temperature(self): + """ get the temperature of the thermistor and the control input""" + therm = np.average(self.thermistor_temperatures[-self.avg:]) + control = np.average(self.control_temperatures[-self.avg:]) + return therm, control + + def get_temperature_raw(self): + """ Get unaveraged temp measurement """ + return self.thermistor_temperatures[-1], self.control_temperatures[-1] + + def get_target_temperature(self): + """ get the target temperature""" + return self.input.target_temperature + + def is_target_temperature_reached(self): + """ Returns true if the target temperature is reached """ + + current_temp = self.control_temperatures[-1] + target_temp = self.input.target_temperature + + if target_temp == 0: + return True + if current_temp == 0: + self.input.target_temperature = 0 + target_temp = 0 + err = abs(current_temp - target_temp) + reached = err < self.input.ok_range + return reached + + def is_temperature_stable(self, seconds=10): + """ Returns true if the temperature has been stable for n seconds """ + target_temp = self.input.target_temperature + ok_range = self.input.ok_range + if len(self.control_temperatures) < int(seconds/self.sleep): + return False + if max(self.control_temperatures[-int(seconds/self.sleep):]) > (target_temp + ok_range): + return False + if min(self.control_temperatures[-int(seconds/self.sleep):]) < (target_temp - ok_range): + return False + return True + + def get_noise_magnitude(self, measurements=10): + """ Calculate and return the magnitude in the noise """ + measurements = min(measurements, len(self.temperatures)) + #logging.debug("Measurements: "+str(self.temperatures)) + avg = np.average(self.control_temperatures[-measurements:]) + mag = np.max(self.control_temperatures[-measurements:]) + #logging.debug("Avg: "+str(avg)) + #logging.debug("Mag: "+str(mag)) + return abs(mag-avg) + + def set_min_temp(self, min_temp): + """ Set the minimum temperature. If current temp goes below this, + sound the alarm """ + self.current_min_temp = min_temp + + def enable_min_temp(self): + """ Enable minimum temperature alarm """ + self.min_temp_enabled = True + logging.info("Min temp alarm enabled at {} for {}".format(self.min_temp, self.name)) + + def disable(self): + """ Stops the heater and the PID controller """ + self.input.target_temperature = 0 + self.enabled = False + self.mosfet.set_power(0.0) + # Wait for PID to stop + self.t.join() + logging.debug("{} disabled".format(self.name)) + self.mosfet.set_power(0.0) + self.input.reset() + + + def enable(self): + """ Start the PID controller """ + if self.heater_error: + self.enabled = False + return + self.avg = max(int(1.0/self.sleep), 3) + self.prev_time = self.current_time = time.time() + self.thermistor_temperatures = [self.thermistor.get_temperature()] + self.control_temperatures = [self.input.input.get_temperature()] + self.enabled = True + self.t = Thread(target=self.run_controller, name=self.name) + self.t.start() + + def run_controller(self): + """ thread to run the controller in """ + try: + while self.enabled: + + # get the controllers recommendation + power = self.input.get_power() + power = max(min(power, self.max_power, 1.0), 0.0) + + # get the attached thermistor temperature + therm_temp = self.thermistor.get_temperature() + self.thermistor_temperatures.append(therm_temp) + self.thermistor_temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history + + # get the controlling temperature + cntrl_temp = self.input.input.get_temperature() + self.control_temperatures.append(cntrl_temp) + self.control_temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history + + # Run safety checks + self.time_diff = self.current_time-self.prev_time + self.prev_time = self.current_time + self.current_time = time.time() + + if not self.heater_error: + self.check_temperature_error() + + # Set temp if temperature is OK + if not self.heater_error and therm_temp > 0: + self.mosfet.set_power(power) + else: + self.mosfet.set_power(0) + time.sleep(self.sleep) + finally: + # Disable this mosfet if anything goes wrong + self.mosfet.set_power(0) + + def check_temperature_error(self): + """ Check the temperatures, make sure they are sane. + Sound the alarm if something is wrong """ + temps = self.thermistor_temperatures + current_temp = temps[-1] + if len(temps) < 2: + return + temp_delta = temps[-1]-temps[-2] + # Check that temperature is not rising too quickly + if temp_delta > self.max_temp_rise: + a = Alarm(Alarm.HEATER_RISING_FAST, + "Temperature rising too quickly ({} degrees) for {}".format(temp_delta, self.name)) + # Check that temperature is not rising quickly enough when power is applied + if (temp_delta < self.min_temp_rise) and (self.mosfet.get_power() > 0): + a = Alarm(Alarm.HEATER_RISING_SLOW, + "Temperature rising too slowly ({} degrees) for {}".format(temp_delta, self.name)) + # Check that temperature is not falling too quickly + if temp_delta < -self.max_temp_fall: + a = Alarm(Alarm.HEATER_FALLING_FAST, + "Temperature falling too quickly ({} degrees) for {}".format(temp_delta, self.name)) + # Check that temperature has not fallen below a certain setpoint from target + if self.min_temp_enabled and self.current_temp < (self.target_temp - self.min_temp): + a = Alarm(Alarm.HEATER_TOO_COLD, + "Temperature below min set point ({} degrees) for {}".format(self.min_temp, self.name), + "Alarm: Heater {}".format(self.name)) + # Check if the temperature has gone beyond the max value + if self.current_temp > self.max_temp: + a = Alarm(Alarm.HEATER_TOO_HOT, + "Temperature beyond max ({} degrees) for {}".format(self.max_temp, self.name)) + # Check the time diff, only warn if something is off. + if self.time_diff > 4: + logging.warning("Heater time update large: " + + self.name + " temp: " + + str(current_temp) + " time delta: " + + str(self.current_time-self.prev_time)) diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 0effb504..9d41d37c 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -39,14 +39,14 @@ from Mosfet import Mosfet from Stepper import * from TemperatureSensor import * -import TemperatureControl +from TemperatureControl import * +from Fan import Fan +from Heater import Heater from Servo import Servo from EndStop import EndStop from USB import USB from Pipe import Pipe from Ethernet import Ethernet -from Extruder import Extruder, HBP -from Cooler import Cooler from Path import Path from PathPlanner import PathPlanner from Gcode import Gcode @@ -269,48 +269,30 @@ def __init__(self, config_location="/etc/redeem"): self.printer.cold_ends.append(ColdEnd(path, "ds18b20-"+str(i))) logging.info("Found Cold end "+str(i)+" on " + path) - # Make Mosfets, temperature sensors and extruders + # Make Mosfets and temperature sensors heaters = ["E", "H", "HBP"] if self.printer.config.reach_revision: heaters.extend(["A", "B", "C"]) + for e in heaters: - # Mosfets - channel = self.printer.config.getint("Heaters", "mosfet_"+e) - self.printer.mosfets[e] = Mosfet(channel) # Thermistors - adc = self.printer.config.get("Heaters", "path_adc_"+e) - if not self.printer.config.has_option("Heaters", "sensor_"+e): - sensor = self.printer.config.get("Heaters", "temp_chart_"+e) - logging.warning("Deprecated config option temp_chart_"+e+" use sensor_"+e+" instead.") - else: - sensor = self.printer.config.get("Heaters", "sensor_"+e) - self.printer.thermistors[e] = TemperatureSensor(adc, 'MOSFET '+e, sensor) - self.printer.thermistors[e].printer = printer - - # Extruders - onoff = self.printer.config.getboolean('Heaters', 'onoff_'+e) - prefix = self.printer.config.get('Heaters', 'prefix_'+e) - if e != "HBP": - self.printer.heaters[e] = Extruder( - self.printer.steppers[e], - self.printer.thermistors[e], - self.printer.mosfets[e], e, onoff) - else: - self.printer.heaters[e] = HBP( - self.printer.thermistors[e], - self.printer.mosfets[e], onoff) - self.printer.heaters[e].prefix = prefix - self.printer.heaters[e].Kp = self.printer.config.getfloat('Heaters', 'pid_Kp_'+e) - self.printer.heaters[e].Ti = self.printer.config.getfloat('Heaters', 'pid_Ti_'+e) - self.printer.heaters[e].Td = self.printer.config.getfloat('Heaters', 'pid_Td_'+e) - - # Min/max settings - self.printer.heaters[e].min_temp = self.printer.config.getfloat('Heaters', 'min_temp_'+e) - self.printer.heaters[e].max_temp = self.printer.config.getfloat('Heaters', 'max_temp_'+e) - self.printer.heaters[e].max_temp_rise = self.printer.config.getfloat('Heaters', 'max_rise_temp_'+e) - self.printer.heaters[e].max_temp_fall = self.printer.config.getfloat('Heaters', 'max_fall_temp_'+e) - self.printer.heaters[e].max_power = self.printer.config.getfloat('Heaters', 'max_power_'+e) - + name = "Thermistor-{}".format(e) + adc = self.printer.config.get("Thermistors", name, "path_adc") + sensor = self.printer.config.get("Thermistors", name, "sensor") + self.printer.thermistors[name] = TemperatureSensor(adc, name, sensor) + self.printer.thermistors[name].printer = printer + self.printer.config["Thermistors"][name]["active"] = True + + # Mosfets + name = "Heater-{}".format(e) + channel = self.printer.config.getint("Heaters", name, "mosfet") + self.printer.mosfets["MOSFET-{}".format(e)] = Mosfet(channel) + self.printer.config["Heaters"][name]["active"] = True + + # Control + name = "Control-{}".format(e) + self.printer.config["Temperature Control"][name]["active"] = True + # update the channel information for fans if self.revision == "00A3": @@ -325,17 +307,55 @@ def __init__(self, config_location="/etc/redeem"): if printer.config.reach_revision == "00A0": for i, c in enumerate([14,15,7]): self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - + + # build and connect all of the temperature control infrastructure self.printer.controlled_fans = [] self.printer.fans = [] + self.printer.heaters = {} # fans and ... generated and added to printer in build_temperature_control - TemperatureControl.build_temperature_control(self.printer) + control_units = {"alias":Alias, "difference":Difference, + "maximum":Maximum, "minimum":Minimum, + "constant-control":ConstantControl, + "on-off-control":OnOffControl, + "pid-control":PIDControl, + "proportional-control":ProportionalControl, + "fan":Fan, + "heater":Heater} + + units = {} + for section in ["Temperature Control", "Fans", "Heaters"]: + cfg = self.printer.config[section] - # turn on the fans and ... + # generate units + for name, options in cfg.items(): + if not isinstance(options, Section): + continue + + # check the unit is active + active = options["active"] + if (not active) or (active == "False"): + continue + + input_type = options["type"] + unit = control_units[input_type](name, options, self.printer) + units[name] = unit + + # connect units + for name, unit in units.items(): + unit.connect(units) + + # initialise units + for name, unit in units.items(): + unit.initialise() + + # turn on the fans and heaters for fan in self.printer.fans: fan.enable() + + for heater in self.printer.heaters: + heater.enable() ####################################################################### # SERVO diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index b917a516..e0061c36 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -40,15 +40,21 @@ class Unit: counter = 0 def get_unit(self, name, units): - """ retrieve a thermistor, cold_end, or unit""" + """ retrieve a thermistor, cold_end, mosfet, or unit""" + + # check if we already have what we are looking for + if isinstance(name, Unit): + return name # check units, thermistors, and cold ends if name in units: return units[name] - elif "thermistor-" in name: - g = name.replace("thermistor-","") - if g in self.printer.thermistors: - return self.printer.thermistors[g] + elif "Thermistor" in name: + if name in self.printer.thermistors: + return self.printer.thermistors[name] + elif "MOSFET" in name: + if name in self.printer.mosfets: + return self.printer.mosfets[name] elif "ds18b20" in name: for sensor in self.printer.cold_ends: if name == sensor.name: @@ -61,6 +67,13 @@ def get_unit(self, name, units): return + + def initialise(self): + """ stuff to do after connecting""" + + # + + return class Alias(Unit): @@ -142,6 +155,8 @@ def __init__(self, name, options, printer): if "output" in options: self.output = options["output"] + self.power = 0.0 + self.get_options() self.counter += 1 @@ -153,10 +168,14 @@ def connect(self, units): if self.output: self.output = self.get_unit(self.output, units) self.output.input = self + + return class ConstantControl(Control): + feedback_control = False + def get_options(self): self.power = int(self.options['input'])/255.0 return @@ -166,6 +185,8 @@ def get_power(self): class OnOffControl(Control): + + feedback_control = True def get_options(self): self.on_temperature = float(self.options['on_temperature']) @@ -181,15 +202,17 @@ def get_power(self): temp = self.input.get_temperature() - if temp >= self.on_temperature: + if temp <= self.on_temperature: self.power = self.on_power - elif temp <= self.off_temperature: + elif temp >= self.off_temperature: self.power = self.off_power return self.power class ProportionalControl(Control): + + feedback_control = True def get_options(self): """ Init """ @@ -217,124 +240,74 @@ def get_power(self): power = max(power, self.min_speed) return power - -### -class Fan(Unit): +class PIDControl(Control): - def __init__(self, name, options, printer): - """ - channel : channel that this fan is on - fan_id : number of the fan - printer : description of this printer - """ + feedback_control = True + + def get_options(self): - self.name = name - self.options = options - self.printer = printer + self.Kp = float(self.options['pid_Kp']) + self.Ti = float(self.options['pid_Ti']) + self.Td = float(self.options['pid_Td']) + self.ok_range = float(self.options['ok_range']) + self.sleep = float(self.options['sleep']) - self.input = self.options["input"] - self.channel = int(self.options["channel"]) - self.force_disable = False + return - self.printer.fans.append(self) + def initialise(self): - self.counter += 1 - - return + self.avg = max(int(1.0/self.sleep), 3) + self.error = 0 + self.errors = [0]*self.avg + self.averages = [0]*self.avg - def connect(self, units): - self.input = self.get_unit(self.input, units) + current_temp = self.input.get_temperature() + self.temperatures = [current_temp] - if self.options["add-to-M106"] == "True": - self.force_disable = True - if not isinstance(self.input, ConstantControl): - msg = "{} has a non-constant controller attached. For control by M106/M107 set config 'input' as a constant".format(self.name) - logging.error(msg) - raise RuntimeError(msg) - - self.printer.controlled_fans.append(self) - logging.info("Added {} to M106/M107".format(self.name)) + self.error_integral = 0.0 # Accumulated integral since the temperature came within the boudry + self.error_integral_limit = 100.0 # Integral temperature boundary + + def get_power(self): + + current_temp = self.input.get_temperature() + self.temperatures.append(current_temp) + self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history - def set_PWM_frequency(self, value): - """ Set the amount of on-time from 0..1 """ - self.pwm_frequency = int(value) - PWM.set_frequency(value) - - def set_value(self, value): - """ Set the amount of on-time from 0..1 """ - self.value = value - PWM.set_value(value, self.channel) - return - + self.error = self.target_temp-current_temp + self.errors.append(self.error) + self.errors.pop(0) - def ramp_to(self, value, delay=0.01): - ''' Set the fan/light value to the given value, in degree, with the given speed in deg / sec ''' - for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): - logging.debug("Fan value: "+str(w)) - self.set_value(w/255.0) - time.sleep(delay) - self.set_value(value) + derivative = self.get_error_derivative() + integral = self.get_error_integral() + # The standard formula for the PID + power = self.Kp*(self.error + (1.0/self.Ti)*integral + self.Td*derivative) + power = max(min(power, self.max_power, 1.0), 0.0) # Normalize to 0, max - def run_controller(self): - """ follow a target PWM value 0..1""" + return power - while self.enabled: - self.set_value(self.input.get_power()) - time.sleep(1) - self.disabled = True - - def disable(self): - """ stops the controller """ - self.enabled = False - # Wait for controller to stop - while self.disabled == False: - time.sleep(0.2) - # The controller loop has finished - self.set_value(0.0) + def get_error_derivative(self): + """ Get the derivative of the temperature""" + # Using temperature and not error for calculating derivative + # gets rid of the derivative kick. dT/dt + der = (self.temperatures[-2]-self.temperatures[-1])/self.sleep + self.averages.append(der) + if len(self.averages) > 11: + self.averages.pop(0) + return np.average(self.averages) - def enable(self): - """ starts the controller """ - if self.force_disable: - self.disabled = True - self.enabled = False - return - self.enabled = True - self.disabled = False - self.t = Thread(target=self.run_controller, name=self.name) - self.t.daemon = True - self.t.start() + def get_error_integral(self): + """ Calculate and return the error integral """ + self.error_integral += self.error*self.sleep + # Avoid windup by clippping the integral part + # to the reciprocal of the integral term + self.error_integral = np.clip(self.error_integral, 0, self.max_power*self.Ti/self.Kp) + return self.error_integral + + def reset(self): + + self.error_integral = 0.0 + return - -#============================================================================== -# FUNCTIONS -#============================================================================== - -def build_temperature_control(printer): - """ build the network linking sensors to controllers """ - - control_units = {"alias":Alias, "difference":Difference, - "maximum":Maximum, "minimum":Minimum, - "constant-control":ConstantControl, - "on-off-control":OnOffControl, - "proportional-control":ProportionalControl, - "fan":Fan} - - units = {} - for section in ["Temperature Control", "Fans"]: #, Heaters]: TODO - cfg = printer.config[section] - - # generate units - for name, options in cfg.items(): - if not isinstance(options, Section): - continue - input_type = options["type"] - unit = control_units[input_type](name, options, printer) - units[name] = unit - - # connect units - for name, unit in units.items(): - unit.connect(units) - - return + diff --git a/redeem/TemperatureSensor.py b/redeem/TemperatureSensor.py index 451329af..493cbc47 100644 --- a/redeem/TemperatureSensor.py +++ b/redeem/TemperatureSensor.py @@ -40,10 +40,10 @@ class TemperatureSensor: mutex = Lock() - def __init__(self, pin, heater_name, sensorIdentifier): + def __init__(self, pin, name, sensorIdentifier): self.pin = pin - self.heater = heater_name + self.name = name self.sensorIdentifier = sensorIdentifier self.maxAdc = 4095.0 @@ -51,7 +51,7 @@ def __init__(self, pin, heater_name, sensorIdentifier): found = False for s in TemperatureSensorConfigs.thermistors_shh: if s[0] == self.sensorIdentifier: - self.sensor = Thermistor(pin, s, self.heater) + self.sensor = Thermistor(pin, s, self.name) found = True break @@ -60,7 +60,7 @@ def __init__(self, pin, heater_name, sensorIdentifier): if p[0] == self.sensorIdentifier: logging.warning("PT100 temperature sensor support is experimental at this stage.") """ Experimental solution """ - self.sensor = PT100(pin, p, self.heater) + self.sensor = PT100(pin, p, self.name) found = True break @@ -69,7 +69,7 @@ def __init__(self, pin, heater_name, sensorIdentifier): if p[0] == self.sensorIdentifier: logging.warning("Tboard sensors are experimental") """ Not working yet. No known hardware solution """ - self.sensor = Tboard(pin, p, self.heater) + self.sensor = Tboard(pin, p, self.name) found = True break diff --git a/redeem/gcodes/M105.py b/redeem/gcodes/M105.py index ae5c08a9..f25777fe 100644 --- a/redeem/gcodes/M105.py +++ b/redeem/gcodes/M105.py @@ -23,9 +23,9 @@ def format_temperature(heater, prefix): Returns : for a given heater and prefix. Temperature values are formatted as integers. """ - temperature = self.printer.heaters[heater].get_temperature() + thermistor_temperature, control_temperature = self.printer.heaters[heater].get_temperature() target = self.printer.heaters[heater].get_target_temperature() - return "{0}:{1:.1f}/{2:.1f}".format(prefix, temperature, target) + return "{0}:{1:.1f}/{2:.1f}({3:0.1f})".format(prefix, control_temperature, target, thermistor_temperature) # Cura expects the temperature from the first current_tool = self.printer.current_tool From 7c7daa319fefd76a5613d1a08b1f50895eca5bde Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 12 Jan 2018 13:54:20 +0000 Subject: [PATCH 12/27] Added Safety units to the control scheme. Each safety unit is connected to one heater, however, multiple safety units can be attached to one heater. Runs with default settings but is otherwise completely untested. --- configs/default.cfg | 168 ++++++++++++++++++++++------------- redeem/Alarm.py | 10 +-- redeem/Heater.py | 113 ++++++++--------------- redeem/Redeem.py | 59 ++++++------ redeem/TemperatureControl.py | 123 +++++++++++++++++++++++-- 5 files changed, 297 insertions(+), 176 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index 5faa782f..c3502312 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -225,62 +225,143 @@ e_axis_active = True [[Control-E]] type = pid-control input = Thermistor-E +target_temperature = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 sleep = 0.25 -active = False + [[Control-H]] type = pid-control input = Thermistor-H +target_temperature = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 sleep = 0.25 -active = False + [[Control-A]] type = pid-control input = Thermistor-A +target_temperature = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 sleep = 0.25 -active = False + [[Control-B]] type = pid-control input = Thermistor-B +target_temperature = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 sleep = 0.25 -active = False + [[Control-C]] type = pid-control input = Thermistor-C +target_temperature = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 sleep = 0.25 -active = False + [[Control-HBP]] type = pid-control input = Thermistor-HBP +target_temperature = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 sleep = 0.5 -active = False + +# safety limits for heaters +# multiple safety units may be attached to each heater +# each safety only has one heater +# these allow safety limits to be written for each temperature sensor + +# min_rise_* : when power is supplied to the heater, expect min_rise_temp +# temperature rise per second, as long as temp is min_rise_offset below the +# heaters target temperature + +[[Safety-E]] +type = safety +max_rise_temp = 10.0 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_temp = 0.1 +min_rise_offset = 20 +input = Thermistor-E +heater = Heater-E + +[[Safety-H]] +type = safety +max_rise_temp = 10.0 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_temp = 0.1 +min_rise_offset = 20 +input = Thermistor-H +heater = Heater-H + +[[Safety-A]] +type = safety +max_rise_temp = 10.0 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_temp = 0.1 +min_rise_offset = 20 +input = Thermistor-A +heater = Heater-A + +[[Safety-B]] +type = safety +max_rise_temp = 10.0 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_temp = 0.1 +min_rise_offset = 20 +input = Thermistor-B +heater = Heater-B + +[[Safety-C]] +type = safety +max_rise_temp = 10.0 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_temp = 0.1 +min_rise_offset = 20 +input = Thermistor-C +heater = Heater-C + +[[Safety-HBP]] +type = safety +max_rise_temp = 10.0 +max_fall_temp = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_temp = 0.1 +min_rise_offset = 20 +input = Thermistor-HBP +heater = Heater-HBP + [Thermistors] @@ -292,32 +373,32 @@ active = False [[Thermistor-E]] sensor = B57560G104F path_adc = /sys/bus/iio/devices/iio:device0/in_voltage4_raw -active = False + [[Thermistor-H]] sensor = B57560G104F path_adc = /sys/bus/iio/devices/iio:device0/in_voltage5_raw -active = False + [[Thermistor-A]] sensor = B57560G104F path_adc = /sys/bus/iio/devices/iio:device0/in_voltage0_raw -active = False + [[Thermistor-B]] sensor = B57560G104F path_adc = /sys/bus/iio/devices/iio:device0/in_voltage3_raw -active = False + [[Thermistor-C]] sensor = B57560G104F path_adc = /sys/bus/iio/devices/iio:device0/in_voltage2_raw -active = False + [[Thermistor-HBP]] sensor = B57560G104F path_adc = /sys/bus/iio/devices/iio:device0/in_voltage6_raw -active = False + [Fans] @@ -331,127 +412,92 @@ type = fan input = 0 add-to-M106 = False channel = 0 -active = True [[Fan-1]] type=fan input = 0 add-to-M106 = False channel = 0 -active = True [[Fan-2]] type = fan input = 0 add-to-M106 = False channel = 0 -active = True [[Fan-3]] type = fan input = 0 add-to-M106 = False channel = 0 -active = True [Heaters] # make things hot, can be connected to control units defined in [Temperature Control] -# NOTE: Safety parameters are important, make sure they are set correctly for your printer! -# When this config is parsed the 'active' flag is updated to reflect the -# available heaters for your Replicape version +# can have multiple safety units assigned to one heater i.e. safety = Safety-1, Safety-2 [[Heater-E]] type = heater mosfet = 5 -max_rise_temp = 10.0 -min_rise_temp = 0.1 -max_fall_temp = 10.0 -min_temp = 20.0 -max_temp = 250.0 max_power = 1.0 sleep = 0.25 prefix = T0 -temperature = Thermistor-E #input = 0 input = Control-E -active = False +safety = Safety-E + [[Heater-H]] type = heater mosfet = 3 -max_rise_temp = 10.0 -min_rise_temp = 0.1 -max_fall_temp = 10.0 -min_temp = 20.0 -max_temp = 250.0 max_power = 1.0 sleep = 0.25 prefix = T1 -temperature = Thermistor-H input = Control-H -active = False +safety = Safety-H + [[Heater-A]] type = heater mosfet = 11 -max_rise_temp = 10.0 -min_rise_temp = 0.1 -max_fall_temp = 10.0 -min_temp = 20.0 -max_temp = 250.0 max_power = 1.0 sleep = 0.25 prefix = T2 -temperature = Thermistor-A input = Control-A -active = False +safety = Safety-A + [[Heater-B]] type = heater mosfet = 12 -max_rise_temp = 10.0 -min_rise_temp = 0.1 -max_fall_temp = 10.0 -min_temp = 20.0 -max_temp = 250.0 max_power = 1.0 sleep = 0.25 prefix = T3 -temperature = Thermistor-B input = Control-B -active = False +safety = Safety-B + [[Heater-C]] type = heater mosfet = 13 -max_rise_temp = 10.0 -min_rise_temp = 0.1 -max_fall_temp = 10.0 -min_temp = 20.0 -max_temp = 250.0 max_power = 1.0 sleep = 0.25 prefix = T4 -temperature = Thermistor-C input = Control-C -active = False +safety = Safety-C + [[Heater-HBP]] type = heater mosfet = 4 -max_rise_temp = 10.0 -min_rise_temp = 0.1 -max_fall_temp = 10.0 -min_temp = 20.0 -max_temp = 250.0 max_power = 1.0 sleep = 0.5 prefix = B -temperature = Thermistor-HBP input = Control-HBP -active = False +safety = Safety-HBP + + [Endstops] # Which axis should be homed. diff --git a/redeem/Alarm.py b/redeem/Alarm.py index 380aec95..f0e477a4 100644 --- a/redeem/Alarm.py +++ b/redeem/Alarm.py @@ -80,16 +80,16 @@ def execute(self): self.inform_listeners() Alarm.action_command("pause") Alarm.action_command("alarm_heater_rising_fast", self.message) - elif self.type == Alarm.HEATER_RISING_SLOW: - self.stop_print() - self.inform_listeners() - Alarm.action_command("pause") - Alarm.action_command("alarm_heater_rising_slow", self.message) elif self.type == Alarm.HEATER_FALLING_FAST: self.disable_heaters() self.inform_listeners() Alarm.action_command("pause") Alarm.action_command("alarm_heater_falling_fast", self.message) + elif self.type == Alarm.HEATER_RISING_SLOW: + self.stop_print() + self.inform_listeners() + Alarm.action_command("pause") + Alarm.action_command("alarm_heater_rising_slow", self.message) elif self.type == Alarm.STEPPER_FAULT: self.inform_listeners() Alarm.action_command("pause") diff --git a/redeem/Heater.py b/redeem/Heater.py index dbdfc358..015d451a 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -40,49 +40,53 @@ def __init__(self, name, options, printer): self.options = options self.printer = printer - self.thermistor = self.options["temperature"] self.mosfet = self.options["mosfet"] self.prefix = self.options["prefix"] self.sleep = float(self.options["sleep"]) # Time to sleep between measurements + self.safety = [s.strip() for s in options["safety"].split(",")] self.max_power = float(self.options["max_power"]) # Maximum power self.min_temp_enabled = False # Temperature error limit - self.min_temp = float(self.options["min_temp"]) # If temperature falls below this point from the target, disable. - self.max_temp = float(self.options["max_temp"]) # Max temp that can be reached before disabling printer. - self.max_temp_rise = float(self.options["max_rise_temp"]) # Fastest temp can rise pr measrement - self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor - self.max_temp_fall = float(self.options["max_fall_temp"]) # Fastest temp can fall pr measurement - self.input = self.options["input"] self.heater_error = False - self.thermistor_temperatures = [] - self.control_temperatures = [] + self.temperatures = [] return def connect(self, units): """ connect to sensors and control units""" - - # connect the thermistor - self.thermistor = self.get_unit(self.thermistor, units) # connect a MOSFET self.mosfet = self.printer.mosfets[self.name.replace("Heater", "MOSFET")] # connect the controller self.input = self.get_unit(self.input, units) + + #connect the safety + for i, s in enumerate(self.safety): + self.safety[i] = self.get_unit(s, units) return def initialise(self): """ stuff to do after connecting""" - if not self.thermistor.sensor: - logging.warning("{} temperature sensor is not set, heater disabled".format(self.name)) + # inherit the sleep timer from PID controller if that is what we are using + if isinstance(self.input, PIDControl): + self.sleep = self.input.sleep + + return + + def check(self): + """ run checks""" + + if not self.safety: + self.mosfet.set_power(0.0) + logging.warning("{} has no safety connected, heater disabled".format(self.name)) self.heater_error = True # ensure the controller is one that allows feedback, i.e. not open loop @@ -95,14 +99,6 @@ def initialise(self): logging.error("{} has non-feedback control assigned, heater disabled".format(self.name)) self.heater_error = True - # if the thermistor is not the input to the controller driving this heater generate a warning - if self.thermistor != self.input.input: - logging.warning("{} control driven by {}".format(self.name, self.input.input.name)) - - # inherit the sleep timer from PID controller if that is what we are using - if isinstance(self.input, PIDControl): - self.sleep = self.input.sleep - return def set_target_temperature(self, temp): @@ -112,13 +108,11 @@ def set_target_temperature(self, temp): def get_temperature(self): """ get the temperature of the thermistor and the control input""" - therm = np.average(self.thermistor_temperatures[-self.avg:]) - control = np.average(self.control_temperatures[-self.avg:]) - return therm, control + return np.average(self.temperatures[-self.avg:]) def get_temperature_raw(self): """ Get unaveraged temp measurement """ - return self.thermistor_temperatures[-1], self.control_temperatures[-1] + return self.temperatures[-1] def get_target_temperature(self): """ get the target temperature""" @@ -127,7 +121,7 @@ def get_target_temperature(self): def is_target_temperature_reached(self): """ Returns true if the target temperature is reached """ - current_temp = self.control_temperatures[-1] + current_temp = self.temperatures[-1] target_temp = self.input.target_temperature if target_temp == 0: @@ -143,11 +137,11 @@ def is_temperature_stable(self, seconds=10): """ Returns true if the temperature has been stable for n seconds """ target_temp = self.input.target_temperature ok_range = self.input.ok_range - if len(self.control_temperatures) < int(seconds/self.sleep): + if len(self.temperatures) < int(seconds/self.sleep): return False - if max(self.control_temperatures[-int(seconds/self.sleep):]) > (target_temp + ok_range): + if max(self.temperatures[-int(seconds/self.sleep):]) > (target_temp + ok_range): return False - if min(self.control_temperatures[-int(seconds/self.sleep):]) < (target_temp - ok_range): + if min(self.temperatures[-int(seconds/self.sleep):]) < (target_temp - ok_range): return False return True @@ -155,8 +149,8 @@ def get_noise_magnitude(self, measurements=10): """ Calculate and return the magnitude in the noise """ measurements = min(measurements, len(self.temperatures)) #logging.debug("Measurements: "+str(self.temperatures)) - avg = np.average(self.control_temperatures[-measurements:]) - mag = np.max(self.control_temperatures[-measurements:]) + avg = np.average(self.temperatures[-measurements:]) + mag = np.max(self.temperatures[-measurements:]) #logging.debug("Avg: "+str(avg)) #logging.debug("Mag: "+str(mag)) return abs(mag-avg) @@ -190,8 +184,7 @@ def enable(self): return self.avg = max(int(1.0/self.sleep), 3) self.prev_time = self.current_time = time.time() - self.thermistor_temperatures = [self.thermistor.get_temperature()] - self.control_temperatures = [self.input.input.get_temperature()] + self.temperatures = [self.input.input.get_temperature()] self.enabled = True self.t = Thread(target=self.run_controller, name=self.name) self.t.start() @@ -205,15 +198,10 @@ def run_controller(self): power = self.input.get_power() power = max(min(power, self.max_power, 1.0), 0.0) - # get the attached thermistor temperature - therm_temp = self.thermistor.get_temperature() - self.thermistor_temperatures.append(therm_temp) - self.thermistor_temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history - # get the controlling temperature - cntrl_temp = self.input.input.get_temperature() - self.control_temperatures.append(cntrl_temp) - self.control_temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history + temp = self.input.input.get_temperature() + self.temperatures.append(cntrl_temp) + self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history # Run safety checks self.time_diff = self.current_time-self.prev_time @@ -224,7 +212,7 @@ def run_controller(self): self.check_temperature_error() # Set temp if temperature is OK - if not self.heater_error and therm_temp > 0: + if not self.heater_error: self.mosfet.set_power(power) else: self.mosfet.set_power(0) @@ -234,37 +222,8 @@ def run_controller(self): self.mosfet.set_power(0) def check_temperature_error(self): - """ Check the temperatures, make sure they are sane. - Sound the alarm if something is wrong """ - temps = self.thermistor_temperatures - current_temp = temps[-1] - if len(temps) < 2: - return - temp_delta = temps[-1]-temps[-2] - # Check that temperature is not rising too quickly - if temp_delta > self.max_temp_rise: - a = Alarm(Alarm.HEATER_RISING_FAST, - "Temperature rising too quickly ({} degrees) for {}".format(temp_delta, self.name)) - # Check that temperature is not rising quickly enough when power is applied - if (temp_delta < self.min_temp_rise) and (self.mosfet.get_power() > 0): - a = Alarm(Alarm.HEATER_RISING_SLOW, - "Temperature rising too slowly ({} degrees) for {}".format(temp_delta, self.name)) - # Check that temperature is not falling too quickly - if temp_delta < -self.max_temp_fall: - a = Alarm(Alarm.HEATER_FALLING_FAST, - "Temperature falling too quickly ({} degrees) for {}".format(temp_delta, self.name)) - # Check that temperature has not fallen below a certain setpoint from target - if self.min_temp_enabled and self.current_temp < (self.target_temp - self.min_temp): - a = Alarm(Alarm.HEATER_TOO_COLD, - "Temperature below min set point ({} degrees) for {}".format(self.min_temp, self.name), - "Alarm: Heater {}".format(self.name)) - # Check if the temperature has gone beyond the max value - if self.current_temp > self.max_temp: - a = Alarm(Alarm.HEATER_TOO_HOT, - "Temperature beyond max ({} degrees) for {}".format(self.max_temp, self.name)) - # Check the time diff, only warn if something is off. - if self.time_diff > 4: - logging.warning("Heater time update large: " + - self.name + " temp: " + - str(current_temp) + " time delta: " + - str(self.current_time-self.prev_time)) + """ for errors according to the attached safety units """ + + for s in self.safety: + s.set_min_temp_enabled(self.min_temp_enabled) + s.run_safety_checks() diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 9d41d37c..49aee9a5 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -268,12 +268,33 @@ def __init__(self, config_location="/etc/redeem"): for i, path in enumerate(paths): self.printer.cold_ends.append(ColdEnd(path, "ds18b20-"+str(i))) logging.info("Found Cold end "+str(i)+" on " + path) + + # update the channel information for fans + if self.revision == "00A3": + for i, c in enumerate([0,1,2]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c + elif self.revision == "0A4A": + for i, c in enumerate([8,9,10]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c + elif self.revision in ["00B1", "00B2", "00B3", "0B3A"]: + for i, c in enumerate([7,8,9,10]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c + if printer.config.reach_revision == "00A0": + for i, c in enumerate([14,15,7]): + self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - # Make Mosfets and temperature sensors + + # define the inputs/outputs available on this board + # also define those that are NOT available for later use... + exclude = [] heaters = ["E", "H", "HBP"] + extra = ["A", "B", "C"] if self.printer.config.reach_revision: - heaters.extend(["A", "B", "C"]) + heaters.extend(extra) + else: + exclude = extra + # Make Mosfets and thermistors for e in heaters: # Thermistors name = "Thermistor-{}".format(e) @@ -281,47 +302,26 @@ def __init__(self, config_location="/etc/redeem"): sensor = self.printer.config.get("Thermistors", name, "sensor") self.printer.thermistors[name] = TemperatureSensor(adc, name, sensor) self.printer.thermistors[name].printer = printer - self.printer.config["Thermistors"][name]["active"] = True # Mosfets name = "Heater-{}".format(e) channel = self.printer.config.getint("Heaters", name, "mosfet") self.printer.mosfets["MOSFET-{}".format(e)] = Mosfet(channel) - self.printer.config["Heaters"][name]["active"] = True - - # Control - name = "Control-{}".format(e) - self.printer.config["Temperature Control"][name]["active"] = True - # update the channel information for fans - if self.revision == "00A3": - for i, c in enumerate([0,1,2]): - self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - elif self.revision == "0A4A": - for i, c in enumerate([8,9,10]): - self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - elif self.revision in ["00B1", "00B2", "00B3", "0B3A"]: - for i, c in enumerate([7,8,9,10]): - self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - if printer.config.reach_revision == "00A0": - for i, c in enumerate([14,15,7]): - self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - # build and connect all of the temperature control infrastructure self.printer.controlled_fans = [] self.printer.fans = [] self.printer.heaters = {} - # fans and ... generated and added to printer in build_temperature_control + # available control unit types, see TemperatureControl.py control_units = {"alias":Alias, "difference":Difference, "maximum":Maximum, "minimum":Minimum, "constant-control":ConstantControl, "on-off-control":OnOffControl, "pid-control":PIDControl, "proportional-control":ProportionalControl, - "fan":Fan, - "heater":Heater} + "fan":Fan, "heater":Heater, "safety":Safety} units = {} for section in ["Temperature Control", "Fans", "Heaters"]: @@ -332,9 +332,8 @@ def __init__(self, config_location="/etc/redeem"): if not isinstance(options, Section): continue - # check the unit is active - active = options["active"] - if (not active) or (active == "False"): + e = name.split('-')[-1] + if e in exclude: continue input_type = options["type"] @@ -348,6 +347,10 @@ def __init__(self, config_location="/etc/redeem"): # initialise units for name, unit in units.items(): unit.initialise() + + # run checks + for name, unit in units.items(): + unit.check() # turn on the fans and heaters diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index e0061c36..6705c09d 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -30,6 +30,9 @@ import logging from threading import Thread +from TemperatureSensor import TemperatureSensor +from ColdEnd import ColdEnd + #============================================================================== # CLASSES #============================================================================== @@ -70,9 +73,10 @@ def get_unit(self, name, units): def initialise(self): """ stuff to do after connecting""" + return - # - + def check(self): + """ run any checks that need to be performed after full initialisation""" return @@ -143,6 +147,110 @@ def get_temperature(self): return min(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) +class Safety(Unit): + + def __init__(self, name, options, printer): + self.name = name + self.options = options + self.printer = printer + + self.input = options["input"] + self.heater = options["heater"] + + self.min_temp = float(self.options["min_temp"]) # If temperature falls below this point from the target, disable. + self.max_temp = float(self.options["max_temp"]) # Max temp that can be reached before disabling printer. + self.max_temp_rise = float(self.options["max_rise_temp"]) # Fastest temp can rise pr measrement + self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor + self.max_temp_fall = float(self.options["max_fall_temp"]) # Fastest temp can fall pr measurement + + self.temp = None + self.time = None + + self.min_temp_enabled = False + + return + + def connect(self, units): + + self.input = self.get_unit(self.input, units) + self.heater = self.get_unit(self.heater, units) + + return + + def initialise(self): + + # insert into the attached heater, if it isn't already there + if self not in self.heater.safety: + self.heater.safety.append(self) + + if (not isinstance(self.input, TemperatureSensor)) and (not isinstance(self.input, ColdEnd)): + msg = "{} will not work, input = {} is not a temperature sensor".format(self.name, self.input.name) + logging.error(msg) + + # disconnect from the heater + for i, s in enumerate(self.heater.safety): + if self == s: + self.heater.safety.pop(i) + break + + return + + def set_min_temp_enabled(self, flag): + """ enable the min_temp flag """ + self.min_temp_enabled = flag + + def run_safety_checks(self): + """ Check the temperatures, make sure they are sane. + Sound the alarm if something is wrong """ + + if not self.time: + self.time = time.time() + self.temp = self.input.get_temperature() + return + + old_time = self.time + old_temp = self.temp + self.time = time.time() + self.temp = self.input.get_temperature() + + time_delta = self.time - old_time + temp_delta = self.temp - old_temp + + temp_delta /= time_delta # get a gradient deg C / sec + + target_temperature = self.heater.input.target_temperature + power_on = self.heater.mosfet.get_power() > 0 + + # Check that temperature is not rising too quickly + if temp_delta > self.max_temp_rise: + a = Alarm(Alarm.HEATER_RISING_FAST, + "Temperature rising too quickly ({} degrees) for {}".format(temp_delta, self.name)) + # Check that temperature is not rising quickly enough when power is applied + if (temp_delta < self.min_temp_rise) and (power_on): + a = Alarm(Alarm.HEATER_RISING_SLOW, + "Temperature rising too slowly ({} degrees) for {}".format(temp_delta, self.name)) + # Check that temperature is not falling too quickly + if temp_delta < -self.max_temp_fall: + a = Alarm(Alarm.HEATER_FALLING_FAST, + "Temperature falling too quickly ({} degrees) for {}".format(temp_delta, self.name)) + # Check that temperature has not fallen below a certain setpoint from target + if self.min_temp_enabled and self.temp < (target_temperature - self.min_temp): + a = Alarm(Alarm.HEATER_TOO_COLD, + "Temperature below min set point ({} degrees) for {}".format(self.min_temp, self.name)) + # Check if the temperature has gone beyond the max value + if self.temp > self.max_temp: + a = Alarm(Alarm.HEATER_TOO_HOT, + "Temperature beyond max ({} degrees) for {}".format(self.max_temp, self.name)) + # Check the time diff, only warn if something is off. + if time_delta > 4: + logging.warning("Time between updates too large: " + + self.name + " temp: " + + str(self.temp) + " time delta: " + + str(time_delta)) + + return + + class Control(Unit): def __init__(self, name, options, printer): @@ -163,6 +271,9 @@ def __init__(self, name, options, printer): return + def get_options(self): + return + def connect(self, units): self.input = self.get_unit(self.input, units) if self.output: @@ -193,6 +304,7 @@ def get_options(self): self.off_temperature = float(self.options['off_temperature']) self.on_power = float(self.options['on_power'])/255.0 self.off_power = float(self.options['off_power'])/255.0 + self.target_temperature = float(self.options['target_temperature']) self.power = self.off_power @@ -217,7 +329,7 @@ class ProportionalControl(Control): def get_options(self): """ Init """ self.current_temp = 0.0 - self.target_temp = float(self.options['target_temperature']) # Target temperature (Ts). Start off. + self.target_temperature = float(self.options['target_temperature']) # Target temperature (Ts). Start off. self.P = float(self.options['proportional']) # Proportional self.max_speed = float(self.options['max_speed'])/255.0 self.min_speed = float(self.options['min_speed'])/255.0 @@ -226,7 +338,7 @@ def get_options(self): def get_power(self): """ PID Thread that keeps the temperature stable """ self.current_temp = self.input.get_temperature() - error = self.target_temp-self.current_temp + error = self.target_temperature-self.current_temp if error <= self.ok_range: return 0.0 @@ -247,6 +359,7 @@ class PIDControl(Control): def get_options(self): + self.target_temperature = float(self.options['target_temperature']) self.Kp = float(self.options['pid_Kp']) self.Ti = float(self.options['pid_Ti']) self.Td = float(self.options['pid_Td']) @@ -275,7 +388,7 @@ def get_power(self): self.temperatures.append(current_temp) self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history - self.error = self.target_temp-current_temp + self.error = self.target_temperature-current_temp self.errors.append(self.error) self.errors.pop(0) From 47bb53c5f40ca4f30e3260a5f8a1320b15728c3a Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 12 Jan 2018 14:07:41 +0000 Subject: [PATCH 13/27] corrected implementation of checking for slow temperature rise --- redeem/TemperatureControl.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 6705c09d..6d3c97dc 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -160,8 +160,9 @@ def __init__(self, name, options, printer): self.min_temp = float(self.options["min_temp"]) # If temperature falls below this point from the target, disable. self.max_temp = float(self.options["max_temp"]) # Max temp that can be reached before disabling printer. self.max_temp_rise = float(self.options["max_rise_temp"]) # Fastest temp can rise pr measrement - self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor self.max_temp_fall = float(self.options["max_fall_temp"]) # Fastest temp can fall pr measurement + self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor + self.min_rise_offset = float(self.options["min_rise_offset"]) # Allow checking for slow temp rise when temp is below this offset from target temp self.temp = None self.time = None @@ -218,7 +219,7 @@ def run_safety_checks(self): temp_delta /= time_delta # get a gradient deg C / sec - target_temperature = self.heater.input.target_temperature + target_temp = self.heater.input.target_temperature power_on = self.heater.mosfet.get_power() > 0 # Check that temperature is not rising too quickly @@ -226,7 +227,7 @@ def run_safety_checks(self): a = Alarm(Alarm.HEATER_RISING_FAST, "Temperature rising too quickly ({} degrees) for {}".format(temp_delta, self.name)) # Check that temperature is not rising quickly enough when power is applied - if (temp_delta < self.min_temp_rise) and (power_on): + if (temp_delta < self.min_temp_rise) and (power_on) and (self.temp < (target_temp - self.min_rise_offset)): a = Alarm(Alarm.HEATER_RISING_SLOW, "Temperature rising too slowly ({} degrees) for {}".format(temp_delta, self.name)) # Check that temperature is not falling too quickly @@ -234,7 +235,7 @@ def run_safety_checks(self): a = Alarm(Alarm.HEATER_FALLING_FAST, "Temperature falling too quickly ({} degrees) for {}".format(temp_delta, self.name)) # Check that temperature has not fallen below a certain setpoint from target - if self.min_temp_enabled and self.temp < (target_temperature - self.min_temp): + if self.min_temp_enabled and self.temp < (target_temp - self.min_temp): a = Alarm(Alarm.HEATER_TOO_COLD, "Temperature below min set point ({} degrees) for {}".format(self.min_temp, self.name)) # Check if the temperature has gone beyond the max value From 4abf5f9eeb571f8c49a5a559e89a1558f96b2e8e Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sun, 14 Jan 2018 10:14:11 +0000 Subject: [PATCH 14/27] give feedback to user about any issues with generating the temperature control infrastructure --- configs/default.cfg | 19 ++---- configs/local.cfg | 0 configs/test.cfg | 32 ++++++++++ redeem/Alarm.py | 10 ++- redeem/CascadingConfigParser.py | 2 + redeem/Fan.py | 11 +++- redeem/Heater.py | 31 +++++----- redeem/Printer.py | 13 ++++ redeem/Redeem.py | 105 ++++++++++++++++++++++---------- redeem/TemperatureControl.py | 75 ++++++++++++++++++----- redeem/gcodes/M105.py | 13 ++-- redeem/gcodes/M106_M107.py | 2 + redeem/gcodes/M115.py | 5 ++ redeem/gcodes/M562.py | 6 +- 14 files changed, 239 insertions(+), 85 deletions(-) create mode 100755 configs/local.cfg create mode 100644 configs/test.cfg diff --git a/configs/default.cfg b/configs/default.cfg index c3502312..67e3ae1f 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -230,6 +230,7 @@ pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 +max_power = 255 sleep = 0.25 @@ -241,6 +242,7 @@ pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 +max_power = 255 sleep = 0.25 @@ -252,6 +254,7 @@ pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 +max_power = 255 sleep = 0.25 @@ -263,6 +266,7 @@ pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 +max_power = 255 sleep = 0.25 @@ -274,6 +278,7 @@ pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 +max_power = 255 sleep = 0.25 @@ -285,6 +290,7 @@ pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 +max_power = 255 sleep = 0.5 # safety limits for heaters @@ -440,10 +446,7 @@ channel = 0 [[Heater-E]] type = heater mosfet = 5 -max_power = 1.0 -sleep = 0.25 prefix = T0 -#input = 0 input = Control-E safety = Safety-E @@ -451,8 +454,6 @@ safety = Safety-E [[Heater-H]] type = heater mosfet = 3 -max_power = 1.0 -sleep = 0.25 prefix = T1 input = Control-H safety = Safety-H @@ -461,8 +462,6 @@ safety = Safety-H [[Heater-A]] type = heater mosfet = 11 -max_power = 1.0 -sleep = 0.25 prefix = T2 input = Control-A safety = Safety-A @@ -471,8 +470,6 @@ safety = Safety-A [[Heater-B]] type = heater mosfet = 12 -max_power = 1.0 -sleep = 0.25 prefix = T3 input = Control-B safety = Safety-B @@ -481,8 +478,6 @@ safety = Safety-B [[Heater-C]] type = heater mosfet = 13 -max_power = 1.0 -sleep = 0.25 prefix = T4 input = Control-C safety = Safety-C @@ -491,8 +486,6 @@ safety = Safety-C [[Heater-HBP]] type = heater mosfet = 4 -max_power = 1.0 -sleep = 0.5 prefix = B input = Control-HBP safety = Safety-HBP diff --git a/configs/local.cfg b/configs/local.cfg new file mode 100755 index 00000000..e69de29b diff --git a/configs/test.cfg b/configs/test.cfg new file mode 100644 index 00000000..2246482b --- /dev/null +++ b/configs/test.cfg @@ -0,0 +1,32 @@ +[Temperature Control] + +[[AmbientTemperature]] +type = alias +input = ds18b20-0 + +[[CoolantTemperature]] +type = alias +input = ds18b20-1 + +[[CoolantWarmup]] +type = difference +input-1 = CoolantTemperature +input-0 = AmbientTemperature + +[[CoolantFan]] +type = proportional-control +input = CoolantWarmup +target_temperature = 0 +proportional = 0.2 +max_speed = 255 +min_speed = 127 +ok_range = 0.5 + + +[System] + +test = blah + +[Fans] +[[Fan-0]] +add-to-M106 = True \ No newline at end of file diff --git a/redeem/Alarm.py b/redeem/Alarm.py index f0e477a4..5b23a9b0 100644 --- a/redeem/Alarm.py +++ b/redeem/Alarm.py @@ -39,7 +39,8 @@ class Alarm: IMPOSSIBLE_MOVE_ATTEMPTED = 9 STEPPER_FAULT = 10 # Error on a stepper ALARM_TEST = 11 # Testsignal, used during start-up - HEATER_RISING_SLOW = 12 # Temperture is rising too fast + HEATER_RISING_SLOW = 12 # Temperature is rising too slow + CONFIG_ERROR = 13 # error when importing config printer = None executor = None @@ -57,6 +58,8 @@ def __init__(self, alarm_type, message, short_message=None): Alarm.executor.queue.put(self) else: logging.error("Enable to enqueue alarm!") + + self.printer.alarms.append(self) def execute(self): """ Execute the alarm """ @@ -115,6 +118,11 @@ def execute(self): elif self.type == Alarm.ALARM_TEST: logging.info("Alarm: Operational") Alarm.action_command("alarm_operational", self.message) + elif self.type == Alarm.CONFIG_ERROR: + self.stop_print() + Alarm.action_command("pause") + self.inform_listeners() + Alarm.action_command("alarm_config_error", self.message) else: logging.warning("An Alarm of unknown type was sounded!") diff --git a/redeem/CascadingConfigParser.py b/redeem/CascadingConfigParser.py index eb12198e..60634a35 100644 --- a/redeem/CascadingConfigParser.py +++ b/redeem/CascadingConfigParser.py @@ -22,6 +22,7 @@ import os import logging import struct +import copy #============================================================================== # Functions @@ -169,6 +170,7 @@ def load(self): if i == 0: # generate the base config self._initialise(self.parser_options) self._load(config_file, self._original_configspec) + self.default_cfg = self.dict() else: cfg = ConfigObj(config_file, **self.parser_options) diff --git a/redeem/Fan.py b/redeem/Fan.py index 5550815a..03f87fc8 100644 --- a/redeem/Fan.py +++ b/redeem/Fan.py @@ -49,6 +49,7 @@ def __init__(self, name, options, printer): self.force_disable = False self.printer.fans.append(self) + self.max_power = 1.0 self.counter += 1 @@ -66,6 +67,14 @@ def connect(self, units): self.printer.controlled_fans.append(self) logging.info("Added {} to M106/M107".format(self.name)) + + def initialise(self): + """ stuff to do after connecting""" + + # inherit the sleep timer from controller + self.sleep = self.input.sleep + + return def set_PWM_frequency(self, value): @@ -93,7 +102,7 @@ def run_controller(self): while self.enabled: self.set_value(self.input.get_power()) - time.sleep(1) + time.sleep(self.sleep) self.disabled = True def disable(self): diff --git a/redeem/Heater.py b/redeem/Heater.py index 015d451a..e5405bef 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -38,15 +38,12 @@ def __init__(self, name, options, printer): self.name = name self.options = options - self.printer = printer + self.printer = printer self.mosfet = self.options["mosfet"] self.prefix = self.options["prefix"] - self.sleep = float(self.options["sleep"]) # Time to sleep between measurements - self.safety = [s.strip() for s in options["safety"].split(",")] - self.max_power = float(self.options["max_power"]) # Maximum power self.min_temp_enabled = False # Temperature error limit self.input = self.options["input"] @@ -55,13 +52,17 @@ def __init__(self, name, options, printer): self.temperatures = [] + # add to printer + self.short_name = self.name.split("-")[-1] + self.printer.heaters[self.short_name] = self + return def connect(self, units): """ connect to sensors and control units""" # connect a MOSFET - self.mosfet = self.printer.mosfets[self.name.replace("Heater", "MOSFET")] + self.mosfet = self.printer.mosfets[self.short_name] # connect the controller self.input = self.get_unit(self.input, units) @@ -75,9 +76,9 @@ def connect(self, units): def initialise(self): """ stuff to do after connecting""" - # inherit the sleep timer from PID controller if that is what we are using - if isinstance(self.input, PIDControl): - self.sleep = self.input.sleep + # inherit the sleep timer from controller + self.sleep = self.input.sleep + self.max_power = self.input.max_power return @@ -104,7 +105,7 @@ def check(self): def set_target_temperature(self, temp): """ Set the target temperature of the controller """ self.min_temp_enabled = False - self.input.target_temperature = float(temp) + self.input.set_target_temperature(temp) def get_temperature(self): """ get the temperature of the thermistor and the control input""" @@ -116,18 +117,18 @@ def get_temperature_raw(self): def get_target_temperature(self): """ get the target temperature""" - return self.input.target_temperature + return self.input.get_target_temperature() def is_target_temperature_reached(self): """ Returns true if the target temperature is reached """ current_temp = self.temperatures[-1] - target_temp = self.input.target_temperature + target_temp = self.get_target_temperature() if target_temp == 0: return True if current_temp == 0: - self.input.target_temperature = 0 + self.set_target_temperature(0) target_temp = 0 err = abs(current_temp - target_temp) reached = err < self.input.ok_range @@ -135,7 +136,7 @@ def is_target_temperature_reached(self): def is_temperature_stable(self, seconds=10): """ Returns true if the temperature has been stable for n seconds """ - target_temp = self.input.target_temperature + target_temp = self.get_target_temperature() ok_range = self.input.ok_range if len(self.temperatures) < int(seconds/self.sleep): return False @@ -167,7 +168,7 @@ def enable_min_temp(self): def disable(self): """ Stops the heater and the PID controller """ - self.input.target_temperature = 0 + self.set_target_temperature(0) self.enabled = False self.mosfet.set_power(0.0) # Wait for PID to stop @@ -200,7 +201,7 @@ def run_controller(self): # get the controlling temperature temp = self.input.input.get_temperature() - self.temperatures.append(cntrl_temp) + self.temperatures.append(temp) self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history # Run safety checks diff --git a/redeem/Printer.py b/redeem/Printer.py index 88bc5b12..6f67d0d5 100755 --- a/redeem/Printer.py +++ b/redeem/Printer.py @@ -44,6 +44,7 @@ class Printer: def __init__(self): self.config_location = None + self.alarms = [] self.steppers = {} self.heaters = {} self.thermistors = {} @@ -233,6 +234,18 @@ def save_bed_compensation_matrix(self): # Only update if they are different if mat != self.config.get('Geometry', 'bed_compensation_matrix'): self.config.set('Geometry', 'bed_compensation_matrix', mat) + + def resend_alarms(self): + """ send all alarms that are in the alarms queue """ + + for alarm in self.alarms: + alarm.execute() + #logging.info("Resent alarm : {}".format(alarm.message)) + + # clear alarms + self.alarms = [] + + return def movement_axis(self, axis): if self.e_axis_active and axis == "E": diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 49aee9a5..1dbe3f61 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -34,7 +34,7 @@ from multiprocessing import JoinableQueue import Queue import numpy as np -import sys +import sys, traceback from Mosfet import Mosfet from Stepper import * @@ -84,9 +84,51 @@ def __init__(self, config_location="/etc/redeem"): - default is installed directory - allows for running in a local directory when debugging """ + + self.config_location = config_location + + # check for config files + file_path = os.path.join(config_location,"default.cfg") + if not os.path.exists(file_path): + logging.error(file_path + " does not exist, this file is required for operation") + sys.exit() # maybe use something more graceful? + + local_path = os.path.join(config_location,"local.cfg") + if not os.path.exists(local_path): + logging.info(local_path + " does not exist, Creating one") + os.mknod(local_path) + os.chmod(local_path, 0o777) + + + configs = [os.path.join(config_location,'default.cfg'), + os.path.join(config_location,'printer.cfg'), + os.path.join(config_location,'local.cfg')] + + self.config_error = [] + + # run initialisation + self.initialize(configs) + + if self.config_error: + for err in self.config_error: + Alarm(Alarm.CONFIG_ERROR, err) + + return + + + def initialize(self, configs=[]): + """ + set up the printer + """ + + if not configs: + msg = "No configuration files provided, aborting" + logging.error(msg) + raise RuntimeError(msg) + firmware_version = "{}~{}".format(__version__, __release_name__) logging.info("Redeem initializing "+firmware_version) - + printer = Printer() self.printer = printer Path.printer = printer @@ -94,31 +136,16 @@ def __init__(self, config_location="/etc/redeem"): printer.firmware_version = firmware_version - printer.config_location = config_location + printer.config_location = self.config_location # Set up and Test the alarm framework Alarm.printer = self.printer Alarm.executor = AlarmExecutor() alarm = Alarm(Alarm.ALARM_TEST, "Alarm framework operational") - - # check for config files - file_path = os.path.join(config_location,"default.cfg") - if not os.path.exists(file_path): - logging.error(file_path + " does not exist, this file is required for operation") - sys.exit() # maybe use something more graceful? - - local_path = os.path.join(config_location,"local.cfg") - if not os.path.exists(local_path): - logging.info(local_path + " does not exist, Creating one") - os.mknod(local_path) - os.chmod(local_path, 0o777) - + # Parse the config files. - printer.config = CascadingConfigParser( - [os.path.join(config_location,'default.cfg'), - os.path.join(config_location,'printer.cfg'), - os.path.join(config_location,'local.cfg')], - allow_new = ["Temperature Control"]) # <-- this is where users are allowed to add stuff to the config + printer.config = CascadingConfigParser(configs, + allow_new = ["Temperature Control"]) # <-- this is where users are allowed to add stuff to the config # Get the revision and loglevel from the Config file level = self.printer.config.getint('System', 'loglevel') @@ -282,10 +309,9 @@ def __init__(self, config_location="/etc/redeem"): if printer.config.reach_revision == "00A0": for i, c in enumerate([14,15,7]): self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c - # define the inputs/outputs available on this board - # also define those that are NOT available for later use... + # also define those that are NOT available (for later use) exclude = [] heaters = ["E", "H", "HBP"] extra = ["A", "B", "C"] @@ -300,13 +326,13 @@ def __init__(self, config_location="/etc/redeem"): name = "Thermistor-{}".format(e) adc = self.printer.config.get("Thermistors", name, "path_adc") sensor = self.printer.config.get("Thermistors", name, "sensor") - self.printer.thermistors[name] = TemperatureSensor(adc, name, sensor) - self.printer.thermistors[name].printer = printer + self.printer.thermistors[e] = TemperatureSensor(adc, name, sensor) + self.printer.thermistors[e].printer = printer # Mosfets name = "Heater-{}".format(e) channel = self.printer.config.getint("Heaters", name, "mosfet") - self.printer.mosfets["MOSFET-{}".format(e)] = Mosfet(channel) + self.printer.mosfets[e] = Mosfet(channel) # build and connect all of the temperature control infrastructure @@ -323,11 +349,12 @@ def __init__(self, config_location="/etc/redeem"): "proportional-control":ProportionalControl, "fan":Fan, "heater":Heater, "safety":Safety} + # generate units + all_built = True units = {} for section in ["Temperature Control", "Fans", "Heaters"]: cfg = self.printer.config[section] - - # generate units + for name, options in cfg.items(): if not isinstance(options, Section): continue @@ -337,8 +364,22 @@ def __init__(self, config_location="/etc/redeem"): continue input_type = options["type"] - unit = control_units[input_type](name, options, self.printer) - units[name] = unit + try: + unit = control_units[input_type](name, options, self.printer) + units[name] = unit + except Exception as e: + msg = "Configuration section '{}' failed to build. Have you defined {}?".format(name, str(e)) + self.config_error.append(msg) + logging.error(msg, exc_info=True) + all_built = False + + if not all_built: + logging.warning("Control unit/s failed. Using default.cfg only.") + self.initialize(configs = [configs[0]]) # re-run this method but only on default.cfg + return + # ^^^ this means that even if we stuff something up, redeem will not crash + # it will probably be basically unusable, but it won't crash. + # connect units for name, unit in units.items(): @@ -355,9 +396,11 @@ def __init__(self, config_location="/etc/redeem"): # turn on the fans and heaters for fan in self.printer.fans: + logging.info("{} enabled".format(fan.name)) fan.enable() - for heater in self.printer.heaters: + for name, heater in self.printer.heaters.items(): + logging.info("{} enabled".format(name)) heater.enable() ####################################################################### diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 6d3c97dc..8ba75c97 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -29,6 +29,7 @@ from configobj import Section import logging from threading import Thread +import numpy as np from TemperatureSensor import TemperatureSensor from ColdEnd import ColdEnd @@ -53,18 +54,18 @@ def get_unit(self, name, units): if name in units: return units[name] elif "Thermistor" in name: - if name in self.printer.thermistors: - return self.printer.thermistors[name] + e = name.split("-")[-1] + return self.printer.thermistors[e] elif "MOSFET" in name: - if name in self.printer.mosfets: - return self.printer.mosfets[name] + e = name.split("-")[-1] + return self.printer.mosfets[name] elif "ds18b20" in name: for sensor in self.printer.cold_ends: if name == sensor.name: return sensor else: #assume it is a constant c_name = "Constant-{}".format(self.counter) - unit = ConstantControl(c_name, {"input":int(name)}, self.printer) + unit = ConstantControl(c_name, {"input":int(name), "sleep":1.0}, self.printer) units[c_name] = unit return unit @@ -282,6 +283,9 @@ def connect(self, units): self.output.input = self return + + def reset(self): + return class ConstantControl(Control): @@ -290,6 +294,7 @@ class ConstantControl(Control): def get_options(self): self.power = int(self.options['input'])/255.0 + self.sleep = 1.0 # use a default return def get_power(self): @@ -301,22 +306,39 @@ class OnOffControl(Control): feedback_control = True def get_options(self): - self.on_temperature = float(self.options['on_temperature']) - self.off_temperature = float(self.options['off_temperature']) - self.on_power = float(self.options['on_power'])/255.0 - self.off_power = float(self.options['off_power'])/255.0 self.target_temperature = float(self.options['target_temperature']) + self.on_offset = float(self.options['on_offset']) + self.off_offset = float(self.options['off_offset']) + self.max_power = float(self.options['on_power'])/255.0 + self.off_power = float(self.options['off_power'])/255.0 + self.sleep = float(self.options['sleep']) + + self.on_temperature = self.target_temperature + self.on_offset + self.off_temperature = self.target_temperature + self.off_offset self.power = self.off_power return + def set_target_temperature(self, temp): + """ set the target temperature """ + + self.target_temperature = float(temp) + self.on_temperature = self.target_temperature + self.on_offset + self.off_temperature = self.target_temperature + self.off_offset + + return + + def get_target_temperature(self): + """ get the target temperature """ + return self.target_temperature + def get_power(self): temp = self.input.get_temperature() if temp <= self.on_temperature: - self.power = self.on_power + self.power = self.max_power elif temp >= self.off_temperature: self.power = self.off_power @@ -332,9 +354,19 @@ def get_options(self): self.current_temp = 0.0 self.target_temperature = float(self.options['target_temperature']) # Target temperature (Ts). Start off. self.P = float(self.options['proportional']) # Proportional - self.max_speed = float(self.options['max_speed'])/255.0 - self.min_speed = float(self.options['min_speed'])/255.0 + self.max_power = min(1.0, float(self.options['max_power'])/255.0) + self.min_power = max(0, float(self.options['min_power'])/255.0) self.ok_range = float(self.options['ok_range']) + self.sleep = float(self.options['sleep']) + + def set_target_temperature(self, temp): + """ set the target temperature """ + self.target_temperature = float(temp) + return + + def get_target_temperature(self): + """ get the target temperature """ + return self.target_temperature def get_power(self): """ PID Thread that keeps the temperature stable """ @@ -347,10 +379,10 @@ def get_power(self): power = self.P*error # The formula for the PID (only P) power = max(min(power, 1.0), 0.0) # Normalize to 0,1 - # Clamp the max speed - power = min(power, self.max_speed) - # Clamp min speed - power = max(power, self.min_speed) + # Clamp the max power + power = min(power, self.max_power) + # Clamp min power + power = max(power, self.min_power) return power @@ -365,6 +397,7 @@ def get_options(self): self.Ti = float(self.options['pid_Ti']) self.Td = float(self.options['pid_Td']) self.ok_range = float(self.options['ok_range']) + self.max_power = min(1.0, float(self.options['ok_range'])/255.0) self.sleep = float(self.options['sleep']) return @@ -382,6 +415,16 @@ def initialise(self): self.error_integral = 0.0 # Accumulated integral since the temperature came within the boudry self.error_integral_limit = 100.0 # Integral temperature boundary + + def set_target_temperature(self, temp): + """ set the target temperature """ + self.target_temperature = float(temp) + return + + def get_target_temperature(self): + """ get the target temperature """ + return self.target_temperature + def get_power(self): diff --git a/redeem/gcodes/M105.py b/redeem/gcodes/M105.py index f25777fe..0d2ddfa4 100644 --- a/redeem/gcodes/M105.py +++ b/redeem/gcodes/M105.py @@ -11,6 +11,7 @@ import math from six import iteritems +import logging from .GCodeCommand import GCodeCommand @@ -23,9 +24,9 @@ def format_temperature(heater, prefix): Returns : for a given heater and prefix. Temperature values are formatted as integers. """ - thermistor_temperature, control_temperature = self.printer.heaters[heater].get_temperature() + temp = self.printer.heaters[heater].get_temperature() target = self.printer.heaters[heater].get_target_temperature() - return "{0}:{1:.1f}/{2:.1f}({3:0.1f})".format(prefix, control_temperature, target, thermistor_temperature) + return "{0}:{1:.1f}/{2:.1f}".format(prefix, temp, target) # Cura expects the temperature from the first current_tool = self.printer.current_tool @@ -35,14 +36,16 @@ def format_temperature(heater, prefix): for heater, data in sorted(iteritems(self.printer.heaters), key=lambda(k, v): (v, k)): answer += " " + format_temperature(heater, data.prefix) - # Append the current tool power is using PID - if not self.printer.heaters[current_tool].onoff_control: - answer += " @:" + str(math.floor(255*self.printer.heaters[current_tool].mosfet.get_power())) + # Append the current tool power + current_power = math.floor(255*self.printer.heaters[current_tool].mosfet.get_power()) + if current_power > 0.0: + answer += " @:" + str(curent_power) for c, cooler in enumerate(self.printer.cold_ends): temp = cooler.get_temperature() answer += " C{0}:{1:.0f}".format(c, temp) + #logging.info(answer) g.set_answer(answer) def get_description(self): diff --git a/redeem/gcodes/M106_M107.py b/redeem/gcodes/M106_M107.py index afd575ef..97e31385 100644 --- a/redeem/gcodes/M106_M107.py +++ b/redeem/gcodes/M106_M107.py @@ -27,6 +27,8 @@ def execute(self, gcode): value = float(gcode.get_float_by_letter("S", 255)) / 255.0 for fan in fans: + # exit any control loop that the fan maybe in + fan.disable() if gcode.has_letter("R"): # Ramp to value delay = gcode.get_float_by_letter("R", 0.01) fan.ramp_to(value, delay) diff --git a/redeem/gcodes/M115.py b/redeem/gcodes/M115.py index e7d659ca..c61a011e 100644 --- a/redeem/gcodes/M115.py +++ b/redeem/gcodes/M115.py @@ -39,6 +39,11 @@ def execute(self, g): extruder_count ) ) + + # tell printer to push all outstanding alarms + self.printer.resend_alarms() + + return def get_description(self): return "Get Firmware Version and Capabilities" diff --git a/redeem/gcodes/M562.py b/redeem/gcodes/M562.py index 466c3eee..78505092 100644 --- a/redeem/gcodes/M562.py +++ b/redeem/gcodes/M562.py @@ -22,11 +22,11 @@ def execute(self, g): if g.has_letter("P"): heater_nr = g.get_int_by_letter("P", 1) if P == 0: - self.printer.heaters["HBP"].extruder_error = False + self.printer.heaters["HBP"].heater_error = False elif P == 1: - self.printer.heaters["E"].extruder_error = False + self.printer.heaters["E"].heater_error = False elif P == 2: - self.printer.heaters["H"].extruder_error = False + self.printer.heaters["H"].heater_error = False else: # No P, Enable all heaters for _, heater in iteritems(self.printer.heaters): heater.extruder_error = False From 455e3e69c75ac09fa5aa325b5e5b6b2f322048b9 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sun, 14 Jan 2018 10:22:01 +0000 Subject: [PATCH 15/27] removed testing files that were added by mistake --- configs/local.cfg | 0 configs/test.cfg | 32 -------------------------------- 2 files changed, 32 deletions(-) delete mode 100755 configs/local.cfg delete mode 100644 configs/test.cfg diff --git a/configs/local.cfg b/configs/local.cfg deleted file mode 100755 index e69de29b..00000000 diff --git a/configs/test.cfg b/configs/test.cfg deleted file mode 100644 index 2246482b..00000000 --- a/configs/test.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[Temperature Control] - -[[AmbientTemperature]] -type = alias -input = ds18b20-0 - -[[CoolantTemperature]] -type = alias -input = ds18b20-1 - -[[CoolantWarmup]] -type = difference -input-1 = CoolantTemperature -input-0 = AmbientTemperature - -[[CoolantFan]] -type = proportional-control -input = CoolantWarmup -target_temperature = 0 -proportional = 0.2 -max_speed = 255 -min_speed = 127 -ok_range = 0.5 - - -[System] - -test = blah - -[Fans] -[[Fan-0]] -add-to-M106 = True \ No newline at end of file From 80c9980b1e795b9a3f4ce95cf354ac81dc6de0ed Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sun, 14 Jan 2018 12:34:58 +0000 Subject: [PATCH 16/27] Getting alarms to work with heating. Added delay time to slow temp rise error so that a heater has time to warm up a bit first --- configs/default.cfg | 6 ++++++ redeem/Alarm.py | 4 ++-- redeem/Heater.py | 1 + redeem/PathPlanner.py | 6 ++++-- redeem/Stepper.py | 10 ++++++++++ redeem/TemperatureControl.py | 26 ++++++++++++++++++-------- redeem/gcodes/M105.py | 2 +- redeem/gcodes/M562.py | 2 +- 8 files changed, 43 insertions(+), 14 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index 67e3ae1f..e0cc7fc3 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -310,6 +310,7 @@ min_temp = 20.0 max_temp = 250.0 min_rise_temp = 0.1 min_rise_offset = 20 +min_rise_delay = 1 input = Thermistor-E heater = Heater-E @@ -321,6 +322,7 @@ min_temp = 20.0 max_temp = 250.0 min_rise_temp = 0.1 min_rise_offset = 20 +min_rise_delay = 1 input = Thermistor-H heater = Heater-H @@ -332,6 +334,7 @@ min_temp = 20.0 max_temp = 250.0 min_rise_temp = 0.1 min_rise_offset = 20 +min_rise_delay = 1 input = Thermistor-A heater = Heater-A @@ -343,6 +346,7 @@ min_temp = 20.0 max_temp = 250.0 min_rise_temp = 0.1 min_rise_offset = 20 +min_rise_delay = 1 input = Thermistor-B heater = Heater-B @@ -354,6 +358,7 @@ min_temp = 20.0 max_temp = 250.0 min_rise_temp = 0.1 min_rise_offset = 20 +min_rise_delay = 1 input = Thermistor-C heater = Heater-C @@ -365,6 +370,7 @@ min_temp = 20.0 max_temp = 250.0 min_rise_temp = 0.1 min_rise_offset = 20 +min_rise_delay = 1 input = Thermistor-HBP heater = Heater-HBP diff --git a/redeem/Alarm.py b/redeem/Alarm.py index 5b23a9b0..c0a461bf 100644 --- a/redeem/Alarm.py +++ b/redeem/Alarm.py @@ -89,7 +89,7 @@ def execute(self): Alarm.action_command("pause") Alarm.action_command("alarm_heater_falling_fast", self.message) elif self.type == Alarm.HEATER_RISING_SLOW: - self.stop_print() + self.disable_heaters() self.inform_listeners() Alarm.action_command("pause") Alarm.action_command("alarm_heater_rising_slow", self.message) @@ -137,7 +137,7 @@ def stop_print(self): def disable_heaters(self): logging.warning("Disabling heaters") for _, heater in iteritems(self.printer.heaters): - heater.extruder_error = True + heater.heater_error = True def inform_listeners(self): """ Inform all listeners (comm channels) of the occured error """ diff --git a/redeem/Heater.py b/redeem/Heater.py index e5405bef..9927789f 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -221,6 +221,7 @@ def run_controller(self): finally: # Disable this mosfet if anything goes wrong self.mosfet.set_power(0) + self.set_target_temperature(0.0) def check_temperature_error(self): """ for errors according to the attached safety units """ diff --git a/redeem/PathPlanner.py b/redeem/PathPlanner.py index ce06d09d..6d4afaeb 100644 --- a/redeem/PathPlanner.py +++ b/redeem/PathPlanner.py @@ -96,6 +96,7 @@ def _init_path_planner(self): fw1 = self.pru_firmware.get_firmware(1) if fw0 is None or fw1 is None: + logging.error("Unable to get PRU firmware") return self.native_planner.initPRU(fw0, fw1) @@ -119,6 +120,8 @@ def _init_path_planner(self): self.native_planner.setState(self.prev.end_pos) self.printer.plugins.path_planner_initialized(self) self.native_planner.runThread() + + logging.info("PathPlanner initialized") def configure_slaves(self): self.native_planner.enableSlaves(self.printer.has_slaves) @@ -190,8 +193,7 @@ def emergency_interrupt(self): stepper.set_disabled(True) #Create a new path planner to have everything clean when it restarts - self.native_planner.stopThread(True) - self._init_path_planner() + self.restart() def suspend(self): ''' Temporary pause of planner ''' diff --git a/redeem/Stepper.py b/redeem/Stepper.py index 3da1119e..f8ff1ade 100644 --- a/redeem/Stepper.py +++ b/redeem/Stepper.py @@ -264,6 +264,10 @@ def set_enabled(self, force_update=False): elif self.name == "H": ShiftRegister.registers[4].remove_state(0x1) self.enabled = True + + def reset(self): + self.set_disabled() + self.set_enabled() class Stepper_00B3(Stepper_00B2): @@ -325,6 +329,12 @@ def set_current_enabled(self): self.set_current_value(self.current_enable_value) self.current_enabled = True + def reset(self): + self.set_disabled() + self.set_current_disabled() + self.set_enabled() + self.set_current_enabled() + """ The bits in the shift register are as follows (Rev A4) : diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 8ba75c97..9e59fc76 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -25,12 +25,13 @@ import time from builtins import range -from PWM import PWM from configobj import Section import logging from threading import Thread import numpy as np +from PWM import PWM +from Alarm import Alarm from TemperatureSensor import TemperatureSensor from ColdEnd import ColdEnd @@ -164,6 +165,7 @@ def __init__(self, name, options, printer): self.max_temp_fall = float(self.options["max_fall_temp"]) # Fastest temp can fall pr measurement self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor self.min_rise_offset = float(self.options["min_rise_offset"]) # Allow checking for slow temp rise when temp is below this offset from target temp + self.min_rise_delay = float(self.options["min_rise_delay"]) # Allow checking for slow temp rise after this delay time to allow for heat soak self.temp = None self.time = None @@ -223,26 +225,34 @@ def run_safety_checks(self): target_temp = self.heater.input.target_temperature power_on = self.heater.mosfet.get_power() > 0 + # track when the heater was first turned on + if target_temp == 0.0: + self. start_heating_time = time.time() + heating_time = self.time - self.start_heating_time + # Check that temperature is not rising too quickly if temp_delta > self.max_temp_rise: a = Alarm(Alarm.HEATER_RISING_FAST, - "Temperature rising too quickly ({} degrees) for {}".format(temp_delta, self.name)) + "Temperature rising too quickly ({} degrees/sec) for {}".format(temp_delta, self.name)) # Check that temperature is not rising quickly enough when power is applied - if (temp_delta < self.min_temp_rise) and (power_on) and (self.temp < (target_temp - self.min_rise_offset)): + if (temp_delta < self.min_temp_rise) \ + and (power_on) \ + and (self.temp < (target_temp - self.min_rise_offset))\ + and (heating_time > self.min_rise_delay): a = Alarm(Alarm.HEATER_RISING_SLOW, - "Temperature rising too slowly ({} degrees) for {}".format(temp_delta, self.name)) + "Temperature rising too slowly ({} degrees/sec) for {}".format(temp_delta, self.name)) # Check that temperature is not falling too quickly if temp_delta < -self.max_temp_fall: a = Alarm(Alarm.HEATER_FALLING_FAST, - "Temperature falling too quickly ({} degrees) for {}".format(temp_delta, self.name)) + "Temperature falling too quickly ({} degrees/sec) for {}".format(temp_delta, self.name)) # Check that temperature has not fallen below a certain setpoint from target if self.min_temp_enabled and self.temp < (target_temp - self.min_temp): a = Alarm(Alarm.HEATER_TOO_COLD, - "Temperature below min set point ({} degrees) for {}".format(self.min_temp, self.name)) + "Temperature of {} below min set point ({} degrees) for {}".format(self.temp, self.min_temp, self.name)) # Check if the temperature has gone beyond the max value if self.temp > self.max_temp: a = Alarm(Alarm.HEATER_TOO_HOT, - "Temperature beyond max ({} degrees) for {}".format(self.max_temp, self.name)) + "Temperature of {} beyond max ({} degrees) for {}".format(self.temp, self.max_temp, self.name)) # Check the time diff, only warn if something is off. if time_delta > 4: logging.warning("Time between updates too large: " + @@ -397,7 +407,7 @@ def get_options(self): self.Ti = float(self.options['pid_Ti']) self.Td = float(self.options['pid_Td']) self.ok_range = float(self.options['ok_range']) - self.max_power = min(1.0, float(self.options['ok_range'])/255.0) + self.max_power = min(1.0, float(self.options['max_power'])/255.0) self.sleep = float(self.options['sleep']) return diff --git a/redeem/gcodes/M105.py b/redeem/gcodes/M105.py index 0d2ddfa4..b47a206f 100644 --- a/redeem/gcodes/M105.py +++ b/redeem/gcodes/M105.py @@ -39,7 +39,7 @@ def format_temperature(heater, prefix): # Append the current tool power current_power = math.floor(255*self.printer.heaters[current_tool].mosfet.get_power()) if current_power > 0.0: - answer += " @:" + str(curent_power) + answer += " @:" + str(current_power) for c, cooler in enumerate(self.printer.cold_ends): temp = cooler.get_temperature() diff --git a/redeem/gcodes/M562.py b/redeem/gcodes/M562.py index 78505092..01412756 100644 --- a/redeem/gcodes/M562.py +++ b/redeem/gcodes/M562.py @@ -29,7 +29,7 @@ def execute(self, g): self.printer.heaters["H"].heater_error = False else: # No P, Enable all heaters for _, heater in iteritems(self.printer.heaters): - heater.extruder_error = False + heater.heater_error = False def get_description(self): return "Reset temperature fault. " From 10967bcf78d650940535562cf3fdb122ae7c6370 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sun, 14 Jan 2018 14:47:41 +0000 Subject: [PATCH 17/27] getting temperature based errors to work with measurement jitter --- redeem/Heater.py | 3 -- redeem/TemperatureControl.py | 98 +++++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/redeem/Heater.py b/redeem/Heater.py index 9927789f..d6f0f334 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -205,9 +205,6 @@ def run_controller(self): self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history # Run safety checks - self.time_diff = self.current_time-self.prev_time - self.prev_time = self.current_time - self.current_time = time.time() if not self.heater_error: self.check_temperature_error() diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 9e59fc76..ff512a16 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -39,6 +39,36 @@ # CLASSES #============================================================================== +class CircularBuffer(object): + #https://stackoverflow.com/a/40784706 + def __init__(self, size): + """Initialization""" + self.index = 0 + self.size = size + self._data = [] + + def append(self, value): + """Append an element""" + if len(self._data) == self.size: + self._data[self.index] = value + else: + self._data.append(value) + self.index = (self.index + 1) % self.size + + def get_length(self): + return len(self._data) + + def __getitem__(self, key): + """Get element by index, relative to the current index""" + if len(self._data) == self.size: + return(self._data[(key + self.index) % self.size]) + else: + return(self._data[key]) + + def __repr__(self): + """Return string representation""" + return self._data.__repr__() + ' (' + str(len(self._data))+' items)' + class Unit: printer = None @@ -167,9 +197,6 @@ def __init__(self, name, options, printer): self.min_rise_offset = float(self.options["min_rise_offset"]) # Allow checking for slow temp rise when temp is below this offset from target temp self.min_rise_delay = float(self.options["min_rise_delay"]) # Allow checking for slow temp rise after this delay time to allow for heat soak - self.temp = None - self.time = None - self.min_temp_enabled = False return @@ -196,6 +223,10 @@ def initialise(self): if self == s: self.heater.safety.pop(i) break + + self.avg = max(int(1.0/self.heater.input.sleep), 5) + self.temp = CircularBuffer(self.avg) + self.time = CircularBuffer(self.avg) return @@ -207,57 +238,66 @@ def run_safety_checks(self): """ Check the temperatures, make sure they are sane. Sound the alarm if something is wrong """ - if not self.time: - self.time = time.time() - self.temp = self.input.get_temperature() + # add to ring buffers + self.time.append(time.time()) + self.temp.append(self.input.get_temperature()) + + # get ordered lists + times = [self.time[i] for i in range(self.time.get_length())] + temps = [self.temp[i] for i in range(self.temp.get_length())] + n = len(times) + + if not n == self.time.size: return - - old_time = self.time - old_temp = self.temp - self.time = time.time() - self.temp = self.input.get_temperature() - time_delta = self.time - old_time - temp_delta = self.temp - old_temp + # last recorded temperature + current_time = times[-1] + current_temp = sum(temps)/float(n) #average - temp_delta /= time_delta # get a gradient deg C / sec + # rate of change of temperature wrt time + temp_rate = np.polyfit(times, temps, 1)[0] + time_delta = times[-1] - times[0] + # heater info target_temp = self.heater.input.target_temperature power_on = self.heater.mosfet.get_power() > 0 # track when the heater was first turned on if target_temp == 0.0: self. start_heating_time = time.time() - heating_time = self.time - self.start_heating_time + heating_time = current_time - self.start_heating_time # Check that temperature is not rising too quickly - if temp_delta > self.max_temp_rise: + if temp_rate > self.max_temp_rise: a = Alarm(Alarm.HEATER_RISING_FAST, - "Temperature rising too quickly ({} degrees/sec) for {}".format(temp_delta, self.name)) + "Temperature rising too quickly ({:.2f} degrees/sec) for {} ({} = {:.2f})".format(temp_rate, self.name, self.input.name, current_temp)) + + # Check that temperature is not rising quickly enough when power is applied - if (temp_delta < self.min_temp_rise) \ - and (power_on) \ - and (self.temp < (target_temp - self.min_rise_offset))\ - and (heating_time > self.min_rise_delay): + check = [temp_rate < self.min_temp_rise, + power_on, + current_temp < (target_temp - self.min_rise_offset), + heating_time > self.min_rise_delay] + if np.all(check): a = Alarm(Alarm.HEATER_RISING_SLOW, - "Temperature rising too slowly ({} degrees/sec) for {}".format(temp_delta, self.name)) + "Temperature rising too slowly ({:.2f} degrees/sec) for {} ({} = {:.2f})".format(temp_rate, self.name, self.input.name, current_temp)) # Check that temperature is not falling too quickly - if temp_delta < -self.max_temp_fall: + if temp_rate < -self.max_temp_fall: a = Alarm(Alarm.HEATER_FALLING_FAST, - "Temperature falling too quickly ({} degrees/sec) for {}".format(temp_delta, self.name)) + "Temperature falling too quickly ({:.2f} degrees/sec) for {} ({} = {:.2f})".format(temp_rate, self.name, self.input.name, current_temp)) # Check that temperature has not fallen below a certain setpoint from target - if self.min_temp_enabled and self.temp < (target_temp - self.min_temp): + if self.min_temp_enabled and (current_temp < (target_temp - self.min_temp)): a = Alarm(Alarm.HEATER_TOO_COLD, - "Temperature of {} below min set point ({} degrees) for {}".format(self.temp, self.min_temp, self.name)) + "Temperature of {:.2f} below min set point ({:.2f} degrees) for {}".format(current_temp, self.min_temp, self.name)) # Check if the temperature has gone beyond the max value - if self.temp > self.max_temp: + if current_temp > self.max_temp: a = Alarm(Alarm.HEATER_TOO_HOT, - "Temperature of {} beyond max ({} degrees) for {}".format(self.temp, self.max_temp, self.name)) + "Temperature of {:.2f} beyond max ({:.2f} degrees) for {}".format(current_temp, self.max_temp, self.name)) # Check the time diff, only warn if something is off. if time_delta > 4: logging.warning("Time between updates too large: " + self.name + " temp: " + - str(self.temp) + " time delta: " + + str(current_temp) + " time delta: " + str(time_delta)) return From 85401de4bf901c6cb98fdf84aea6c47e4c2fe92b Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Thu, 18 Jan 2018 13:46:01 +0000 Subject: [PATCH 18/27] added pathway for G- and M-codes to be passed through the temperature control interface --- configs/default.cfg | 42 ++--- redeem/ColdEnd.py | 6 + redeem/Fan.py | 52 +++--- redeem/Heater.py | 63 +++++--- redeem/Printer.py | 1 + redeem/Redeem.py | 10 +- redeem/TemperatureControl.py | 298 ++++++++++++++++++++++------------- redeem/TemperatureSensor.py | 6 + redeem/Util.py | 2 +- redeem/gcodes/M106_M107.py | 41 +++-- 10 files changed, 318 insertions(+), 203 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index e0cc7fc3..92f2ad0f 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -219,78 +219,83 @@ e_axis_active = True # Set 'active' = True for all units that are to be parsed. A False value means # it will be ignored +[[M106/M107]] +type = gcode +command = M106, M107 +output = Fan-0, Fan-1, Fan-2, Fan-3 + # default PID control for heaters # When this config is parsed the 'active' flag is updated to reflect the # available thermistors for your Replicape version [[Control-E]] type = pid-control input = Thermistor-E -target_temperature = 0.0 +target_value = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 -max_power = 255 +max_value = 255 sleep = 0.25 [[Control-H]] type = pid-control input = Thermistor-H -target_temperature = 0.0 +target_value = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 -max_power = 255 +max_value = 255 sleep = 0.25 [[Control-A]] type = pid-control input = Thermistor-A -target_temperature = 0.0 +target_value = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 -max_power = 255 +max_value = 255 sleep = 0.25 [[Control-B]] type = pid-control input = Thermistor-B -target_temperature = 0.0 +target_value = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 -max_power = 255 +max_value = 255 sleep = 0.25 [[Control-C]] type = pid-control input = Thermistor-C -target_temperature = 0.0 +target_value = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 -max_power = 255 +max_value = 255 sleep = 0.25 [[Control-HBP]] type = pid-control input = Thermistor-HBP -target_temperature = 0.0 +target_value = 0.0 pid_Kp = 0.1 pid_Ti = 100.0 pid_Td = 0.3 ok_range = 4.0 -max_power = 255 +max_value = 255 sleep = 0.5 # safety limits for heaters @@ -421,28 +426,23 @@ path_adc = /sys/bus/iio/devices/iio:device0/in_voltage6_raw [[Fan-0]] type = fan -input = 0 -add-to-M106 = False channel = 0 +input = 0 [[Fan-1]] type=fan -input = 0 -add-to-M106 = False channel = 0 +input = 0 [[Fan-2]] type = fan -input = 0 -add-to-M106 = False channel = 0 +input = 0 [[Fan-3]] type = fan -input = 0 -add-to-M106 = False channel = 0 - +input = 0 [Heaters] diff --git a/redeem/ColdEnd.py b/redeem/ColdEnd.py index d5630ef6..f662c59a 100644 --- a/redeem/ColdEnd.py +++ b/redeem/ColdEnd.py @@ -39,3 +39,9 @@ def get_temperature(self): logging.warning("Unable to get temperature from "+self.name) return -1 return temperature + + def get_value(self): + return self.get_temperature() + + def __str__(self): + return self.name diff --git a/redeem/Fan.py b/redeem/Fan.py index 03f87fc8..38874ac9 100644 --- a/redeem/Fan.py +++ b/redeem/Fan.py @@ -44,37 +44,31 @@ def __init__(self, name, options, printer): self.options = options self.printer = printer - self.input = self.options["input"] + self.input = None + if "input" in self.options: + self.input = self.options["input"] + self.channel = int(self.options["channel"]) - self.force_disable = False - self.printer.fans.append(self) - self.max_power = 1.0 + # get fan index + i = int(name[-1]) + + self.printer.fans[i] = self + self.max_value = 1.0 self.counter += 1 return def connect(self, units): - self.input = self.get_unit(self.input, units) - - if self.options["add-to-M106"] == "True": - self.force_disable = True - if not isinstance(self.input, ConstantControl): - msg = "{} has a non-constant controller attached. For control by M106/M107 set config 'input' as a constant".format(self.name) - logging.error(msg) - raise RuntimeError(msg) + if self.input: + self.input = self.get_unit(self.input, units) + if not self.input.output: + self.input.output = self - self.printer.controlled_fans.append(self) - logging.info("Added {} to M106/M107".format(self.name)) + def check(self): + logging.info("{} --> {}".format(self.input, self.name)) - def initialise(self): - """ stuff to do after connecting""" - - # inherit the sleep timer from controller - self.sleep = self.input.sleep - - return def set_PWM_frequency(self, value): @@ -101,8 +95,8 @@ def run_controller(self): """ follow a target PWM value 0..1""" while self.enabled: - self.set_value(self.input.get_power()) - time.sleep(self.sleep) + self.set_value(self.input.get_value()) + time.sleep(self.input.sleep) self.disabled = True def disable(self): @@ -116,13 +110,19 @@ def disable(self): def enable(self): """ starts the controller """ - if self.force_disable: - self.disabled = True + + if not self.input: self.enabled = False + self.disabled = True + self.set_value(0.0) return + self.enabled = True self.disabled = False self.t = Thread(target=self.run_controller, name=self.name) self.t.daemon = True self.t.start() - return \ No newline at end of file + return + + def __str__(self): + return self.name \ No newline at end of file diff --git a/redeem/Heater.py b/redeem/Heater.py index d6f0f334..722a2695 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -43,10 +43,15 @@ def __init__(self, name, options, printer): self.mosfet = self.options["mosfet"] self.prefix = self.options["prefix"] - self.safety = [s.strip() for s in options["safety"].split(",")] + self.safety = None + if self.safety in self.options: + self.safety = [s.strip() for s in options["safety"].split(",")] + self.min_temp_enabled = False # Temperature error limit - self.input = self.options["input"] + self.input = None + if "input" in self.options: + self.input = self.options["input"] self.heater_error = False @@ -65,25 +70,37 @@ def connect(self, units): self.mosfet = self.printer.mosfets[self.short_name] # connect the controller - self.input = self.get_unit(self.input, units) + if self.input: + self.input = self.get_unit(self.input, units) + if not self.input.output: + self.input.output = self + #connect the safety - for i, s in enumerate(self.safety): - self.safety[i] = self.get_unit(s, units) + if self.safety: + for i, s in enumerate(self.safety): + self.safety[i] = self.get_unit(s, units) + if not self.safety[i].heater: + self.safety[i].heater = self return def initialise(self): """ stuff to do after connecting""" - # inherit the sleep timer from controller - self.sleep = self.input.sleep - self.max_power = self.input.max_power + if self.input: + self.max_value = self.input.max_value + else: + self.max_value = 0.0 return def check(self): """ run checks""" + + if not self.input: + logging.warning("{} is unconnected".format(self.name)) + self.heater_error = True if not self.safety: self.mosfet.set_power(0.0) @@ -105,7 +122,7 @@ def check(self): def set_target_temperature(self, temp): """ Set the target temperature of the controller """ self.min_temp_enabled = False - self.input.set_target_temperature(temp) + self.input.set_target_value(temp) def get_temperature(self): """ get the temperature of the thermistor and the control input""" @@ -117,7 +134,7 @@ def get_temperature_raw(self): def get_target_temperature(self): """ get the target temperature""" - return self.input.get_target_temperature() + return self.input.get_target_value() def is_target_temperature_reached(self): """ Returns true if the target temperature is reached """ @@ -138,11 +155,11 @@ def is_temperature_stable(self, seconds=10): """ Returns true if the temperature has been stable for n seconds """ target_temp = self.get_target_temperature() ok_range = self.input.ok_range - if len(self.temperatures) < int(seconds/self.sleep): + if len(self.temperatures) < int(seconds/self.input.sleep): return False - if max(self.temperatures[-int(seconds/self.sleep):]) > (target_temp + ok_range): + if max(self.temperatures[-int(seconds/self.input.sleep):]) > (target_temp + ok_range): return False - if min(self.temperatures[-int(seconds/self.sleep):]) < (target_temp - ok_range): + if min(self.temperatures[-int(seconds/self.input.sleep):]) < (target_temp - ok_range): return False return True @@ -183,9 +200,9 @@ def enable(self): if self.heater_error: self.enabled = False return - self.avg = max(int(1.0/self.sleep), 3) + self.avg = max(int(1.0/self.input.sleep), 3) self.prev_time = self.current_time = time.time() - self.temperatures = [self.input.input.get_temperature()] + self.temperatures = [self.input.input.get_value()] self.enabled = True self.t = Thread(target=self.run_controller, name=self.name) self.t.start() @@ -196,13 +213,14 @@ def run_controller(self): while self.enabled: # get the controllers recommendation - power = self.input.get_power() - power = max(min(power, self.max_power, 1.0), 0.0) + sleep = self.input.sleep + value = self.input.get_value() + value = max(min(value, self.max_value, 1.0), 0.0) # get the controlling temperature - temp = self.input.input.get_temperature() + temp = self.input.input.get_value() self.temperatures.append(temp) - self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history + self.temperatures[:-max(int(60/sleep), self.avg)] = [] # Keep only this much history # Run safety checks @@ -211,10 +229,10 @@ def run_controller(self): # Set temp if temperature is OK if not self.heater_error: - self.mosfet.set_power(power) + self.mosfet.set_power(value) else: self.mosfet.set_power(0) - time.sleep(self.sleep) + time.sleep(sleep) finally: # Disable this mosfet if anything goes wrong self.mosfet.set_power(0) @@ -226,3 +244,6 @@ def check_temperature_error(self): for s in self.safety: s.set_min_temp_enabled(self.min_temp_enabled) s.run_safety_checks() + + def __str__(self): + return self.name diff --git a/redeem/Printer.py b/redeem/Printer.py index 6f67d0d5..c61ffebb 100755 --- a/redeem/Printer.py +++ b/redeem/Printer.py @@ -54,6 +54,7 @@ def __init__(self): self.cold_ends = [] self.coolers = [] self.comms = {} # Communication channels + self.command_connect = {} self.path_planner = None self.speed_factor = 1.0 self.unit_factor = 1.0 diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 1dbe3f61..27326f72 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -298,15 +298,19 @@ def initialize(self, configs=[]): # update the channel information for fans if self.revision == "00A3": + self.printer.fans = [None]*3 for i, c in enumerate([0,1,2]): self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c elif self.revision == "0A4A": + self.printer.fans = [None]*3 for i, c in enumerate([8,9,10]): self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c elif self.revision in ["00B1", "00B2", "00B3", "0B3A"]: + self.printer.fans = [None]*4 for i, c in enumerate([7,8,9,10]): self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c if printer.config.reach_revision == "00A0": + self.printer.fans = [None]*3 for i, c in enumerate([14,15,7]): self.printer.config["Fans"]["Fan-{}".format(i)]["channel"] = c @@ -336,9 +340,8 @@ def initialize(self, configs=[]): # build and connect all of the temperature control infrastructure - self.printer.controlled_fans = [] - self.printer.fans = [] self.printer.heaters = {} + self.printer.command_connect = {} # available control unit types, see TemperatureControl.py control_units = {"alias":Alias, "difference":Difference, @@ -347,7 +350,8 @@ def initialize(self, configs=[]): "on-off-control":OnOffControl, "pid-control":PIDControl, "proportional-control":ProportionalControl, - "fan":Fan, "heater":Heater, "safety":Safety} + "fan":Fan, "heater":Heater, "safety":Safety, + "gcode":CommandCode} # generate units all_built = True diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index ff512a16..4b16cde5 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -96,7 +96,7 @@ def get_unit(self, name, units): return sensor else: #assume it is a constant c_name = "Constant-{}".format(self.counter) - unit = ConstantControl(c_name, {"input":int(name), "sleep":1.0}, self.printer) + unit = ConstantControl(c_name, {"value":int(name)}, self.printer) units[c_name] = unit return unit @@ -110,6 +110,9 @@ def initialise(self): def check(self): """ run any checks that need to be performed after full initialisation""" return + + def __str__(self): + return self.name class Alias(Unit): @@ -119,11 +122,8 @@ def __init__(self, name, options, printer): self.name = name self.options = options self.printer = printer - self.input = options["input"] - self.output = None - if "output" in options: - self.output = options["output"] + self.input = self.options["input"] self.counter += 1 @@ -131,12 +131,12 @@ def __init__(self, name, options, printer): def connect(self, units): self.input = self.get_unit(self.input, units) - if self.output: - self.output = self.get_unit(self.output, units) - self.output.input = self - def get_temperature(self): - return self.input.get_temperature() + def get_value(self): + return self.input.get_value() + + def check(self): + logging.info("{} --> {} ".format(self.input, self.name)) class Compare(Unit): @@ -144,41 +144,38 @@ def __init__(self, name, options, printer): self.name = name self.options = options self.printer = printer - self.inputs = [] + + self.input = [] for i in range(2): - self.inputs.append(options["input-{}".format(i)]) - - self.output = None - if "output" in options: - self.output = options["output"] + self.input.append(options["input-{}".format(i)]) self.counter += 1 return def connect(self, units): + for i in range(2): - self.inputs[i] = self.get_unit(self.inputs[i], units) - if self.output: - self.output = self.get_unit(self.output, units) - self.output.input = self + self.input[i] = self.get_unit(self.input[i], units) + + def check(self): + logging.info("({} and {}) --> {}".format(self.input[0].name, self.input[1].name, self.name)) class Difference(Compare): - def get_temperature(self): - return self.inputs[0].get_temperature() - self.inputs[1].get_temperature() + def get_value(self): + return self.input[0].get_value() - self.input[1].get_value() class Maximum(Compare): - def get_temperature(self): - return max(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) + def get_value(self): + return max(self.input[0].get_value(), self.input[1].get_value()) class Minimum(Compare): - def get_temperature(self): - return min(self.inputs[0].get_temperature(), self.inputs[1].get_temperature()) - - + def get_value(self): + return min(self.input[0].get_value(), self.input[1].get_value()) + class Safety(Unit): def __init__(self, name, options, printer): @@ -202,19 +199,23 @@ def __init__(self, name, options, printer): return def connect(self, units): - self.input = self.get_unit(self.input, units) self.heater = self.get_unit(self.heater, units) - return def initialise(self): # insert into the attached heater, if it isn't already there - if self not in self.heater.safety: + if not self.heater.safety: + self.heater.safety = [self] + elif self not in self.heater.safety: self.heater.safety.append(self) + + input_sensor = self.input + if isinstance(self.input, Alias): + input_sensor = self.input.input - if (not isinstance(self.input, TemperatureSensor)) and (not isinstance(self.input, ColdEnd)): + if (not isinstance(input_sensor, TemperatureSensor)) and (not isinstance(input_sensor, ColdEnd)): msg = "{} will not work, input = {} is not a temperature sensor".format(self.name, self.input.name) logging.error(msg) @@ -230,6 +231,9 @@ def initialise(self): return + def check(self): + logging.info("{} --> {} --> {}".format(self.input.name, self.name, self.heater.name)) + def set_min_temp_enabled(self, flag): """ enable the min_temp flag """ self.min_temp_enabled = flag @@ -240,7 +244,7 @@ def run_safety_checks(self): # add to ring buffers self.time.append(time.time()) - self.temp.append(self.input.get_temperature()) + self.temp.append(self.input.get_value()) # get ordered lists times = [self.time[i] for i in range(self.time.get_length())] @@ -259,7 +263,7 @@ def run_safety_checks(self): time_delta = times[-1] - times[0] # heater info - target_temp = self.heater.input.target_temperature + target_temp = self.heater.input.target_value power_on = self.heater.mosfet.get_power() > 0 # track when the heater was first turned on @@ -309,13 +313,12 @@ def __init__(self, name, options, printer): self.name = name self.options = options self.printer = printer - self.input = options["input"] + self.input = None self.output = None - if "output" in options: - self.output = options["output"] - self.power = 0.0 + self.value = 0.0 + self.sleep = 0.25 self.get_options() @@ -324,6 +327,7 @@ def __init__(self, name, options, printer): return def get_options(self): + return def connect(self, units): @@ -336,6 +340,9 @@ def connect(self, units): def reset(self): return + + def check(self): + logging.info("{} --> {} --> {}".format(self.input, self.name, self.output)) class ConstantControl(Control): @@ -343,12 +350,74 @@ class ConstantControl(Control): feedback_control = False def get_options(self): - self.power = int(self.options['input'])/255.0 - self.sleep = 1.0 # use a default + + self.output = None + if "output" in self.options: + self.output = self.options["output"] + + self.value = int(self.options['value'])/255.0 return - def get_power(self): - return self.power + def connect(self, units): + if self.output: + self.output = self.get_unit(self.output, units) + self.output.input = self + + + def get_value(self): + return self.value + + def set_target_value(self, value): + self.value = float(value) + + def ramp_to(self, value, delay): + save_sleep = self.sleep + self.sleep = delay/2.0 + for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): + self.value = w/255.0 + time.sleep(delay) + self.value = value + self.sleep = save_sleep + + + def check(self): + logging.info("{} --> {} --> {}".format(self.value, self.name, self.output)) + + + +class CommandCode(ConstantControl): + """ + For connecting G and M codes + """ + + def get_options(self): + self.command = [c.strip() for c in self.options["command"].split(",")] + for command in self.command: + if command in self.printer.command_connect: + logging.warning("multiple instances of {} used in [Temperature Control]".format(self.command)) + self.printer.command_connect[command] = self + + self.output = [] + if "output" in self.options: + self.output = [o.strip() for o in self.options["output"].split(",")] + + self.input = self.command + + + def connect(self, units): + for i, output in enumerate(self.output): + self.output[i] = self.get_unit(output, units) + self.output[i].input = self + + def check(self): + outputs = "[" + for output in self.output: + outputs += "{}, ".format(output) + outputs = outputs[0:-2] + "]" + logging.info("{} --> {} --> {}".format(self.input, self.name, outputs)) + + def __str__(self): + return str(self.name) class OnOffControl(Control): @@ -356,43 +425,47 @@ class OnOffControl(Control): feedback_control = True def get_options(self): - self.target_temperature = float(self.options['target_temperature']) + self.input = self.options["input"] + self.output = None + if "output" in self.options: + self.output = self.options["output"] + self.target_value = float(self.options['target_value']) self.on_offset = float(self.options['on_offset']) self.off_offset = float(self.options['off_offset']) - self.max_power = float(self.options['on_power'])/255.0 - self.off_power = float(self.options['off_power'])/255.0 + self.max_value = float(self.options['on_value'])/255.0 + self.off_value = float(self.options['off_value'])/255.0 self.sleep = float(self.options['sleep']) - self.on_temperature = self.target_temperature + self.on_offset - self.off_temperature = self.target_temperature + self.off_offset + self.on_value = self.target_value + self.on_offset + self.off_value = self.target_value + self.off_offset - self.power = self.off_power + self.value = self.off_value return - def set_target_temperature(self, temp): - """ set the target temperature """ + def set_target_value(self, value): + """ set the target value """ - self.target_temperature = float(temp) - self.on_temperature = self.target_temperature + self.on_offset - self.off_temperature = self.target_temperature + self.off_offset + self.target_value = float(value) + self.on_value = self.target_value + self.on_offset + self.off_value = self.target_value + self.off_offset return - def get_target_temperature(self): - """ get the target temperature """ - return self.target_temperature + def get_target_value(self): + """ get the target value """ + return self.target_value - def get_power(self): + def get_value(self): - temp = self.input.get_temperature() + value = self.input.get_value() - if temp <= self.on_temperature: - self.power = self.max_power - elif temp >= self.off_temperature: - self.power = self.off_power + if value <= self.on_value: + self.value = self.max_value + elif value >= self.off_value: + self.value = self.off_value - return self.power + return self.value class ProportionalControl(Control): @@ -401,53 +474,60 @@ class ProportionalControl(Control): def get_options(self): """ Init """ - self.current_temp = 0.0 - self.target_temperature = float(self.options['target_temperature']) # Target temperature (Ts). Start off. + self.input = self.options["input"] + self.output = None + if "output" in self.options: + self.output = self.options["output"] + self.current_value = 0.0 + self.target_value = float(self.options['target_value']) # Target value (Ts). Start off. self.P = float(self.options['proportional']) # Proportional - self.max_power = min(1.0, float(self.options['max_power'])/255.0) - self.min_power = max(0, float(self.options['min_power'])/255.0) + self.max_value = min(1.0, float(self.options['max_value'])/255.0) + self.min_value = max(0, float(self.options['min_value'])/255.0) self.ok_range = float(self.options['ok_range']) self.sleep = float(self.options['sleep']) - def set_target_temperature(self, temp): - """ set the target temperature """ - self.target_temperature = float(temp) + def set_target_value(self, value): + """ set the target value """ + self.target_value = float(value) return - def get_target_temperature(self): - """ get the target temperature """ - return self.target_temperature + def get_target_value(self): + """ get the target value """ + return self.target_value - def get_power(self): - """ PID Thread that keeps the temperature stable """ - self.current_temp = self.input.get_temperature() - error = self.target_temperature-self.current_temp + def get_value(self): + """ PID Thread that keeps the value stable """ + self.current_value = self.input.get_value() + error = self.target_value-self.current_value if error <= self.ok_range: return 0.0 - power = self.P*error # The formula for the PID (only P) - power = max(min(power, 1.0), 0.0) # Normalize to 0,1 + value = self.P*error # The formula for the PID (only P) + value = max(min(value, 1.0), 0.0) # Normalize to 0,1 - # Clamp the max power - power = min(power, self.max_power) - # Clamp min power - power = max(power, self.min_power) + # Clamp the max value + value = min(value, self.max_value) + # Clamp min value + value = max(value, self.min_value) - return power + return value class PIDControl(Control): feedback_control = True def get_options(self): - - self.target_temperature = float(self.options['target_temperature']) + self.input = self.options["input"] + self.output = None + if "output" in self.options: + self.output = self.options["output"] + self.target_value = float(self.options['target_value']) self.Kp = float(self.options['pid_Kp']) self.Ti = float(self.options['pid_Ti']) self.Td = float(self.options['pid_Td']) self.ok_range = float(self.options['ok_range']) - self.max_power = min(1.0, float(self.options['max_power'])/255.0) + self.max_value = min(1.0, float(self.options['max_value'])/255.0) self.sleep = float(self.options['sleep']) return @@ -459,46 +539,46 @@ def initialise(self): self.errors = [0]*self.avg self.averages = [0]*self.avg - current_temp = self.input.get_temperature() - self.temperatures = [current_temp] + current_value = self.input.get_value() + self.values = [current_value] - self.error_integral = 0.0 # Accumulated integral since the temperature came within the boudry - self.error_integral_limit = 100.0 # Integral temperature boundary + self.error_integral = 0.0 # Accumulated integral since the value came within the boudry + self.error_integral_limit = 100.0 # Integral value boundary - def set_target_temperature(self, temp): - """ set the target temperature """ - self.target_temperature = float(temp) + def set_target_value(self, value): + """ set the target value """ + self.target_value = float(value) return - def get_target_temperature(self): - """ get the target temperature """ - return self.target_temperature + def get_target_value(self): + """ get the target value """ + return self.target_value - def get_power(self): + def get_value(self): - current_temp = self.input.get_temperature() - self.temperatures.append(current_temp) - self.temperatures[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history + current_value = self.input.get_value() + self.values.append(current_value) + self.values[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history - self.error = self.target_temperature-current_temp + self.error = self.target_value-current_value self.errors.append(self.error) self.errors.pop(0) derivative = self.get_error_derivative() integral = self.get_error_integral() # The standard formula for the PID - power = self.Kp*(self.error + (1.0/self.Ti)*integral + self.Td*derivative) - power = max(min(power, self.max_power, 1.0), 0.0) # Normalize to 0, max + value = self.Kp*(self.error + (1.0/self.Ti)*integral + self.Td*derivative) + value = max(min(value, self.max_value, 1.0), 0.0) # Normalize to 0, max - return power + return value def get_error_derivative(self): - """ Get the derivative of the temperature""" - # Using temperature and not error for calculating derivative + """ Get the derivative of the value""" + # Using value and not error for calculating derivative # gets rid of the derivative kick. dT/dt - der = (self.temperatures[-2]-self.temperatures[-1])/self.sleep + der = (self.values[-2]-self.values[-1])/self.sleep self.averages.append(der) if len(self.averages) > 11: self.averages.pop(0) @@ -509,7 +589,7 @@ def get_error_integral(self): self.error_integral += self.error*self.sleep # Avoid windup by clippping the integral part # to the reciprocal of the integral term - self.error_integral = np.clip(self.error_integral, 0, self.max_power*self.Ti/self.Kp) + self.error_integral = np.clip(self.error_integral, 0, self.max_value*self.Ti/self.Kp) return self.error_integral def reset(self): diff --git a/redeem/TemperatureSensor.py b/redeem/TemperatureSensor.py index 493cbc47..0574a020 100644 --- a/redeem/TemperatureSensor.py +++ b/redeem/TemperatureSensor.py @@ -86,6 +86,9 @@ def get_temperature(self): if not self.sensor: return 0.0 return self.sensor.get_temperature(voltage) + + def get_value(self): + return self.get_temperature() """ @@ -108,6 +111,9 @@ def read_adc(self): TemperatureSensor.mutex.release() return voltage + + def __str__(self): + return self.name """ This class represents standard thermistor sensors. diff --git a/redeem/Util.py b/redeem/Util.py index 106df86c..02a9e883 100644 --- a/redeem/Util.py +++ b/redeem/Util.py @@ -219,4 +219,4 @@ def _plot(x, mph, mpd, threshold, edge, valley, ax, ind): ax.set_title("%s (mph=%s, mpd=%d, threshold=%s, edge='%s')" % (mode, str(mph), mpd, str(threshold), edge)) # plt.grid() - plt.show() + plt.show() \ No newline at end of file diff --git a/redeem/gcodes/M106_M107.py b/redeem/gcodes/M106_M107.py index 97e31385..9db4b6a2 100644 --- a/redeem/gcodes/M106_M107.py +++ b/redeem/gcodes/M106_M107.py @@ -15,25 +15,23 @@ class M106(GCodeCommand): def execute(self, gcode): - fans = [] + # Get the value, 255 if not present + value = float(gcode.get_float_by_letter("S", 255)) / 255.0 + + fan_controller = None if gcode.has_letter("P"): fan_no = gcode.get_int_by_letter("P", 0) if fan_no < len(self.printer.fans): - fans.append(self.printer.fans[fan_no]) - else: # No P in gcode, use fans from settings file - fans = self.printer.controlled_fans - - # Get the value, 255 if not present - value = float(gcode.get_float_by_letter("S", 255)) / 255.0 + fan_controller = self.printer.fans[fan_no].input + else: + fan_controller = self.printer.command_connect["M106"] + - for fan in fans: - # exit any control loop that the fan maybe in - fan.disable() - if gcode.has_letter("R"): # Ramp to value - delay = gcode.get_float_by_letter("R", 0.01) - fan.ramp_to(value, delay) - else: - fan.set_value(value) + if gcode.has_letter("R"): # Ramp to value + delay = gcode.get_float_by_letter("R", 0.01) + fan_controller.ramp_to(value, delay) + else: + fan_controller.set_target_value(value) def get_description(self): return "Set fan power." @@ -51,16 +49,15 @@ def is_buffered(self): class M107(GCodeCommand): def execute(self, gcode): - fans = [] + fan_controller = None if gcode.has_letter("P"): fan_no = gcode.get_int_by_letter("P", 0) if fan_no < len(self.printer.fans): - fans.append(self.printer.fans[fan_no]) - else: # No P in gcode, use fans from settings file - fans = self.printer.controlled_fans - - for fan in fans: - fan.set_value(0) + fan_controller = self.printer.fans[fan_no].input + else: + fan_controller = self.printer.command_connect["M107"] + + fan_controller.set_target_value(0.0) def get_description(self): return "set fan off" From 89bafc209338fd3ac5eca485dfc54bffcdbbdd0f Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 19 Jan 2018 23:03:04 +1000 Subject: [PATCH 19/27] update documentation and comments --- configs/default.cfg | 133 +++++++++++++++++++++++++------- redeem/CascadingConfigParser.py | 24 +++--- redeem/Fan.py | 9 ++- redeem/Heater.py | 4 +- redeem/TemperatureControl.py | 101 ++++++++++++++++++++---- 5 files changed, 209 insertions(+), 62 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index 92f2ad0f..c4e998da 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -214,19 +214,98 @@ arc_segment_length = 0.001 e_axis_active = True [Temperature Control] - # put temperature control units in here, they won't be accepted anywhere else! -# Set 'active' = True for all units that are to be parsed. A False value means -# it will be ignored - +# +# allowed types are : alias, difference, maximum, minimum, constant-control, +# on-off-control, pid-control, proportional-control, safety, gcode +# +# templates for control units are given below. +# +#[[AliasUnitName]] +#type = alias +#input = +# +#[[ComparisonUnitName]] +#type = difference, maximum, or minimum +#input_0 = +#input_1 = +# +#[[ConstantName]] +#type = constant-control +#value = +#output = +# +#[[SafetyName]] +#type = safety +#max_rise_rate = +#max_fall_rate = +#min_temp = +#max_temp = +#min_rise_rate = +#min_rise_offset = +#min_rise_delay = +#input = +#heater = +# +#[[OnOffName]] +#type=on-off-control +#target_value = +#on_offset = +#off_offset = +#on_value = +#off_value = +#sleep = +#output = +# +# +#[[ProportionalControlName]] +#type = proportional-control +#input = +#target_value = +#proportional = +#max_value = +#min_value = +#ok_range = +#sleep = +#output = +# +#[[PIDControlName]] +#type = pid-control +#input = +#target_value = +#pid_Kp = +#pid_Ti = +#pid_Td = +#ok_range = +#max_value = +#sleep = +# +#[[CommandName]] +#type = gcode +#command = +#output = +# +# NOTES: +# <> when connecting a control unit to a heater or fan the connection may be defined from either unit (input or output). +# <> 'difference' units return (input_0 - input_1) +# <> 'gcode' units currently only accept M106 or M107 +# <> in 'safety' units the min_rise_* parameters are used to check for attached/detached/misconnected sensor/heater pairs +# when power is supplied to the heater, we expect min_rise_rate temperature rise per second, as long as temp is min_rise_offset +# below the heaters target temperature and we are min_rise_delay seconds after starting heating. +# <> multiple safety units may be attached to each heater but each safety can only define one input and one output +# <> safety inputs must be temperature sensors, or an alias thereof + +# Default setting is for all fans to be connected to M106/M107 +# this means that the command 'M106 S255' will turn on all fans full blast +# to connect only selected fans edit the output list i.e. output = Fan_1 +# Note that individual fan commands, such as 'M106 S255 P1', will always work +# (depending on the fan controller attached) [[M106/M107]] type = gcode command = M106, M107 output = Fan-0, Fan-1, Fan-2, Fan-3 # default PID control for heaters -# When this config is parsed the 'active' flag is updated to reflect the -# available thermistors for your Replicape version [[Control-E]] type = pid-control input = Thermistor-E @@ -303,17 +382,13 @@ sleep = 0.5 # each safety only has one heater # these allow safety limits to be written for each temperature sensor -# min_rise_* : when power is supplied to the heater, expect min_rise_temp -# temperature rise per second, as long as temp is min_rise_offset below the -# heaters target temperature - [[Safety-E]] type = safety -max_rise_temp = 10.0 -max_fall_temp = 10.0 +max_rise_rate = 10.0 +max_fall_rate = 10.0 min_temp = 20.0 max_temp = 250.0 -min_rise_temp = 0.1 +min_rise_rate = 0.1 min_rise_offset = 20 min_rise_delay = 1 input = Thermistor-E @@ -321,11 +396,11 @@ heater = Heater-E [[Safety-H]] type = safety -max_rise_temp = 10.0 -max_fall_temp = 10.0 +max_rise_rate = 10.0 +max_fall_rate = 10.0 min_temp = 20.0 max_temp = 250.0 -min_rise_temp = 0.1 +min_rise_rate = 0.1 min_rise_offset = 20 min_rise_delay = 1 input = Thermistor-H @@ -333,11 +408,11 @@ heater = Heater-H [[Safety-A]] type = safety -max_rise_temp = 10.0 -max_fall_temp = 10.0 +max_rise_rate = 10.0 +max_fall_rate = 10.0 min_temp = 20.0 max_temp = 250.0 -min_rise_temp = 0.1 +min_rise_rate = 0.1 min_rise_offset = 20 min_rise_delay = 1 input = Thermistor-A @@ -345,11 +420,11 @@ heater = Heater-A [[Safety-B]] type = safety -max_rise_temp = 10.0 -max_fall_temp = 10.0 +max_rise_rate = 10.0 +max_fall_rate = 10.0 min_temp = 20.0 max_temp = 250.0 -min_rise_temp = 0.1 +min_rise_rate = 0.1 min_rise_offset = 20 min_rise_delay = 1 input = Thermistor-B @@ -357,11 +432,11 @@ heater = Heater-B [[Safety-C]] type = safety -max_rise_temp = 10.0 -max_fall_temp = 10.0 +max_rise_rate = 10.0 +max_fall_rate = 10.0 min_temp = 20.0 max_temp = 250.0 -min_rise_temp = 0.1 +min_rise_rate = 0.1 min_rise_offset = 20 min_rise_delay = 1 input = Thermistor-C @@ -369,11 +444,11 @@ heater = Heater-C [[Safety-HBP]] type = safety -max_rise_temp = 10.0 -max_fall_temp = 10.0 +max_rise_rate = 10.0 +max_fall_rate = 10.0 min_temp = 20.0 max_temp = 250.0 -min_rise_temp = 0.1 +min_rise_rate = 0.1 min_rise_offset = 20 min_rise_delay = 1 input = Thermistor-HBP @@ -384,8 +459,6 @@ heater = Heater-HBP # Thermistors for measuring temperature # For list of available temp charts, look in temp_chart.py -# When this config is parsed the 'active' flag is updated to reflect the -# available thermistors for your Replicape version [[Thermistor-E]] sensor = B57560G104F diff --git a/redeem/CascadingConfigParser.py b/redeem/CascadingConfigParser.py index 60634a35..409b3683 100644 --- a/redeem/CascadingConfigParser.py +++ b/redeem/CascadingConfigParser.py @@ -90,10 +90,22 @@ def check_modified(cfg, path, key, value): #============================================================================== class CascadingConfigParser(ConfigObj): + """ + Build a configuration from a hierarchy of inputs where each successive + input is allowed to overwrite the ones before + """ parser_options = OPTION_DEFAULTS def __init__(self, config_files, allow_new=[]): + """ + initialize the config parser + + config_files: list of paths to config files + allow_new: a list of sections that permit children to be added + regardless of whether those children existed in the first + config file + """ self.parser_options["list_values"] = False @@ -289,14 +301,4 @@ def has_section(self, *path): if isinstance(cfg, Section): return True - return False - - -if __name__ == '__main__': - c = CascadingConfigParser(["../configs/default.cfg", "../configs/test.cfg"], allow_new=["Temperature Control"]) - - print c["Fans"] - - #c.save("") - - #print c#["Temperature Control"] \ No newline at end of file + return False \ No newline at end of file diff --git a/redeem/Fan.py b/redeem/Fan.py index 38874ac9..461ca140 100644 --- a/redeem/Fan.py +++ b/redeem/Fan.py @@ -32,12 +32,13 @@ class Fan(Unit): + """ + Used to move air + """ def __init__(self, name, options, printer): """ - channel : channel that this fan is on - fan_id : number of the fan - printer : description of this printer + Fan initialization. """ self.name = name @@ -61,12 +62,14 @@ def __init__(self, name, options, printer): return def connect(self, units): + """ Connect this unit to other units""" if self.input: self.input = self.get_unit(self.input, units) if not self.input.output: self.input.output = self def check(self): + """ Perform any checks or logging after all connections are made""" logging.info("{} --> {}".format(self.input, self.name)) diff --git a/redeem/Heater.py b/redeem/Heater.py index 722a2695..67e804f9 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -30,8 +30,8 @@ class Heater(Unit): """ - A heater element that must keep temperature, - either an extruder, a HBP or could even be a heated chamber + Controls the temperature of a heater element by modulating the power of an + attached MOSFET """ def __init__(self, name, options, printer): """ Init """ diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 4b16cde5..c41988a4 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -40,7 +40,12 @@ #============================================================================== class CircularBuffer(object): - #https://stackoverflow.com/a/40784706 + """ + A list of specified size that overwrites the oldest data when space + runs out. + + https://stackoverflow.com/a/40784706 + """ def __init__(self, size): """Initialization""" self.index = 0 @@ -56,6 +61,7 @@ def append(self, value): self.index = (self.index + 1) % self.size def get_length(self): + """ Get the current length of the list""" return len(self._data) def __getitem__(self, key): @@ -70,6 +76,9 @@ def __repr__(self): return self._data.__repr__() + ' (' + str(len(self._data))+' items)' class Unit: + """ + Base component of all temperature control units + """ printer = None counter = 0 @@ -104,7 +113,7 @@ def get_unit(self, name, units): return def initialise(self): - """ stuff to do after connecting""" + """ perform post connection initialization""" return def check(self): @@ -116,8 +125,12 @@ def __str__(self): class Alias(Unit): + """ + Used as an alias for another unit + """ def __init__(self, name, options, printer): + """ initialize the unit """ self.name = name self.options = options @@ -130,17 +143,25 @@ def __init__(self, name, options, printer): return def connect(self, units): + """ connect to other units """ self.input = self.get_unit(self.input, units) def get_value(self): + """ return the current value """ return self.input.get_value() def check(self): + """ perform any checks or logging after full connection """ logging.info("{} --> {} ".format(self.input, self.name)) class Compare(Unit): + """ + Perform an operation on two inputs + """ + def __init__(self, name, options, printer): + """ initialize the unit """ self.name = name self.options = options self.printer = printer @@ -154,31 +175,43 @@ def __init__(self, name, options, printer): return def connect(self, units): - + """ connect to other units """ for i in range(2): self.input[i] = self.get_unit(self.input[i], units) def check(self): + """ perform any checks or logging after full connection """ logging.info("({} and {}) --> {}".format(self.input[0].name, self.input[1].name, self.name)) class Difference(Compare): + """ Calculate the difference between inputs""" def get_value(self): + """ return the current value """ return self.input[0].get_value() - self.input[1].get_value() class Maximum(Compare): + """ Calculate the maximum of two inputs""" def get_value(self): + """ return the current value """ return max(self.input[0].get_value(), self.input[1].get_value()) class Minimum(Compare): + """ Calculate the minimum of two inputs""" def get_value(self): + """ return the current value """ return min(self.input[0].get_value(), self.input[1].get_value()) class Safety(Unit): + """ + Perform safety related checks based on the temperature of the attached + sensor and raise appropriate alarms + """ def __init__(self, name, options, printer): + """ initialize the unit """ self.name = name self.options = options self.printer = printer @@ -188,9 +221,9 @@ def __init__(self, name, options, printer): self.min_temp = float(self.options["min_temp"]) # If temperature falls below this point from the target, disable. self.max_temp = float(self.options["max_temp"]) # Max temp that can be reached before disabling printer. - self.max_temp_rise = float(self.options["max_rise_temp"]) # Fastest temp can rise pr measrement - self.max_temp_fall = float(self.options["max_fall_temp"]) # Fastest temp can fall pr measurement - self.min_temp_rise = float(self.options["min_rise_temp"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor + self.max_rise_rate = float(self.options["max_rise_rate"]) # Fastest temp can rise pr measrement + self.max_fall_rate = float(self.options["max_fall_rate"]) # Fastest temp can fall pr measurement + self.min_rise_rate = float(self.options["min_rise_rate"]) # Slowest temp can rise pr measurement, to catch incorrect attachment of thermistor self.min_rise_offset = float(self.options["min_rise_offset"]) # Allow checking for slow temp rise when temp is below this offset from target temp self.min_rise_delay = float(self.options["min_rise_delay"]) # Allow checking for slow temp rise after this delay time to allow for heat soak @@ -199,12 +232,13 @@ def __init__(self, name, options, printer): return def connect(self, units): + """ connect to other units """ self.input = self.get_unit(self.input, units) self.heater = self.get_unit(self.heater, units) return def initialise(self): - + """ perform post connection initialization""" # insert into the attached heater, if it isn't already there if not self.heater.safety: self.heater.safety = [self] @@ -232,6 +266,7 @@ def initialise(self): return def check(self): + """ perform any checks or logging after full connection """ logging.info("{} --> {} --> {}".format(self.input.name, self.name, self.heater.name)) def set_min_temp_enabled(self, flag): @@ -272,13 +307,13 @@ def run_safety_checks(self): heating_time = current_time - self.start_heating_time # Check that temperature is not rising too quickly - if temp_rate > self.max_temp_rise: + if temp_rate > self.max_rise_rate: a = Alarm(Alarm.HEATER_RISING_FAST, "Temperature rising too quickly ({:.2f} degrees/sec) for {} ({} = {:.2f})".format(temp_rate, self.name, self.input.name, current_temp)) # Check that temperature is not rising quickly enough when power is applied - check = [temp_rate < self.min_temp_rise, + check = [temp_rate < self.min_rise_rate, power_on, current_temp < (target_temp - self.min_rise_offset), heating_time > self.min_rise_delay] @@ -286,7 +321,7 @@ def run_safety_checks(self): a = Alarm(Alarm.HEATER_RISING_SLOW, "Temperature rising too slowly ({:.2f} degrees/sec) for {} ({} = {:.2f})".format(temp_rate, self.name, self.input.name, current_temp)) # Check that temperature is not falling too quickly - if temp_rate < -self.max_temp_fall: + if temp_rate < -self.max_fall_rate: a = Alarm(Alarm.HEATER_FALLING_FAST, "Temperature falling too quickly ({:.2f} degrees/sec) for {} ({} = {:.2f})".format(temp_rate, self.name, self.input.name, current_temp)) # Check that temperature has not fallen below a certain setpoint from target @@ -308,8 +343,12 @@ def run_safety_checks(self): class Control(Unit): + """ + Control schemes base class + """ def __init__(self, name, options, printer): + """ initialize the unit """ self.name = name self.options = options self.printer = printer @@ -327,10 +366,12 @@ def __init__(self, name, options, printer): return def get_options(self): + """ retrieve options from config""" return def connect(self, units): + """ connect to other units """ self.input = self.get_unit(self.input, units) if self.output: self.output = self.get_unit(self.output, units) @@ -339,17 +380,23 @@ def connect(self, units): return def reset(self): + """ reset any historical data """ return - def check(self): + def check(self): + """ perform any checks or logging after full connection """ logging.info("{} --> {} --> {}".format(self.input, self.name, self.output)) class ConstantControl(Control): + """ + Return a constant value for control applications + """ feedback_control = False def get_options(self): + """ retrieve options from config""" self.output = None if "output" in self.options: @@ -359,18 +406,22 @@ def get_options(self): return def connect(self, units): + """ connect to other units """ if self.output: self.output = self.get_unit(self.output, units) self.output.input = self def get_value(self): + """ return the current value """ return self.value def set_target_value(self, value): + """ set the desired value """ self.value = float(value) def ramp_to(self, value, delay): + """ ramp the control output up to 'value' by 1/255 every 'delay' seconds""" save_sleep = self.sleep self.sleep = delay/2.0 for w in range(int(self.value*255.0), int(value*255.0), (1 if value >= self.value else -1)): @@ -381,6 +432,7 @@ def ramp_to(self, value, delay): def check(self): + """ perform any checks or logging after full connection """ logging.info("{} --> {} --> {}".format(self.value, self.name, self.output)) @@ -391,6 +443,7 @@ class CommandCode(ConstantControl): """ def get_options(self): + """ retrieve options from config""" self.command = [c.strip() for c in self.options["command"].split(",")] for command in self.command: if command in self.printer.command_connect: @@ -405,11 +458,13 @@ def get_options(self): def connect(self, units): + """ connect to other units """ for i, output in enumerate(self.output): self.output[i] = self.get_unit(output, units) self.output[i].input = self def check(self): + """ perform any checks or logging after full connection """ outputs = "[" for output in self.output: outputs += "{}, ".format(output) @@ -421,10 +476,14 @@ def __str__(self): class OnOffControl(Control): + """ + Control by switching between two defined states + """ feedback_control = True def get_options(self): + """ retrieve options from config""" self.input = self.options["input"] self.output = None if "output" in self.options: @@ -457,7 +516,7 @@ def get_target_value(self): return self.target_value def get_value(self): - + """ return the current value """ value = self.input.get_value() if value <= self.on_value: @@ -469,11 +528,15 @@ def get_value(self): class ProportionalControl(Control): + """ + Control output in proportion to the instantaneous error between current + and target value + """ feedback_control = True def get_options(self): - """ Init """ + """ retrieve options from config""" self.input = self.options["input"] self.output = None if "output" in self.options: @@ -496,7 +559,7 @@ def get_target_value(self): return self.target_value def get_value(self): - """ PID Thread that keeps the value stable """ + """ return the current value based on proportional (P) control""" self.current_value = self.input.get_value() error = self.target_value-self.current_value @@ -514,10 +577,15 @@ def get_value(self): return value class PIDControl(Control): + """ + Control output according to proportional (P), integral (I) and + derivative (D) terms. + """ feedback_control = True def get_options(self): + """ retrieve options from config""" self.input = self.options["input"] self.output = None if "output" in self.options: @@ -533,7 +601,7 @@ def get_options(self): return def initialise(self): - + """ perform post connection initialization""" self.avg = max(int(1.0/self.sleep), 3) self.error = 0 self.errors = [0]*self.avg @@ -557,7 +625,7 @@ def get_target_value(self): def get_value(self): - + """ return the current value based on PID control""" current_value = self.input.get_value() self.values.append(current_value) self.values[:-max(int(60/self.sleep), self.avg)] = [] # Keep only this much history @@ -593,6 +661,7 @@ def get_error_integral(self): return self.error_integral def reset(self): + """ reset any historical values """ self.error_integral = 0.0 From 5562d74264460ca61f4b809185a2df36f85ee257 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 19 Jan 2018 14:48:09 +0000 Subject: [PATCH 20/27] changed import so that any fault throws an error that is caught in Redeem.py such that config reverts to default --- redeem/Heater.py | 29 ++++++++++++------------ redeem/Redeem.py | 44 +++++++++++++++++++++++++----------- redeem/TemperatureControl.py | 12 +++++++--- 3 files changed, 55 insertions(+), 30 deletions(-) diff --git a/redeem/Heater.py b/redeem/Heater.py index 67e804f9..6fbc9c55 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -72,8 +72,9 @@ def connect(self, units): # connect the controller if self.input: self.input = self.get_unit(self.input, units) - if not self.input.output: - self.input.output = self + if self.check_input(): + if not self.input.output: + self.input.output = self #connect the safety @@ -99,13 +100,17 @@ def check(self): """ run checks""" if not self.input: - logging.warning("{} is unconnected".format(self.name)) - self.heater_error = True + raise RuntimeError("{} has no input".format(self.name)) if not self.safety: - self.mosfet.set_power(0.0) - logging.warning("{} has no safety connected, heater disabled".format(self.name)) - self.heater_error = True + raise RuntimeError("{} has no safety connected".format(self.name)) + + self.check_input() + + return + + def check_input(self): + """ check the input is valid """ # ensure the controller is one that allows feedback, i.e. not open loop allow = False @@ -113,11 +118,10 @@ def check(self): if self.input.feedback_control: allow = True if not allow: - self.mosfet.set_power(0.0) - logging.error("{} has non-feedback control assigned, heater disabled".format(self.name)) - self.heater_error = True + raise RuntimeError("{} has non-feedback control".format(self.name)) + + return allow - return def set_target_temperature(self, temp): """ Set the target temperature of the controller """ @@ -197,9 +201,6 @@ def disable(self): def enable(self): """ Start the PID controller """ - if self.heater_error: - self.enabled = False - return self.avg = max(int(1.0/self.input.sleep), 3) self.prev_time = self.current_time = time.time() self.temperatures = [self.input.input.get_value()] diff --git a/redeem/Redeem.py b/redeem/Redeem.py index 27326f72..cd26813b 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -354,7 +354,7 @@ def initialize(self, configs=[]): "gcode":CommandCode} # generate units - all_built = True + success = True units = {} for section in ["Temperature Control", "Fans", "Heaters"]: cfg = self.printer.config[section] @@ -375,27 +375,45 @@ def initialize(self, configs=[]): msg = "Configuration section '{}' failed to build. Have you defined {}?".format(name, str(e)) self.config_error.append(msg) logging.error(msg, exc_info=True) - all_built = False - - if not all_built: - logging.warning("Control unit/s failed. Using default.cfg only.") - self.initialize(configs = [configs[0]]) # re-run this method but only on default.cfg - return - # ^^^ this means that even if we stuff something up, redeem will not crash - # it will probably be basically unusable, but it won't crash. - + success = False # connect units for name, unit in units.items(): - unit.connect(units) + try: + unit.connect(units) + except: + msg = "{} failed when connecting:".format(name) + self.config_error.append(msg) + logging.error(msg, exc_info=True) + success = False # initialise units + all_initialised = True for name, unit in units.items(): - unit.initialise() + try: + unit.initialise() + except: + msg = "{} failed when initializing:".format(name) + self.config_error.append(msg) + logging.error(msg, exc_info=True) + success = False # run checks for name, unit in units.items(): - unit.check() + try: + unit.check() + except: + msg = "{} failed when performing checks:".format(name) + self.config_error.append(msg) + logging.error(msg, exc_info=True) + success = False + + if not success: + logging.warning("Temperature control configuration invalid\n\n\n Restarting Redeem with default.cfg only \n") + self.initialize(configs = [configs[0]]) # re-run this method but only on default.cfg + return + # ^^^ this means that even if we stuff something up, redeem will not crash + # it will probably be basically unusable, but it won't crash. # turn on the fans and heaters diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index c41988a4..98945751 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -249,16 +249,22 @@ def initialise(self): if isinstance(self.input, Alias): input_sensor = self.input.input + disconnect = False if (not isinstance(input_sensor, TemperatureSensor)) and (not isinstance(input_sensor, ColdEnd)): - msg = "{} will not work, input = {} is not a temperature sensor".format(self.name, self.input.name) - logging.error(msg) + msg = "{} disabled. {} is not a temperature sensor".format(self.name, self.input.name) + logging.warning(msg) + disconnect = True - # disconnect from the heater + # disconnect from the heater + if disconnect: for i, s in enumerate(self.heater.safety): if self == s: self.heater.safety.pop(i) break + return + + self.avg = max(int(1.0/self.heater.input.sleep), 5) self.temp = CircularBuffer(self.avg) self.time = CircularBuffer(self.avg) From c6e269792149fb70655f4a284c3b641d6d5e3c23 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 19 Jan 2018 15:08:43 +0000 Subject: [PATCH 21/27] fixed double log entries when re-running config --- redeem/Redeem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/redeem/Redeem.py b/redeem/Redeem.py index cd26813b..208becc1 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -78,6 +78,7 @@ class Redeem: + logger = None def __init__(self, config_location="/etc/redeem"): """ config_location: provide the location to look for config files. @@ -150,7 +151,8 @@ def initialize(self, configs=[]): # Get the revision and loglevel from the Config file level = self.printer.config.getint('System', 'loglevel') if level > 0: - logging.getLogger().setLevel(level) + if not self.logger: + self.logger = logging.getLogger().setLevel(level) # Set up additional logging, if present: if self.printer.config.getboolean('System', 'log_to_file'): From ef057d2f9c3e60b307d146139b12b4ae81820c95 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Fri, 19 Jan 2018 15:08:43 +0000 Subject: [PATCH 22/27] fixed double log entries when re-running config --- redeem/Redeem.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/redeem/Redeem.py b/redeem/Redeem.py index cd26813b..e3b35e02 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -75,8 +75,7 @@ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M') - - + class Redeem: def __init__(self, config_location="/etc/redeem"): """ @@ -85,6 +84,8 @@ def __init__(self, config_location="/etc/redeem"): - allows for running in a local directory when debugging """ + self.logging = False + self.config_location = config_location # check for config files @@ -147,20 +148,23 @@ def initialize(self, configs=[]): printer.config = CascadingConfigParser(configs, allow_new = ["Temperature Control"]) # <-- this is where users are allowed to add stuff to the config - # Get the revision and loglevel from the Config file - level = self.printer.config.getint('System', 'loglevel') - if level > 0: - logging.getLogger().setLevel(level) - - # Set up additional logging, if present: - if self.printer.config.getboolean('System', 'log_to_file'): - logfile = self.printer.config.get('System', 'logfile') - formatter = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' - printer.redeem_logging_handler = logging.handlers.RotatingFileHandler(logfile, maxBytes=2*1024*1024) - printer.redeem_logging_handler.setFormatter(logging.Formatter(formatter)) - printer.redeem_logging_handler.setLevel(level) - logging.getLogger().addHandler(printer.redeem_logging_handler) - logging.info("-- Logfile configured --") + if not self.logging: + # Get the revision and loglevel from the Config file + level = self.printer.config.getint('System', 'loglevel') + if level > 0: + logging.getLogger().setLevel(level) + + # Set up additional logging, if present: + if self.printer.config.getboolean('System', 'log_to_file'): + logfile = self.printer.config.get('System', 'logfile') + formatter = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s' + printer.redeem_logging_handler = logging.handlers.RotatingFileHandler(logfile, maxBytes=2*1024*1024) + printer.redeem_logging_handler.setFormatter(logging.Formatter(formatter)) + printer.redeem_logging_handler.setLevel(level) + logging.getLogger().addHandler(printer.redeem_logging_handler) + logging.info("-- Logfile configured --") + + self.logging = True # Find out which capes are connected self.printer.config.parse_capes() From 8186c39f413c351f9aea1d5a3366588d50c030ad Mon Sep 17 00:00:00 2001 From: darylbond Date: Sun, 21 Jan 2018 11:16:33 +1000 Subject: [PATCH 23/27] Update default.cfg --- configs/default.cfg | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/configs/default.cfg b/configs/default.cfg index c4e998da..5cac57db 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -214,7 +214,13 @@ arc_segment_length = 0.001 e_axis_active = True [Temperature Control] -# put temperature control units in here, they won't be accepted anywhere else! +# Thermal management is implemented in Redeem through a user configurable network +# of sensors, heaters and fans. The user specifies the nodes of this network in this +# section of the configuration file. Each node is a uniquely configured instance +# from a pre-defined set, all of which are shown below. This approach allows +# for a high degree of flexibility in setting up when fans turn on/off, the +# type of control logic that is used for each heater/fan, and even allowing +# multiple sensors to control their behaviour. # # allowed types are : alias, difference, maximum, minimum, constant-control, # on-off-control, pid-control, proportional-control, safety, gcode From 0bf276053751c3890fd60fd81dfc416cee4a3ce7 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sun, 21 Jan 2018 02:34:23 +0000 Subject: [PATCH 24/27] added on-off regime to PID control as a fix for temp overshoot. Fixed copy-paste error in default.cfg --- configs/default.cfg | 3 ++- redeem/TemperatureControl.py | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index 5cac57db..6a2826e4 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -257,7 +257,7 @@ e_axis_active = True #type=on-off-control #target_value = #on_offset = -#off_offset = +#off_offset = = target + off_offset> #on_value = #off_value = #sleep = @@ -283,6 +283,7 @@ e_axis_active = True #pid_Ti = #pid_Td = #ok_range = +#on_off_range = #max_value = #sleep = # diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 98945751..8b54deb1 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -604,6 +604,10 @@ def get_options(self): self.max_value = min(1.0, float(self.options['max_value'])/255.0) self.sleep = float(self.options['sleep']) + self.on_off_range = np.inf + if "on_off_range" in self.options: + self.on_off_range = float(self.options['on_off_range']) + return def initialise(self): @@ -639,12 +643,18 @@ def get_value(self): self.error = self.target_value-current_value self.errors.append(self.error) self.errors.pop(0) - - derivative = self.get_error_derivative() - integral = self.get_error_integral() - # The standard formula for the PID - value = self.Kp*(self.error + (1.0/self.Ti)*integral + self.Td*derivative) - value = max(min(value, self.max_value, 1.0), 0.0) # Normalize to 0, max + + if self.error > self.on_off_range: + # on off control + self.reset() + value = self.max_value + else: + # pid control + derivative = self.get_error_derivative() + integral = self.get_error_integral() + # The standard formula for the PID + value = self.Kp*(self.error + (1.0/self.Ti)*integral + self.Td*derivative) + value = max(min(value, self.max_value, 1.0), 0.0) # Normalize to 0, max return value @@ -672,4 +682,3 @@ def reset(self): self.error_integral = 0.0 return - From c056c80baf1cece107eadd5c0f2681b7e2ec8fa9 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Sun, 21 Jan 2018 03:58:58 +0000 Subject: [PATCH 25/27] fix error in OnOffControl where off_value was overwritten --- configs/default.cfg | 2 +- redeem/TemperatureControl.py | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index 6a2826e4..420d347e 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -229,7 +229,7 @@ e_axis_active = True # #[[AliasUnitName]] #type = alias -#input = +#input = # #[[ComparisonUnitName]] #type = difference, maximum, or minimum diff --git a/redeem/TemperatureControl.py b/redeem/TemperatureControl.py index 8b54deb1..e598799b 100644 --- a/redeem/TemperatureControl.py +++ b/redeem/TemperatureControl.py @@ -501,8 +501,7 @@ def get_options(self): self.off_value = float(self.options['off_value'])/255.0 self.sleep = float(self.options['sleep']) - self.on_value = self.target_value + self.on_offset - self.off_value = self.target_value + self.off_offset + self.ok_range = abs(self.on_offset) self.value = self.off_value @@ -510,11 +509,7 @@ def get_options(self): def set_target_value(self, value): """ set the target value """ - self.target_value = float(value) - self.on_value = self.target_value + self.on_offset - self.off_value = self.target_value + self.off_offset - return def get_target_value(self): @@ -525,9 +520,9 @@ def get_value(self): """ return the current value """ value = self.input.get_value() - if value <= self.on_value: + if value <= (self.target_value + self.on_offset): self.value = self.max_value - elif value >= self.off_value: + elif value >= (self.target_value + self.off_offset): self.value = self.off_value return self.value From bc3af4bf9076c47e68d8cd57bc9bcdabe68696f1 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Tue, 23 Jan 2018 14:17:11 +0000 Subject: [PATCH 26/27] ensure that heating has stabilized before flagging it as finished --- redeem/gcodes/M116.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/redeem/gcodes/M116.py b/redeem/gcodes/M116.py index 3425cf83..c15837aa 100644 --- a/redeem/gcodes/M116.py +++ b/redeem/gcodes/M116.py @@ -46,15 +46,16 @@ def execute(self, g): has_parameter and heater_index != 4 ]) + stable_time = 3.0 while True: - all_ok[0] |= self.printer.heaters['E'].is_target_temperature_reached() - all_ok[1] |= self.printer.heaters['H'].is_target_temperature_reached() - all_ok[2] |= self.printer.heaters['HBP'].is_target_temperature_reached() + all_ok[0] |= self.printer.heaters['E'].is_temperature_stable(stable_time) + all_ok[1] |= self.printer.heaters['H'].is_temperature_stable(stable_time) + all_ok[2] |= self.printer.heaters['HBP'].is_temperature_stable(stable_time) if self.printer.config.reach_revision: - all_ok[3] |= self.printer.heaters['A'].is_target_temperature_reached() - all_ok[4] |= self.printer.heaters['B'].is_target_temperature_reached() - all_ok[5] |= self.printer.heaters['C'].is_target_temperature_reached() + all_ok[3] |= self.printer.heaters['A'].is_temperature_stable(stable_time) + all_ok[4] |= self.printer.heaters['B'].is_temperature_stable(stable_time) + all_ok[5] |= self.printer.heaters['C'].is_temperature_stable(stable_time) m105 = Gcode({"message": "M105", "parent": g}) self.printer.processor.execute(m105) From 8fb218c2dfb23f9b8114bcfb0eb9c12340f5db07 Mon Sep 17 00:00:00 2001 From: Daryl Bond Date: Wed, 24 Jan 2018 06:45:28 +0000 Subject: [PATCH 27/27] add time over which to check for stable heating as a parameter in heater config --- configs/default.cfg | 6 ++++++ redeem/Heater.py | 6 +++++- redeem/gcodes/M116.py | 12 ++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/configs/default.cfg b/configs/default.cfg index 420d347e..03e417b8 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -535,6 +535,7 @@ mosfet = 5 prefix = T0 input = Control-E safety = Safety-E +stable_time = 5 [[Heater-H]] @@ -543,6 +544,7 @@ mosfet = 3 prefix = T1 input = Control-H safety = Safety-H +stable_time = 5 [[Heater-A]] @@ -551,6 +553,7 @@ mosfet = 11 prefix = T2 input = Control-A safety = Safety-A +stable_time = 5 [[Heater-B]] @@ -559,6 +562,7 @@ mosfet = 12 prefix = T3 input = Control-B safety = Safety-B +stable_time = 5 [[Heater-C]] @@ -567,6 +571,7 @@ mosfet = 13 prefix = T4 input = Control-C safety = Safety-C +stable_time = 5 [[Heater-HBP]] @@ -575,6 +580,7 @@ mosfet = 4 prefix = B input = Control-HBP safety = Safety-HBP +stable_time = 5 diff --git a/redeem/Heater.py b/redeem/Heater.py index 6fbc9c55..06bb5ebb 100644 --- a/redeem/Heater.py +++ b/redeem/Heater.py @@ -42,6 +42,7 @@ def __init__(self, name, options, printer): self.mosfet = self.options["mosfet"] self.prefix = self.options["prefix"] + self.stable_time = float(self.options["stable_time"]) self.safety = None if self.safety in self.options: @@ -155,8 +156,11 @@ def is_target_temperature_reached(self): reached = err < self.input.ok_range return reached - def is_temperature_stable(self, seconds=10): + def is_temperature_stable(self, seconds=None): """ Returns true if the temperature has been stable for n seconds """ + if not seconds: + seconds = self.stable_time + target_temp = self.get_target_temperature() ok_range = self.input.ok_range if len(self.temperatures) < int(seconds/self.input.sleep): diff --git a/redeem/gcodes/M116.py b/redeem/gcodes/M116.py index c15837aa..64fd37eb 100644 --- a/redeem/gcodes/M116.py +++ b/redeem/gcodes/M116.py @@ -48,14 +48,14 @@ def execute(self, g): stable_time = 3.0 while True: - all_ok[0] |= self.printer.heaters['E'].is_temperature_stable(stable_time) - all_ok[1] |= self.printer.heaters['H'].is_temperature_stable(stable_time) - all_ok[2] |= self.printer.heaters['HBP'].is_temperature_stable(stable_time) + all_ok[0] |= self.printer.heaters['E'].is_temperature_stable() + all_ok[1] |= self.printer.heaters['H'].is_temperature_stable() + all_ok[2] |= self.printer.heaters['HBP'].is_temperature_stable() if self.printer.config.reach_revision: - all_ok[3] |= self.printer.heaters['A'].is_temperature_stable(stable_time) - all_ok[4] |= self.printer.heaters['B'].is_temperature_stable(stable_time) - all_ok[5] |= self.printer.heaters['C'].is_temperature_stable(stable_time) + all_ok[3] |= self.printer.heaters['A'].is_temperature_stable() + all_ok[4] |= self.printer.heaters['B'].is_temperature_stable() + all_ok[5] |= self.printer.heaters['C'].is_temperature_stable() m105 = Gcode({"message": "M105", "parent": g}) self.printer.processor.execute(m105)