mirror of
https://github.com/sinseman44/PyCNC.git
synced 2026-01-12 02:40:04 +00:00
heaters implementation: extruder and bed, adding answers to gcode commands
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
114
cnc/gmachine.py
114
cnc/gmachine.py
@@ -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
|
||||
|
||||
52
cnc/hal.py
52
cnc/hal.py
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
94
cnc/heater.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
32
cnc/pid.py
32
cnc/pid.py
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
82
tests/test_heater.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user