diff --git a/configs/default.cfg b/configs/default.cfg index c51838dc..03e417b8 100644 --- a/configs/default.cfg +++ b/configs/default.cfg @@ -213,158 +213,376 @@ 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 +[Temperature Control] +# 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 +# +# 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 = = target + 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 = +#on_off_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 +[[Control-E]] +type = pid-control +input = Thermistor-E +target_value = 0.0 +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +max_value = 255 +sleep = 0.25 + + +[[Control-H]] +type = pid-control +input = Thermistor-H +target_value = 0.0 +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +max_value = 255 +sleep = 0.25 + + +[[Control-A]] +type = pid-control +input = Thermistor-A +target_value = 0.0 +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +max_value = 255 +sleep = 0.25 + + +[[Control-B]] +type = pid-control +input = Thermistor-B +target_value = 0.0 +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +max_value = 255 +sleep = 0.25 + + +[[Control-C]] +type = pid-control +input = Thermistor-C +target_value = 0.0 +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +max_value = 255 +sleep = 0.25 + + +[[Control-HBP]] +type = pid-control +input = Thermistor-HBP +target_value = 0.0 +pid_Kp = 0.1 +pid_Ti = 100.0 +pid_Td = 0.3 +ok_range = 4.0 +max_value = 255 +sleep = 0.5 + +# 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 + +[[Safety-E]] +type = safety +max_rise_rate = 10.0 +max_fall_rate = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_rate = 0.1 +min_rise_offset = 20 +min_rise_delay = 1 +input = Thermistor-E +heater = Heater-E + +[[Safety-H]] +type = safety +max_rise_rate = 10.0 +max_fall_rate = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_rate = 0.1 +min_rise_offset = 20 +min_rise_delay = 1 +input = Thermistor-H +heater = Heater-H + +[[Safety-A]] +type = safety +max_rise_rate = 10.0 +max_fall_rate = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_rate = 0.1 +min_rise_offset = 20 +min_rise_delay = 1 +input = Thermistor-A +heater = Heater-A + +[[Safety-B]] +type = safety +max_rise_rate = 10.0 +max_fall_rate = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_rate = 0.1 +min_rise_offset = 20 +min_rise_delay = 1 +input = Thermistor-B +heater = Heater-B + +[[Safety-C]] +type = safety +max_rise_rate = 10.0 +max_fall_rate = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_rate = 0.1 +min_rise_offset = 20 +min_rise_delay = 1 +input = Thermistor-C +heater = Heater-C + +[[Safety-HBP]] +type = safety +max_rise_rate = 10.0 +max_fall_rate = 10.0 +min_temp = 20.0 +max_temp = 250.0 +min_rise_rate = 0.1 +min_rise_offset = 20 +min_rise_delay = 1 +input = Thermistor-HBP +heater = Heater-HBP + + +[Thermistors] + +# Thermistors for measuring temperature +# For list of available temp charts, look in temp_chart.py + +[[Thermistor-E]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage4_raw + + +[[Thermistor-H]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage5_raw + + +[[Thermistor-A]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage0_raw + + +[[Thermistor-B]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage3_raw + + +[[Thermistor-C]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage2_raw + + +[[Thermistor-HBP]] +sensor = B57560G104F +path_adc = /sys/bus/iio/devices/iio:device0/in_voltage6_raw + + [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 + +# 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 +channel = 0 +input = 0 + +[[Fan-1]] +type=fan +channel = 0 +input = 0 + +[[Fan-2]] +type = fan +channel = 0 +input = 0 + +[[Fan-3]] +type = fan +channel = 0 +input = 0 [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] +# can have multiple safety units assigned to one heater i.e. safety = Safety-1, Safety-2 + +[[Heater-E]] +type = heater +mosfet = 5 +prefix = T0 +input = Control-E +safety = Safety-E +stable_time = 5 + + +[[Heater-H]] +type = heater +mosfet = 3 +prefix = T1 +input = Control-H +safety = Safety-H +stable_time = 5 + + +[[Heater-A]] +type = heater +mosfet = 11 +prefix = T2 +input = Control-A +safety = Safety-A +stable_time = 5 + + +[[Heater-B]] +type = heater +mosfet = 12 +prefix = T3 +input = Control-B +safety = Safety-B +stable_time = 5 + + +[[Heater-C]] +type = heater +mosfet = 13 +prefix = T4 +input = Control-C +safety = Safety-C +stable_time = 5 + + +[[Heater-HBP]] +type = heater +mosfet = 4 +prefix = B +input = Control-HBP +safety = Safety-HBP +stable_time = 5 + + [Endstops] # Which axis should be homed. @@ -548,7 +766,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 @@ -580,13 +798,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/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 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: diff --git a/redeem/Alarm.py b/redeem/Alarm.py index ed5dcd76..c0a461bf 100644 --- a/redeem/Alarm.py +++ b/redeem/Alarm.py @@ -39,6 +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 # Temperature is rising too slow + CONFIG_ERROR = 13 # error when importing config printer = None executor = None @@ -56,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 """ @@ -84,6 +88,11 @@ def execute(self): self.inform_listeners() Alarm.action_command("pause") Alarm.action_command("alarm_heater_falling_fast", self.message) + elif self.type == Alarm.HEATER_RISING_SLOW: + self.disable_heaters() + 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") @@ -109,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!") @@ -123,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/CascadingConfigParser.py b/redeem/CascadingConfigParser.py index 983f48c9..409b3683 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,101 @@ along with Redeem. If not, see . """ -import ConfigParser +from configobj import OPTION_DEFAULTS, ConfigObj, Section import os import logging import struct +import copy +#============================================================================== +# 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): + """ + 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 + + 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 +120,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 +170,65 @@ 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) + self.default_cfg = self.dict() + 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 +247,58 @@ def get_key(self): except IOError as e: logging.warning("Unable to write new key to EEPROM") return self.replicape_key - - -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()) + + 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 \ No newline at end of file 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/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 index 1b3a0ead..461ca140 100644 --- a/redeem/Fan.py +++ b/redeem/Fan.py @@ -1,9 +1,9 @@ #!/usr/bin/env python """ -A fan is for blowing stuff away. This one is for Replicape. +For running a fan. -Author: Elias Bakken -email: elias(dot)bakken(at)gmail(dot)com +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 @@ -24,13 +24,55 @@ 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 +from configobj import Section +import logging +from threading import Thread + +from TemperatureControl import Unit, ConstantControl + + +class Fan(Unit): + """ + Used to move air + """ + + def __init__(self, name, options, printer): + """ + Fan initialization. + """ + + self.name = name + self.options = options + self.printer = printer + + self.input = None + if "input" in self.options: + self.input = self.options["input"] + + self.channel = int(self.options["channel"]) + + # 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): + """ 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)) + + def set_PWM_frequency(self, value): """ Set the amount of on-time from 0..1 """ @@ -41,6 +83,7 @@ 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): @@ -51,33 +94,38 @@ def ramp_to(self, value, delay=0.01): 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) - - + def run_controller(self): + """ follow a target PWM value 0..1""" + + while self.enabled: + self.set_value(self.input.get_value()) + time.sleep(self.input.sleep) + 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 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 + + def __str__(self): + return self.name \ No newline at end of file diff --git a/redeem/Heater.py b/redeem/Heater.py new file mode 100644 index 00000000..06bb5ebb --- /dev/null +++ b/redeem/Heater.py @@ -0,0 +1,254 @@ +""" +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): + """ + Controls the temperature of a heater element by modulating the power of an + attached MOSFET + """ + def __init__(self, name, options, printer): + """ Init """ + + self.name = name + self.options = options + self.printer = 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: + self.safety = [s.strip() for s in options["safety"].split(",")] + + self.min_temp_enabled = False # Temperature error limit + + self.input = None + if "input" in self.options: + self.input = self.options["input"] + + self.heater_error = False + + 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.short_name] + + # connect the controller + if self.input: + self.input = self.get_unit(self.input, units) + if self.check_input(): + if not self.input.output: + self.input.output = self + + + #connect the safety + 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""" + + 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: + raise RuntimeError("{} has no input".format(self.name)) + + if not self.safety: + 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 + if isinstance(self.input, Control): + if self.input.feedback_control: + allow = True + if not allow: + raise RuntimeError("{} has non-feedback control".format(self.name)) + + return allow + + + def set_target_temperature(self, temp): + """ Set the target temperature of the controller """ + self.min_temp_enabled = False + self.input.set_target_value(temp) + + def get_temperature(self): + """ get the temperature of the thermistor and the control input""" + 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 target temperature""" + return self.input.get_target_value() + + def is_target_temperature_reached(self): + """ Returns true if the target temperature is reached """ + + current_temp = self.temperatures[-1] + target_temp = self.get_target_temperature() + + if target_temp == 0: + return True + if current_temp == 0: + self.set_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=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): + return False + if max(self.temperatures[-int(seconds/self.input.sleep):]) > (target_temp + ok_range): + return False + if min(self.temperatures[-int(seconds/self.input.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.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.set_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 """ + 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()] + 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 + 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_value() + self.temperatures.append(temp) + self.temperatures[:-max(int(60/sleep), self.avg)] = [] # Keep only this much history + + # Run safety checks + + if not self.heater_error: + self.check_temperature_error() + + # Set temp if temperature is OK + if not self.heater_error: + self.mosfet.set_power(value) + else: + self.mosfet.set_power(0) + time.sleep(sleep) + 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 """ + + 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/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/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/Printer.py b/redeem/Printer.py index 88bc5b12..c61ffebb 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 = {} @@ -53,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 @@ -233,6 +235,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/PruFirmware.py b/redeem/PruFirmware.py index a5eb8704..148fec58 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, @@ -254,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 4901e53b..6fbfc039 100755 --- a/redeem/Redeem.py +++ b/redeem/Redeem.py @@ -34,19 +34,19 @@ from multiprocessing import JoinableQueue import Queue import numpy as np -import sys +import sys, traceback from Mosfet import Mosfet from Stepper import * from TemperatureSensor import * +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 @@ -75,18 +75,62 @@ 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"): """ config_location: provide the location to look for config files. - default is installed directory - allows for running in a local directory when debugging """ + + self.logging = False + + 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,51 +138,34 @@ 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')]) - - # 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')) - - # 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 --") + printer.config = CascadingConfigParser(configs, + allow_new = ["Temperature Control"]) # <-- this is where users are allowed to add stuff to the config + + 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() @@ -189,6 +216,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 +265,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 +290,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") @@ -267,72 +300,138 @@ 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) - - # Make Mosfets, temperature sensors and extruders + + # 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 + + # 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: - # 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) + name = "Thermistor-{}".format(e) + adc = self.printer.config.get("Thermistors", name, "path_adc") + sensor = self.printer.config.get("Thermistors", name, "sensor") + self.printer.thermistors[e] = TemperatureSensor(adc, name, 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) - - # Init the three fans. Argument is PWM channel number - 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)) - elif self.revision == "0A4A": - self.printer.fans.append(Fan(8)) - self.printer.fans.append(Fan(9)) - self.printer.fans.append(Fan(10)) - 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)) - 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))) + + # Mosfets + name = "Heater-{}".format(e) + channel = self.printer.config.getint("Heaters", name, "mosfet") + self.printer.mosfets[e] = Mosfet(channel) + + + # build and connect all of the temperature control infrastructure + self.printer.heaters = {} + self.printer.command_connect = {} + + # 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, "safety":Safety, + "gcode":CommandCode} + + # generate units + success = True + units = {} + for section in ["Temperature Control", "Fans", "Heaters"]: + cfg = self.printer.config[section] + + for name, options in cfg.items(): + if not isinstance(options, Section): + continue + + e = name.split('-')[-1] + if e in exclude: + continue + + input_type = options["type"] + 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) + success = False + + # connect units + for name, unit in units.items(): + 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(): + 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(): + 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 + + for fan in self.printer.fans: + logging.info("{} enabled".format(fan.name)) + fan.enable() + + for name, heater in self.printer.heaters.items(): + logging.info("{} enabled".format(name)) + heater.enable() + + ####################################################################### + # SERVO # Init the servos printer.servos = [] @@ -350,56 +449,8 @@ 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)): - 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)) + ####################################################################### + # ROTARY ENCODER # Init roatray encs. printer.filament_sensors = [] @@ -424,6 +475,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) @@ -634,6 +689,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/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/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 new file mode 100644 index 00000000..e598799b --- /dev/null +++ b/redeem/TemperatureControl.py @@ -0,0 +1,679 @@ +#!/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 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 + +#============================================================================== +# CLASSES +#============================================================================== + +class CircularBuffer(object): + """ + 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 + 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): + """ Get the current length of the list""" + 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: + """ + Base component of all temperature control units + """ + + printer = None + counter = 0 + + def get_unit(self, name, units): + """ 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: + e = name.split("-")[-1] + return self.printer.thermistors[e] + elif "MOSFET" in 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, {"value":int(name)}, self.printer) + units[c_name] = unit + return unit + + + return + + def initialise(self): + """ perform post connection initialization""" + return + + def check(self): + """ run any checks that need to be performed after full initialisation""" + return + + def __str__(self): + return self.name + + +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 + self.printer = printer + + self.input = self.options["input"] + + self.counter += 1 + + 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 + + self.input = [] + for i in range(2): + self.input.append(options["input-{}".format(i)]) + + self.counter += 1 + + 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 + + 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_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 + + self.min_temp_enabled = False + + 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] + 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 + + disconnect = False + if (not isinstance(input_sensor, TemperatureSensor)) and (not isinstance(input_sensor, ColdEnd)): + msg = "{} disabled. {} is not a temperature sensor".format(self.name, self.input.name) + logging.warning(msg) + disconnect = True + + # 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) + + 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): + """ 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 """ + + # add to ring buffers + self.time.append(time.time()) + self.temp.append(self.input.get_value()) + + # 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 + + # last recorded temperature + current_time = times[-1] + current_temp = sum(temps)/float(n) #average + + # 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_value + 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 = current_time - self.start_heating_time + + # Check that temperature is not rising too quickly + 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_rise_rate, + 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 ({:.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_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 + if self.min_temp_enabled and (current_temp < (target_temp - self.min_temp)): + a = Alarm(Alarm.HEATER_TOO_COLD, + "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 current_temp > self.max_temp: + a = Alarm(Alarm.HEATER_TOO_HOT, + "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(current_temp) + " time delta: " + + str(time_delta)) + + return + + +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 + + self.input = None + self.output = None + + self.value = 0.0 + self.sleep = 0.25 + + self.get_options() + + self.counter += 1 + + 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) + self.output.input = self + + return + + def reset(self): + """ reset any historical data """ + return + + 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: + self.output = self.options["output"] + + self.value = int(self.options['value'])/255.0 + 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)): + self.value = w/255.0 + time.sleep(delay) + self.value = value + self.sleep = save_sleep + + + def check(self): + """ perform any checks or logging after full connection """ + logging.info("{} --> {} --> {}".format(self.value, self.name, self.output)) + + + +class CommandCode(ConstantControl): + """ + For connecting G and M codes + """ + + 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: + 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): + """ 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) + outputs = outputs[0:-2] + "]" + logging.info("{} --> {} --> {}".format(self.input, self.name, outputs)) + + def __str__(self): + return str(self.name) + + +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: + 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_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.ok_range = abs(self.on_offset) + + self.value = self.off_value + + return + + def set_target_value(self, value): + """ set the target value """ + self.target_value = float(value) + return + + def get_target_value(self): + """ get the target value """ + return self.target_value + + def get_value(self): + """ return the current value """ + value = self.input.get_value() + + if value <= (self.target_value + self.on_offset): + self.value = self.max_value + elif value >= (self.target_value + self.off_offset): + self.value = self.off_value + + return self.value + + +class ProportionalControl(Control): + """ + Control output in proportion to the instantaneous error between current + and target value + """ + + feedback_control = True + + def get_options(self): + """ retrieve options from config""" + 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_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_value(self, value): + """ set the target value """ + self.target_value = float(value) + return + + def get_target_value(self): + """ get the target value """ + return self.target_value + + def get_value(self): + """ return the current value based on proportional (P) control""" + self.current_value = self.input.get_value() + error = self.target_value-self.current_value + + if error <= self.ok_range: + return 0.0 + + 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 value + value = min(value, self.max_value) + # Clamp min value + value = max(value, self.min_value) + + 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: + 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_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): + """ perform post connection initialization""" + self.avg = max(int(1.0/self.sleep), 3) + self.error = 0 + self.errors = [0]*self.avg + self.averages = [0]*self.avg + + current_value = self.input.get_value() + self.values = [current_value] + + 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_value(self, value): + """ set the target value """ + self.target_value = float(value) + return + + def get_target_value(self): + """ get the target value """ + return self.target_value + + + 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 + + self.error = self.target_value-current_value + self.errors.append(self.error) + self.errors.pop(0) + + 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 + + def get_error_derivative(self): + """ Get the derivative of the value""" + # Using value and not error for calculating derivative + # gets rid of the derivative kick. dT/dt + der = (self.values[-2]-self.values[-1])/self.sleep + self.averages.append(der) + if len(self.averages) > 11: + self.averages.pop(0) + 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 the reciprocal of the integral term + self.error_integral = np.clip(self.error_integral, 0, self.max_value*self.Ti/self.Kp) + return self.error_integral + + def reset(self): + """ reset any historical values """ + + self.error_integral = 0.0 + + return diff --git a/redeem/TemperatureSensor.py b/redeem/TemperatureSensor.py index 451329af..0574a020 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 @@ -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/M105.py b/redeem/gcodes/M105.py index ae5c08a9..b47a206f 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. """ - 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}".format(prefix, temperature, target) + 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(current_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..9db4b6a2 100644 --- a/redeem/gcodes/M106_M107.py +++ b/redeem/gcodes/M106_M107.py @@ -15,23 +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: - 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." @@ -49,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" 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/M116.py b/redeem/gcodes/M116.py index 9048197c..64fd37eb 100644 --- a/redeem/gcodes/M116.py +++ b/redeem/gcodes/M116.py @@ -46,21 +46,22 @@ 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() + 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_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() + 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) 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: 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." diff --git a/redeem/gcodes/M562.py b/redeem/gcodes/M562.py index 466c3eee..01412756 100644 --- a/redeem/gcodes/M562.py +++ b/redeem/gcodes/M562.py @@ -22,14 +22,14 @@ 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 + heater.heater_error = False def get_description(self): return "Reset temperature fault. " 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