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

View File

@@ -31,12 +31,13 @@ perfect choice for easy development of this project.
Video demo - [YouTube video](https://youtu.be/vcedo59raS4)
# Current gcode support
Commands G0, G1, G2, G3, G4, G17, G18, G19, G20, G21, G28, G53, G90, G91, G92, M2, M3,
M5, M30 are supported. Commands can be easily added, see
[gmachine.py](./cnc/gmachine.py) file.
Commands G0, G1, G2, G3, G4, G17, G18, G19, G20, G21, G28, G53, G90, G91, G92,
M2, M3, M5, M30, M104, M105, M106, M107, M109, M114, M140, M190 are supported.
Commands can be easily added, see [gmachine.py](./cnc/gmachine.py) file.
Four axis are supported - X, Y, Z, E.
Circular interpolation for XY, ZX, YZ planes is supported.
Spindle with rpm control is supported.
Circular interpolation for XY, ZX, YZ planes is supported.
Extruder and bed heaters are supported.
# Config
All configs are stored in [config.py](./cnc/config.py) and contain hardware

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

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")

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.

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

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():

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)

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
cnc/heater.py Normal file
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)

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

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

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)

View File

@@ -20,6 +20,15 @@ class TestGCode(unittest.TestCase):
self.assertEqual(gc.coordinates(self.default, 1).z, 0.0)
self.assertEqual(gc.coordinates(self.default, 1).e, 99.0)
def test_has(self):
gc = GCode.parse_line("g1X2Y3z4E5F50")
self.assertTrue(gc.has("G"))
self.assertTrue(gc.has("X"))
self.assertTrue(gc.has("Y"))
self.assertTrue(gc.has("Z"))
self.assertTrue(gc.has("E"))
self.assertTrue(gc.has("F"))
def test_parser(self):
gc = GCode.parse_line("G1X2Y-3Z4E1.5")
self.assertEqual(gc.command(), "G1")

View File

@@ -3,11 +3,14 @@ import unittest
from cnc.gcode import *
from cnc.gmachine import *
from cnc.coordinates import *
from cnc.heater import *
from cnc.pid import *
class TestGMachine(unittest.TestCase):
def setUp(self):
pass
Pid.FIX_TIME_S = 0.01
Heater.LOOP_INTERVAL_S = 0.001
def tearDown(self):
pass
@@ -192,6 +195,58 @@ class TestGMachine(unittest.TestCase):
m.do_command, GCode.parse_line("M3S999999999"))
m.do_command(GCode.parse_line("M5"))
def test_m104_m109(self):
m = GMachine()
m.do_command(GCode.parse_line("M104S"+str(MIN_TEMPERATURE)))
self.assertEqual(m.extruder_target_temperature(), MIN_TEMPERATURE)
m.do_command(GCode.parse_line("M104S0"))
self.assertEqual(m.extruder_target_temperature(), 0)
# blocking heating should be called with max temperature since virtual
# hal always return this temperature.
m.do_command(GCode.parse_line("M109S" + str(EXTRUDER_MAX_TEMPERATURE)))
self.assertEqual(m.extruder_target_temperature(),
EXTRUDER_MAX_TEMPERATURE)
m.do_command(GCode.parse_line("M104S0"))
self.assertEqual(m.extruder_target_temperature(), 0)
self.assertRaises(GMachineException, m.do_command,
GCode.parse_line("M104S"+str(MIN_TEMPERATURE - 1)))
self.assertRaises(GMachineException, m.do_command,
GCode.parse_line("M109S"
+ str(EXTRUDER_MAX_TEMPERATURE + 1)))
self.assertRaises(GMachineException, m.do_command,
GCode.parse_line("M109"))
def test_m106_m107(self):
m = GMachine()
m.do_command(GCode.parse_line("M106"))
self.assertTrue(m.fan_state())
m.do_command(GCode.parse_line("M106S0"))
self.assertFalse(m.fan_state())
m.do_command(GCode.parse_line("M106S123"))
self.assertTrue(m.fan_state())
m.do_command(GCode.parse_line("M107"))
self.assertFalse(m.fan_state())
def test_m140_m190(self):
m = GMachine()
m.do_command(GCode.parse_line("M140S"+str(MIN_TEMPERATURE)))
self.assertEqual(m.bed_target_temperature(), MIN_TEMPERATURE)
m.do_command(GCode.parse_line("M140S0"))
self.assertEqual(m.bed_target_temperature(), 0)
# blocking heating should be called with max temperature since virtual
# hal always return this temperature.
m.do_command(GCode.parse_line("M190S" + str(BED_MAX_TEMPERATURE)))
self.assertEqual(m.bed_target_temperature(), BED_MAX_TEMPERATURE)
m.do_command(GCode.parse_line("M190S0"))
self.assertEqual(m.bed_target_temperature(), 0)
self.assertRaises(GMachineException, m.do_command,
GCode.parse_line("M140S"+str(MIN_TEMPERATURE - 1)))
self.assertRaises(GMachineException, m.do_command,
GCode.parse_line("M190S"
+ str(BED_MAX_TEMPERATURE + 1)))
self.assertRaises(GMachineException, m.do_command,
GCode.parse_line("M190"))
if __name__ == '__main__':
unittest.main()

