heaters implementation: extruder and bed, adding answers to gcode commands

This commit is contained in:
Nikolay Khabarov
2017-06-19 01:08:38 +03:00
parent df7eba664f
commit 53146941d1
16 changed files with 602 additions and 53 deletions
+14
View File
@@ -13,6 +13,15 @@ TABLE_SIZE_Y_MM = 300
TABLE_SIZE_Z_MM = 48
SPINDLE_MAX_RPM = 10000
EXTRUDER_MAX_TEMPERATURE = 250
BED_MAX_TEMPERATURE = 100
MIN_TEMPERATURE = 40
EXTRUDER_PID = {"P": 0.0993079964195,
"I": 0.00267775053311,
"D": 0.267775053311}
BED_PID = {"P": 5.06820175723,
"I": 0.0476413193519,
"D": 4.76413193519}
# Pins config
STEPPER_STEP_PIN_X = 16
@@ -26,6 +35,11 @@ STEPPER_DIR_PIN_Z = 26
STEPPER_DIR_PIN_E = 8
SPINDLE_PWM_PIN = 7
FAN_PIN = 10
EXTRUDER_HEATER_PIN = 9
BED_HEATER_PIN = 11
EXTRUDER_TEMPERATURE_SENSOR_CHANNEL = 0
BED_TEMPERATURE_SENSOR_CHANNEL = 1
ENDSTOP_PIN_X = 12
ENDSTOP_PIN_Y = 6
+9
View File
@@ -39,3 +39,12 @@ class RotationDirection(Enum):
CW = RotationDirection("CW")
CCW = RotationDirection("CCW")
class Heaters(Enum):
""" Enum for selecting heater.
"""
pass
HEATER_EXTRUDER = Heaters("extruder")
HEATER_BED = Heaters("bed")
+8
View File
@@ -24,6 +24,14 @@ class GCode(object):
"""
self.params = params
def has(self, arg_name):
"""
Check if value is specified.
:param arg_name: Value name.
:return: boolean value.
"""
return arg_name in self.params
def get(self, arg_name, default=None, multiply=1.0):
""" Get value from gcode line.
:param arg_name: Value name.
+107 -7
View File
@@ -1,10 +1,11 @@
from __future__ import division
import time
import cnc.logging_config as logging_config
from cnc import hal
from cnc.pulses import *
from cnc.coordinates import *
from cnc.heater import *
from cnc.enums import *
class GMachineException(Exception):
@@ -29,14 +30,18 @@ class GMachine(object):
self._convertCoordinates = 0
self._absoluteCoordinates = 0
self._plane = None
self._fan_state = False
self._heaters = dict()
self.reset()
hal.init()
def release(self):
""" Return machine to original position and free all resources.
"""
self._spindle(0)
self.home()
self._spindle(0)
for h in self._heaters:
self._heaters[h].stop()
hal.deinit()
def reset(self):
@@ -55,6 +60,35 @@ class GMachine(object):
hal.join()
hal.spindle_control(100.0 * spindle_speed / SPINDLE_MAX_RPM)
def _fan(self, state):
hal.fan_control(state)
self._fan_state = state
def _heat(self, heater, temperature, wait):
# check if sensor is ok
if heater == HEATER_EXTRUDER:
measure = hal.get_extruder_temperature
control = hal.extruder_heater_control
coefficients = EXTRUDER_PID
elif heater == HEATER_BED:
measure = hal.get_bed_temperature
control = hal.bed_heater_control
coefficients = BED_PID
else:
raise GMachineException("unknown heater")
try:
measure()
except (IOError, OSError):
raise GMachineException("can not measure temperature")
if heater in self._heaters:
self._heaters[heater].stop()
del self._heaters[heater]
if temperature != 0:
self._heaters[heater] = Heater(temperature, coefficients, measure,
control)
if wait:
self._heaters[heater].wait()
def __check_delta(self, delta):
pos = self._position + delta
if not pos.is_in_aabb(Coordinates(0.0, 0.0, 0.0, 0.0),
@@ -211,12 +245,37 @@ class GMachine(object):
"""
return self._plane
def fan_state(self):
""" Check if fan is on.
:return True if fan is on, False otherwise.
"""
return self._fan_state
def __get_target_temperature(self, heater):
if heater not in self._heaters:
return 0
return self._heaters[heater].target_temperature()
def extruder_target_temperature(self):
""" Return desired extruder temperature.
:return Temperature in Celsius, 0 if disabled.
"""
return self.__get_target_temperature(HEATER_EXTRUDER)
def bed_target_temperature(self):
""" Return desired bed temperature.
:return Temperature in Celsius, 0 if disabled.
"""
return self.__get_target_temperature(HEATER_BED)
def do_command(self, gcode):
""" Perform action.
:param gcode: GCode object which represent one gcode line
:return String if any answer require, None otherwise.
"""
if gcode is None:
return
return None
answer = None
logging.debug("got command " + str(gcode.params))
# read command
c = gcode.command()
@@ -232,15 +291,12 @@ class GMachine(object):
self._convertCoordinates)
# coord = self._position + delta
velocity = gcode.get('F', self._velocity)
spindle_rpm = gcode.get('S', self._spindle_rpm)
pause = gcode.get('P', self._pause)
radius = gcode.radius(Coordinates(0.0, 0.0, 0.0, 0.0),
self._convertCoordinates)
# check parameters
if velocity <= 0 or velocity > STEPPER_MAX_VELOCITY_MM_PER_MIN:
raise GMachineException("bad feed speed")
if spindle_rpm < 0 or spindle_rpm > SPINDLE_MAX_RPM:
raise GMachineException("bad spindle speed")
if pause < 0:
raise GMachineException("bad delay")
# select command and run it
@@ -278,19 +334,63 @@ class GMachine(object):
gcode.coordinates(Coordinates(0.0, 0.0, 0.0, 0.0),
self._convertCoordinates)
elif c == 'M3': # spindle on
spindle_rpm = gcode.get('S', self._spindle_rpm)
if spindle_rpm < 0 or spindle_rpm > SPINDLE_MAX_RPM:
raise GMachineException("bad spindle speed")
self._spindle(spindle_rpm)
self._spindle_rpm = spindle_rpm
elif c == 'M5': # spindle off
self._spindle(0)
elif c == 'M2' or c == 'M30': # program finish, reset everything.
self.reset()
# extruder and bed heaters control
elif c == 'M104' or c == 'M109' or c == 'M140' or c == 'M190':
if c == 'M104' or c == 'M109':
heater = HEATER_EXTRUDER
elif c == 'M140' or c == 'M190':
heater = HEATER_BED
else:
raise Exception("Unexpected heater command")
wait = c == 'M109' or c == 'M190'
if not gcode.has("S"):
raise GMachineException("temperature is not specified")
t = gcode.get('S', 0)
if ((heater == HEATER_EXTRUDER and t > EXTRUDER_MAX_TEMPERATURE) or
(heater == HEATER_BED and t > BED_MAX_TEMPERATURE) or
t < MIN_TEMPERATURE) and t != 0:
raise GMachineException("bad temperature")
self._heat(heater, t, wait)
elif c == 'M105': # get temperature
try:
et = hal.get_extruder_temperature()
except (IOError, OSError):
et = None
try:
bt = hal.get_bed_temperature()
except (IOError, OSError):
bt = None
if et is None and bt is None:
raise GMachineException("can not measure temperature")
answer = "E:{} B:{}".format(et, bt)
elif c == 'M106': # fan control
if gcode.get('S', 1) != 0:
self._fan(True)
else:
self._fan(False)
elif c == 'M107': # turn off fan
self._fan(False)
elif c == 'M111': # enable debug
logging_config.debug_enable()
elif c == 'M114': # get current position
hal.join()
p = self.position()
answer = "X:{} Y:{} Z:{} E:{}".format(p.x, p.y, p.z, p.e)
elif c is None: # command not specified(for example, just F was passed)
pass
else:
raise GMachineException("unknown command")
# save parameters on success
self._velocity = velocity
self._spindle_rpm = spindle_rpm
self._pause = pause
logging.debug("position {}".format(self._position))
return answer
+49 -3
View File
@@ -5,18 +5,54 @@
# """ Initialize GPIO pins and machine itself, including calibration if
# needed. Do not return till all procedure is completed.
# """
# logging.info("initialize hal")
# do_something()
#
#
# def spindle_control(percent):
# """ Spindle control implementation.
# :param percent: Spindle speed in percent. 0 turns spindle off.
# :param percent: Spindle speed in percent 0..100. 0 turns spindle off.
# """
# logging.info("spindle control: {}%".format(percent))
# do_something()
#
#
# def fan_control(on_off):
# """
# Cooling fan control.
# :param on_off: boolean value if fan is enabled.
# """
# do_something()
#
#
# def extruder_heater_control(percent):
# """ Extruder heater control.
# :param percent: heater power in percent 0..100. 0 turns heater off.
# """
# do_something()
#
#
# def bed_heater_control(percent):
# """ Hot bed heater control.
# :param percent: heater power in percent 0..100. 0 turns heater off.
# """
# do_something()
#
#
# def get_extruder_temperature():
# """ Measure extruder temperature.
# Can raise OSError or IOError on any issue with sensor.
# :return: temperature in Celsius.
# """
# return measure()
#
#
# def get_bed_temperature():
# """ Measure bed temperature.
# Can raise OSError or IOError on any issue with sensor.
# :return: temperature in Celsius.
# """
# return measure()
#
#
# def move(generator):
# """ Move head to according pulses in PulseGenerator.
# :param generator: PulseGenerator object
@@ -49,6 +85,16 @@ if 'init' not in locals():
raise NotImplementedError("hal.init() not implemented")
if 'spindle_control' not in locals():
raise NotImplementedError("hal.spindle_control() not implemented")
if 'fan_control' not in locals():
raise NotImplementedError("hal.fan_control() not implemented")
if 'extruder_heater_control' not in locals():
raise NotImplementedError("hal.extruder_heater_control() not implemented")
if 'bed_heater_control' not in locals():
raise NotImplementedError("hal.bed_heater_control() not implemented")
if 'get_extruder_temperature' not in locals():
raise NotImplementedError("hal.get_extruder_temperature() not implemented")
if 'get_bed_temperature' not in locals():
raise NotImplementedError("hal.get_bed_temperature() not implemented")
if 'move' not in locals():
raise NotImplementedError("hal.move() not implemented")
if 'join' not in locals():
+59 -1
View File
@@ -3,6 +3,7 @@ import time
from cnc.hal_raspberry import rpgpio
from cnc.pulses import *
from cnc.config import *
from cnc.sensors import thermistor
US_IN_SECONDS = 1000000
@@ -32,7 +33,13 @@ def init():
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
gpio.init(ENDSTOP_PIN_X, rpgpio.GPIO.MODE_INPUT_PULLUP)
gpio.init(SPINDLE_PWM_PIN, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(FAN_PIN, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(EXTRUDER_HEATER_PIN, rpgpio.GPIO.MODE_OUTPUT)
gpio.init(BED_HEATER_PIN, rpgpio.GPIO.MODE_OUTPUT)
gpio.clear(SPINDLE_PWM_PIN)
gpio.clear(FAN_PIN)
gpio.clear(EXTRUDER_HEATER_PIN)
gpio.clear(BED_HEATER_PIN)
# calibration
gpio.set(STEPPER_DIR_PIN_X)
@@ -86,7 +93,7 @@ def init():
def spindle_control(percent):
""" Spindle control implementation.
:param percent: spindle speed in percent. If 0, stop the spindle.
:param percent: spindle speed in percent 0..100. If 0, stop the spindle.
"""
logging.info("spindle control: {}%".format(percent))
if percent > 0:
@@ -95,6 +102,53 @@ def spindle_control(percent):
pwm.remove_pin(SPINDLE_PWM_PIN)
def fan_control(on_off):
"""
Cooling fan control.
:param on_off: boolean value if fan is enabled.
"""
if on_off:
logging.info("Fan is on")
gpio.set(FAN_PIN)
else:
logging.info("Fan is off")
gpio.clear(FAN_PIN)
def extruder_heater_control(percent):
""" Extruder heater control.
:param percent: heater power in percent 0..100. 0 turns heater off.
"""
if percent > 0:
pwm.add_pin(EXTRUDER_HEATER_PIN, percent)
else:
pwm.remove_pin(EXTRUDER_HEATER_PIN)
def bed_heater_control(percent):
""" Hot bed heater control.
:param percent: heater power in percent 0..100. 0 turns heater off.
"""
if percent > 0:
pwm.add_pin(BED_HEATER_PIN, percent)
else:
pwm.remove_pin(BED_HEATER_PIN)
def get_extruder_temperature():
""" Measure extruder temperature.
:return: temperature in Celsius.
"""
return thermistor.get_temperature(EXTRUDER_TEMPERATURE_SENSOR_CHANNEL)
def get_bed_temperature():
""" Measure bed temperature.
:return: temperature in Celsius.
"""
return thermistor.get_temperature(BED_TEMPERATURE_SENSOR_CHANNEL)
def move(generator):
""" Move head to specified position
:param generator: PulseGenerator object.
@@ -186,3 +240,7 @@ def deinit():
"""
join()
pwm.remove_all()
gpio.clear(SPINDLE_PWM_PIN)
gpio.clear(FAN_PIN)
gpio.clear(EXTRUDER_HEATER_PIN)
gpio.clear(BED_HEATER_PIN)
+41 -1
View File
@@ -17,12 +17,52 @@ def init():
def spindle_control(percent):
""" Spindle control implementation.
""" Spindle control implementation 0..100.
:param percent: Spindle speed in percent.
"""
logging.info("spindle control: {}%".format(percent))
def fan_control(on_off):
"""Cooling fan control.
:param on_off: boolean value if fan is enabled.
"""
if on_off:
logging.info("Fan is on")
else:
logging.info("Fan is off")
# noinspection PyUnusedLocal
def extruder_heater_control(percent):
""" Extruder heater control.
:param percent: heater power in percent 0..100. 0 turns heater off.
"""
pass
# noinspection PyUnusedLocal
def bed_heater_control(percent):
""" Hot bed heater control.
:param percent: heater power in percent 0..100. 0 turns heater off.
"""
pass
def get_extruder_temperature():
""" Measure extruder temperature.
:return: temperature in Celsius.
"""
return EXTRUDER_MAX_TEMPERATURE * 0.999
def get_bed_temperature():
""" Measure bed temperature.
:return: temperature in Celsius.
"""
return BED_MAX_TEMPERATURE * 0.999
# noinspection PyUnusedLocal
def move(generator):
""" Move head to specified position.
+94
View File
@@ -0,0 +1,94 @@
import threading
import time
import logging
from cnc.pid import Pid
class Heater(threading.Thread):
LOOP_INTERVAL_S = 0.5
SENSOR_TIMEOUT_S = 1
def __init__(self, target_temp, pid_coefficients, measure_method,
control_method):
""" Initialize and run asynchronous heating.
:param target_temp: temperature which should be reached in Celsius.
:param pid_coefficients: dict with PID coefficients.
:param measure_method: Method which should be called to measure
temperature, it should return temperature in
Celsius.
:param control_method: Method which should be called to control heater
power, it should received one argument with
heater power in percent(0..100).
"""
self._current_power = 0
threading.Thread.__init__(self)
self._pid = Pid(target_temp, pid_coefficients)
self._measure = measure_method
self._control = control_method
self._is_run = True
self._mutex = threading.Lock()
self.setDaemon(True)
self.start()
logging.info("Heating thread start, temperature {}/{} C"
.format(self._measure(), self.target_temperature()))
def target_temperature(self):
""" Return target temperature which should be reached.
:return:
"""
return self._pid.target_value()
def is_fixed(self):
""" Check if target value is reached and PID maintains this value.
:return: boolean value
"""
return self._pid.is_fixed()
def run(self):
""" Thread worker implementation. There is a loop for PID control.
"""
last_error = None
while True:
self._mutex.acquire()
if not self._is_run:
break
try:
current_temperature = self._measure()
except (IOError, OSError):
self._control(0)
if last_error is None:
last_error = time.time()
else:
if time.time() - last_error > self.SENSOR_TIMEOUT_S:
logging.critical("No data from temperature sensor. Stop"
" heating.")
break
continue
last_error = None
self._current_power = self._pid.update(current_temperature) * 100
self._control(self._current_power)
self._mutex.release()
time.sleep(self.LOOP_INTERVAL_S)
def stop(self):
""" Stop heating and free this instance.
"""
# make sure that control will not be called in worker anymore.
self._mutex.acquire()
self._is_run = False
self._mutex.release()
self._control(0)
logging.info("Heating thread stop")
def wait(self):
""" Block until target temperature is reached.
"""
i = 0
while not self._pid.is_fixed():
if i % 8 == 0:
logging.info("Heating... current temperature {} C, power {}%"
.format(self._measure(), int(self._current_power)))
i = 0
i += 1
time.sleep(0.25)
+5 -2
View File
@@ -30,11 +30,14 @@ machine = GMachine()
def do_line(line):
try:
g = GCode.parse_line(line)
machine.do_command(g)
res = machine.do_command(g)
except (GCodeException, GMachineException) as e:
print('ERROR ' + str(e))
return False
print('OK')
if res is not None:
print('OK ' + res)
else:
print('OK')
return True
+21 -11
View File
@@ -3,28 +3,31 @@ import math
class Pid(object):
# PID coefficients
P = 0.422
I = 0.208
D = 0.014
WINDUP_LIMIT = 3.0
FIX_ACCURACY = 0.01
FIX_TIME_S = 2.5
def __init__(self, target_value, start_time=time.time()):
def __init__(self, target_value, coefficients, start_time=None):
"""
Proportional-integral-derivative controller implementation.
:param target_value: value which PID should achieve.
:param coefficients: dict with "P", "I" and "D" coefficients.
:param start_time: start time, current system time by default.
"""
self._last_time = start_time
if start_time is None:
self._last_time = time.time()
else:
self._last_time = start_time
self._target_value = target_value
self.P = coefficients["P"]
self.I = coefficients["I"]
self.D = coefficients["D"]
self.WINDUP_LIMIT = 1.0 / self.I
self._integral = 0
self._last_error = 0
self._is_target_fixed = False
self._target_fix_timer = None
def update(self, current_value, current_time=time.time()):
def update(self, current_value, current_time=None):
"""
Update PID with new current value.
:param current_value: current value.
@@ -32,6 +35,8 @@ class Pid(object):
time if not specified.
:return: value in range 0..1.0 which represents PID output.
"""
if current_time is None:
current_time = time.time()
delta_time = current_time - self._last_time
self._last_time = current_time
error = self._target_value - current_value
@@ -60,16 +65,21 @@ class Pid(object):
return res
def is_fixed(self):
"""
Check if target value is reached and PID maintains this value.
""" Check if target value is reached and PID maintains this value.
:return: boolean value
"""
return self._is_target_fixed
def target_value(self):
""" Get target value.
:return: value.
"""
return self._target_value
# for test purpose, see details in corresponding test file
if __name__ == "__main__":
p = Pid(230, 0)
p = Pid(230, {"P": 0.1000, "I": 0.0274, "D": 0.2055}, 0)
c = 0.0039
h = 3.09
t0 = 25
+6 -1
View File
@@ -49,6 +49,7 @@ Rinf = R0 * math.exp(-BETA / (T0 + CELSIUS_TO_KELVIN))
def get_temperature(channel):
"""
Measure temperature on specified channel.
Can raise OSError or IOError on any issue with sensor.
:param channel: ads111x channel.
:return: temperature in Celsius
"""
@@ -65,6 +66,10 @@ def get_temperature(channel):
if __name__ == "__main__":
while True:
for i in range(0, 4):
print("T{}={}".format(i, get_temperature(i)))
try:
t = get_temperature(i)
except (IOError, OSError):
t = None
print("T{}={}".format(i, t))
print("-----------------------------")
time.sleep(0.5)