82
tests/test_heater.py Normal file
View File

@@ -0,0 +1,82 @@
import unittest
from cnc.heater import *
from cnc.pid import *
from cnc.config import *
class TestHeater(unittest.TestCase):
def setUp(self):
self._target_temp = 100
Pid.FIX_TIME_S = 0
Heater.LOOP_INTERVAL_S = 0.001
self._control_counter = 0
def tearDown(self):
pass
def __get_temperature(self):
return self._target_temp
def __get_bad_temperature(self):
return self._target_temp / 2
# noinspection PyUnusedLocal
def __control(self, percent):
self._control_counter += 1
def test_start_stop(self):
# check if thread stops correctly
he = Heater(self._target_temp, EXTRUDER_PID, self.__get_temperature,
self.__control)
self.assertEqual(self._target_temp, he.target_temperature())
he.stop()
self._control_counter = 0
he.join(5)
self.assertEqual(self._control_counter, 0)
self.assertFalse(he.is_alive())
def test_async(self):
# check asynchronous heating
self._control_counter = 0
he = Heater(self._target_temp, EXTRUDER_PID, self.__get_temperature,
self.__control)
j = 0
while self._control_counter < 3:
time.sleep(0.01)
j += 1
if j > 500:
he.stop()
raise Exception("Heater timeout")
he.stop()
self.assertTrue(he.is_fixed())
def test_sync(self):
# test wait() method
self._control_counter = 0
he = Heater(self._target_temp, EXTRUDER_PID, self.__get_temperature,
self.__control)
he.wait()
he.stop()
self.assertGreater(self._control_counter, 1) # one call for stop()
self.assertTrue(he.is_fixed())
def test_fail(self):
# check if heater will not fix with incorrect temperature
self._control_counter = 0
he = Heater(self._target_temp, EXTRUDER_PID, self.__get_bad_temperature,
self.__control)
j = 0
while self._control_counter < 10:
time.sleep(0.01)
j += 1
if j > 500:
he.stop()
raise Exception("Heater timeout")
he.stop()
self.assertGreater(self._control_counter, 10) # one call for stop()
self.assertFalse(he.is_fixed())
if __name__ == '__main__':
unittest.main()

View File

@@ -1,6 +1,7 @@
import unittest
from cnc.pid import *
from cnc.config import *
class TestPid(unittest.TestCase):
@@ -8,64 +9,78 @@ class TestPid(unittest.TestCase):
self._environment_temp = 25
# Coefficients below were chosen by an experimental way with a real
# hardware: reprap heating bed and extruder.
self._bed_c = 0.00231 # bed cooling coefficient
self._bed_h = 0.25 # bed heating coefficient
self._extruder_c = 0.0039 # extruder cooling coefficient
self._extruder_h = 3.09 # extruder heating coefficient
# See ../utils/heater_model_finder.py to find out this coefficients.
self._bed_c = 0.0027 # bed cooling coefficient
self._bed_h = 0.2522 # bed heating coefficient
self._extruder_c = 0.0108 # extruder cooling coefficient
self._extruder_h = 3.4070 # extruder heating coefficient
def tearDown(self):
pass
def __simulate(self, target_temp, environment_temp, cool, heat):
def __simulate(self, target_temp, pid_c, environment_temp, cool, heat):
# Simulate heating some hypothetical thing with heater(h is a heat
# transfer coefficient, which becomes just a delta temperature each
# second) from environment temperature to target_temp. Consider that
# there is natural air cooling process with some heat transfer
# coefficient c. Heating power is controlled by PID.
pid = Pid(target_temp, 0)
pid = Pid(target_temp, pid_c, 0)
temperature = environment_temp
heater_power = 0
fixed_at = None
zeros_counter = 0
for j in range(1, 15 * 60 + 1): # simulate for 15 minutes
total_counter = 0
iter_pes_s = 2 # step is 0.5s
j = 1
for k in range(1, 20 * 60 * iter_pes_s + 1): # simulate for 20 minutes
j = k / float(iter_pes_s)
# natural cooling
temperature -= (temperature - environment_temp) * cool
temperature -= ((temperature - environment_temp) * cool
/ float(iter_pes_s))
# heating
temperature += heat * heater_power
temperature += heat * heater_power / float(iter_pes_s)
heater_power = pid.update(temperature, j)
if fixed_at is None:
if pid.is_fixed():
fixed_at = j
else:
self.assertLess(abs(temperature - target_temp),
pid.FIX_ACCURACY * target_temp,
pid.FIX_ACCURACY * target_temp * 5.0,
msg="PID failed to control temperature "
"{}/{} {}".format(temperature, target_temp, j))
if heater_power == 0.0:
zeros_counter += 1
self.assertLess(zeros_counter, 20, msg="PID turns on/off, instead of "
"fine control")
self.assertLess(fixed_at, 600,
msg="failed to heat in 10 minutes, final temperature "
total_counter += 1
self.assertLess(abs(temperature - target_temp),
pid.FIX_ACCURACY * target_temp,
msg="PID failed to control temperature "
"{}/{} {}".format(temperature, target_temp, j))
self.assertLess(zeros_counter, total_counter * 0.05,
msg="PID turns on/off, instead of fine control")
self.assertLess(fixed_at, 900,
msg="failed to heat in 15 minutes, final temperature "
"{}/{}".format(temperature, target_temp))
def test_simple(self):
pid = Pid(50, 0)
pid = Pid(50, EXTRUDER_PID, 0)
self.assertEqual(0, pid.update(100, 1))
self.assertEqual(1, pid.update(0, 2))
pid = Pid(50, BED_PID, 0)
self.assertEqual(0, pid.update(100, 1))
self.assertEqual(1, pid.update(0, 2))
def test_bed(self):
# check if bed typical temperatures can be reached in simulation
for target in range(50, 101, 10):
self.__simulate(target, self._environment_temp,
self._bed_c, self._bed_h)
def test_extruder(self):
# check if extruder typical temperatures can be reached in simulation
for target in range(150, 251, 10):
self.__simulate(target, self._environment_temp,
self.__simulate(target, EXTRUDER_PID, self._environment_temp,
self._extruder_c, self._extruder_h)
def test_bed(self):
# check if bed typical temperatures can be reached in simulation
for target in range(50, 101, 10):
self.__simulate(target, BED_PID, self._environment_temp,
self._bed_c, self._bed_h)
if __name__ == '__main__':
unittest.main()