#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal_I2C.h>
// Примерное время дребезга контактов энкодера, мс.
#define ENCODER_JITTER (5)
// Таймаут на ожидание следующего события от энкодера, мс.
#define ENCODER_TIMEOUT (350)
// Параметры длительности сигналов азбуки Морзе, мс. >:3
#define DOT_LEN (500)
#define DASH_LEN (3 * DOT_LEN)
#define SIGN_DELAY (DOT_LEN)
#define LETTER_DELAY (3 * DOT_LEN)
#define REPEAT_DELAY (7 * DOT_LEN)
// Макрос для удобства записи часов.
#define HOURS(value) (value * 3600UL)
// Пин термодатчика.
#define SENSOR_PIN (2)
// Пин "пищалки".
#define BEEPER_PIN (11)
// Пин твердотельного реле управления нагревателем.
#define HEATER_PIN (12)
// Пин сигналов от энкодера.
#define ENCODER_PIN (A0)
// Действие, произведённое энкодером.
enum EncoderAction
{
NoAction, // Бездействие.
ActionNext, // Вращение в одну сторону.
ActionPrev, // Вращение в другую сторону.
ActionConfirm, // Нажатие кнопки.
};
// Стадия (состояние) сушки.
enum HeatingStage
{
Idle, // Выключено (бездействие).
PreHeating, // Прогрев.
Working, // Стабилизация температуры.
};
// Описание настроек пластика.
typedef struct
{
const char *const name; // Название.
const uint8_t temp; // Температура сушки.
const unsigned long time_sec; // Время сушки, с.
} Filament;
// Таблица с настройками для разных видов пластика.
const Filament filaments[] = {
{
.name = "PLA",
.temp = 45,
.time_sec = HOURS(6),
},
{
.name = "ABS",
.temp = 60,
.time_sec = HOURS(4),
},
{
.name = "PETG",
.temp = 65,
.time_sec = HOURS(4),
},
{
.name = "TPU",
.temp = 50,
.time_sec = HOURS(8),
},
{
.name = "Nylon",
.temp = 70,
.time_sec = HOURS(12),
},
};
// Минимальный индекс таблицы с настройками пластиков.
#define MIN_IDX (0)
// Максимальный индекс таблицы с настройками пластиков.
#define MAX_IDX ((sizeof(filaments) / sizeof(filaments[0])) - 1)
// Настройка шины 1-wire и термодатчика DS18B20.
OneWire ow_bus(SENSOR_PIN);
DallasTemperature sensor(&ow_bus);
// Настройка LCD-дисплея 1602.
LiquidCrystal_I2C screen(0x27, 16, 2);
// Выбранный пластик.
volatile const Filament *filament = NULL;
// Флаг, показывающий что пора обновить значения на дисплее.
volatile bool refresh_screen = false;
// Счётчик секунд, прошедших с момента запуска текущей стадии.
volatile unsigned long seconds = 0;
// Флаг, показывающий включен сейчас нагрев или выключен.
// На дисплее отображается буквой 'H'.
volatile bool heater_is_on = false;
// Флаг, показывающий что есть событие от энкодера.
volatile bool event_on_encoder = false;
// Текущая стадия сушки.
volatile HeatingStage heating_stage = Idle;
// Обработчик прерывания от таймера. Срабатывает 1 раз в секунду.
ISR(TIMER1_COMPA_vect)
{
seconds++;
refresh_screen = true;
}
// Обработчик прерывания с пина энкодера.
// Срабатывает по изменению напряжения
// в любую сторону (уменьшение/увеличение).
ISR(PCINT1_vect)
{
event_on_encoder = true;
}
// Сброс таймера.
void reset_timer(void)
{
// На время сброса запрещаем прерывания
// чтобы значение счётчика не изменилось.
noInterrupts();
seconds = 0;
refresh_screen = false;
interrupts();
}
// Включение нагрева.
void turn_on(void)
{
digitalWrite(HEATER_PIN, HIGH);
heater_is_on = true;
}
// Выключение нагрева.
void turn_off(void)
{
digitalWrite(HEATER_PIN, LOW);
heater_is_on = false;
}
// Пищание "пищалкой".
void beep(uint16_t duration)
{
digitalWrite(BEEPER_PIN, HIGH);
delay(duration);
digitalWrite(BEEPER_PIN, LOW);
}
// Полностью очистить дисплей.
void clear_screen(void)
{
screen.clear();
screen.home();
}
// Обработчик ошибок.
// Аргументом получает сообщение об ошибке.
// Играет "пищалкой" сигнал 'S.O.S' азбукой Морзе.
void panic(const char *const reason)
{
turn_off();
clear_screen();
screen.setCursor(0, 0);
screen.print("PANIC! Reason:");
screen.setCursor(0, 1);
screen.print(reason);
for (;;) {
// 'S': ...
beep(DOT_LEN);
delay(SIGN_DELAY);
beep(DOT_LEN);
delay(SIGN_DELAY);
beep(DOT_LEN);
delay(LETTER_DELAY);
// 'O': ---
beep(DASH_LEN);
delay(SIGN_DELAY);
beep(DASH_LEN);
delay(SIGN_DELAY);
beep(DASH_LEN);
delay(LETTER_DELAY);
// 'S': ...
beep(DOT_LEN);
delay(SIGN_DELAY);
beep(DOT_LEN);
delay(SIGN_DELAY);
beep(DOT_LEN);
delay(LETTER_DELAY);
delay(REPEAT_DELAY);
}
}
// Получение текущей температуры с термодатчика.
uint8_t query_sensor(void)
{
sensor.requestTemperatures();
const float value = sensor.getTempCByIndex(0);
if (value == DEVICE_DISCONNECTED_C)
panic("Temp NaN.");
const uint8_t temp = ((unsigned int) value) & 0xFF;
if (temp <= 1)
panic("Frozen.");
if (temp >= 120)
panic("Burned.");
return temp;
}
// Показывает на дисплее температуру и время сушки
// выбранного пластика.
void present_filament(void)
{
screen.setCursor(0, 0);
screen.print(filament->name);
screen.print(" ? ");
screen.setCursor(0, 1);
screen.print(filament->time_sec / 3600);
screen.print(" hours at ");
screen.print(filament->temp);
screen.print("* ");
}
// Чтение действия энкодера.
// Выполняется до тех пор, пока не определит действие,
// игнорируя случайные срабатывания.
// Значения настраиваются эмпирическим путём.
// Текущие значения указаны для номиналов резисторов согласно схеме,
// при точности резисторов 1%.
EncoderAction read_action(void)
{
int value = 0;
for (;;) {
value = analogRead(ENCODER_PIN);
if (value == 0)
return NoAction;
if (value > 840 && value < 850)
return ActionPrev;
if (value > 690 && value < 705)
return ActionNext;
if (value > 560 && value < 610)
return ActionConfirm;
}
}
// Дожидается любого действия от энкодера и возвращает его.
EncoderAction wait_for_action(void)
{
// Ждём пока на энкодере произойдёт какое-либо действие.
while (!event_on_encoder)
delay(1);
const EncoderAction action = read_action();
// Если энкодер бездействует, то сразу же выходим.
if (action == NoAction)
return NoAction;
unsigned long time_diff = 0;
unsigned long time_begin = millis();
/*
Алгоритм обработки вращения энкодера и подавления дребезга контактов.
Основан на механике работы энкодера. При вращении в любую сторону сначала
замыкается один контакт, затем пока он замкнут замыкается другой контакт.
За счёт того, что в схеме реализован делитель напряжения на резисторах,
два этих замыкания контактов дают разное значение напряжения. И мы здесь
получаем два события: сначала о том, что замкнулся один контакт, затем
что замкнулся второй.
При вращении в одну сторону (action == ActionPrev) ожидаем прихода
следующего действие (ActionNext). Однако здесь есть гонка! Если из-за
дребезга контактов или тормозов в АЦП раньше фронта сигнала со второго
контакта напряжение упало до нуля, то придёт действие NoAction. Поэтому
ограничиваем ожидание таймаутом.
При вращении в другую сторону всё точно также, только порядок замыкания
контактов меняется местами.
*/
if (action == ActionPrev) {
for (;;) {
if (read_action() == ActionNext)
break;
delay(1);
if (millis() - time_begin > ENCODER_TIMEOUT)
break;
}
}
if (action == ActionNext) {
for (;;) {
if (read_action() == ActionPrev)
break;
delay(1);
if (millis() - time_begin > ENCODER_TIMEOUT)
break;
}
}
/*
Когда при вращении оба контакта отработали, напряжение возвращается
в ноль (действие NoAction). Дожидаемся этого. Если нажимали кнопку,
то ждём пока её отпустят.
*/
while (read_action() != NoAction)
delay(1);
/*
Если нажимали кнопку, тогда спим в пределах погрешности энкодера
(приблизительного времени, в течение которого контакты дребезжат).
Если энкодер крутили, тогда спим в два раза большее время, чем заняла
длительность импульса, начавшегося с замыкания одного контакта и
закончившаяся с размыканием любого из контактов. Это с достаточно
высокой вероятностью гарантирует, что контакты отработали и даёт
защиту от ложных срабатываний при слишком быстром вращении энкодера.
*/
if (action == ActionConfirm)
time_diff = 0;
else
time_diff = millis() - time_begin;
delay(time_diff * 2 + ENCODER_JITTER);
// Сбрасываем флаг, показывающий что от энкодера приходили события.
// Это нужно для подавления дребезга контактов, событий могло прийти
// множество, но одно мы уже обработали. Остальные будут обработаны
// позже.
noInterrupts();
event_on_encoder = false;
interrupts();
return action;
}
// Цикл отображения меню выбора пластика.
void choose_filament(void)
{
clear_screen();
size_t cur_idx = MIN_IDX;
filament = &(filaments[cur_idx]);
present_filament();
for (;;) {
EncoderAction action = wait_for_action();
if (action == ActionConfirm)
return;
if (action == ActionNext) {
// Если добрались до конца таблицы, переходим в её начало.
if (cur_idx == MAX_IDX)
cur_idx = 0;
else
cur_idx++;
}
if (action == ActionPrev) {
// Если добрались до начала таблицы, переходим в её начало.
if (cur_idx == MIN_IDX)
cur_idx = MAX_IDX;
else
cur_idx--;
}
filament = &(filaments[cur_idx]);
present_filament();
}
}
// Включаем/выключаем нагреватель и переключаем стадию сушки.
void set_heater_state(const uint8_t temp)
{
if (filament == NULL)
panic("Heater state.");
if (temp > filament->temp) {
turn_off();
// Если сушилка была в состоянии прогрева, значит с первого выключения
// нагревателя включается основной рабочий режим просушки. Сбрасываем
// счётчик времени, чтобы начать обратный отсчёт.
// Если сушилка была в состоянии бездействия, но температура уже выше
// нужной, значит плаcтик начали сушить не дождавшись пока она остынет.
// Тоже переключаемся в основной режим.
if (heating_stage == Idle || heating_stage == PreHeating) {
heating_stage = Working;
reset_timer();
}
} else {
turn_on();
if (heating_stage == Idle) {
// Если сушилка бездействовала, значит с первого включения нагревателя
// начинаем прогрев. Сбрасываем счётчик времени, чтобы показать, сколько
// уже идёт прогрев.
heating_stage = PreHeating;
reset_timer();
}
}
}
// Обновление данных на дисплее.
void update_screen(const uint8_t temp)
{
screen.setCursor(0, 0);
screen.print(filament->name);
screen.print(" ");
screen.print(filament->temp);
screen.print(" / ");
screen.print(temp);
screen.print("* ");
// Если нагреватель включен, рисуем в конце первой строки букву 'H'.
if (heater_is_on)
screen.print("H");
// Добавляем в конец несколько пробелов чтобы гарантированно корректно
// отрисовать всю строку и в ней не осталось "призраков" от предыдущих
// символов, если прежняя строка была короче по длине.
screen.print(" ");
screen.setCursor(0, 1);
unsigned long time_val = 0;
// Если сушилка находится в стадии сушки, отображаем
// сколько времени осталось до окончания.
if (heating_stage == Working) {
screen.print("ETA ");
time_val = filament->time_sec - seconds;
const uint8_t hours = (time_val / 3600) & 0xFF;
if (hours < 10)
screen.print("0");
screen.print(hours);
screen.print(":");
} else {
// Если сушилка находится в состоянии прогрева, тогда
// показываем, сколько времени прошло с момента его
// начала.
screen.print("Preheating ");
time_val = seconds;
// Если в течение часа так и не удалось прогреть сушилку
// до заданной температуры, значит что-то точно идёт не так.
if (time_val >= 3600)
panic("Preheating.");
}
const uint8_t mins = ((time_val % 3600) / 60) & 0xFF;
if (mins < 10)
screen.print("0");
screen.print(mins);
screen.print(":");
const uint8_t secs = (time_val % 60) & 0xFF;
if (secs < 10)
screen.print("0");
screen.print(secs);
screen.print(" ");
}
void setup()
{
// Настраиваем пины на выход.
pinMode(BEEPER_PIN, OUTPUT);
pinMode(HEATER_PIN, OUTPUT);
// Сразу же выключаем нагреватель.
turn_off();
// Настраиваем экран, выводим приветствие и пищим.
screen.init();
screen.backlight();
clear_screen();
screen.print("Hello world!");
beep(250);
// Настраиваем термодатчик.
sensor.begin();
// Настраиваем обработчик прерывания от таймера.
// Подробнее см.: https://habr.com/ru/post/453276/
noInterrupts();
TCCR1A = 0;
TCCR1B = 0;
OCR1A = 15624;
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS10);
TCCR1B |= (1 << CS12);
TIMSK1 |= (1 << OCIE1A);
interrupts();
// Настраиваем прерывания от пина, куда подключен энкодер.
// Подробнее см.:
// https://tsibrov.blogspot.com/2019/06/arduino-interrupts-part2.html
PCICR |= (1 << PCIE1);
PCMSK1 |= (1 << PC0);
}
void loop()
{
uint8_t temp = 0;
// Если пластик ещё не выбран, показываем меню выбора.
// Затем запускаем прогрев.
if (filament == NULL) {
turn_off();
choose_filament();
clear_screen();
reset_timer();
heating_stage = Idle;
refresh_screen = true;
}
// Если идёт сушка и время подошло к концу, показываем сообщение,
// пищим, ожидаем нажатия на кнопку энкодера и снова показываем
// меню выбора пластика.
if (heating_stage == Working && seconds > filament->time_sec) {
turn_off();
clear_screen();
screen.setCursor(0, 0);
screen.print("Finished!");
beep(2000);
delay(1000);
beep(2000);
delay(1000);
beep(2000);
screen.setCursor(0, 1);
screen.print("Press any key...");
while (wait_for_action() != ActionConfirm)
;
filament = NULL;
return;
}
temp = query_sensor();
set_heater_state(temp);
if (refresh_screen) {
refresh_screen = false;
update_screen(temp);
}
}
+59 |
2875
75
|
+34 |
3428
49
|
можно еще поставить на датчики весов и сообщать сколько влаги было удалено :)
К 5 кг датчику подвесил 2.5 кг и оставил на выходные — в понедельник получил 2.4 кг и «0» уплыл на те-же 0.1 кг.
Есть хоть какие-то замеры? Или так, «к слову пришлось»?
Кстати, вместо AM2302 лучше поставить BME280, там как раз тоже i2C
Если что, в машинах летом даже белый PLA плывёт )
Или одновременно нельзя использовать по каким-то другим причинам?
Ещё можно сделать фильтры на дырках, чтобы пыль отсеивать и провести тефлоновую трубку напрямик к экструдеру.
А вот это уже интересная идея, спасибо! Надо будет подумать…
Вообще, держатель катушки можно закрепить как угодно, можно даже за стенки сушилки — было бы желание, пластик и небольшое умение чертить 3д модели.
Както так, нюансы уже по месту))
зы. конструкцию лучше делать съемной — тогда сушилку и для фруктов можно использовать пока не печатаете…
Аналогично, когда после океанских купаний с гопро и не полной просушкой увидел испарину внутри стекла. Стеклянный лоток для обедов с пластиковой крышкой с резиновым уплотнителем и пакетик туда с камерой с открытым батарейным отсеком и вся влага высосана и без подогрева и циркуляции…
Кстати, одноюнитовые вентилляторы от серверов по даташиту выдерживают +70, и их везде как грязи…
Ключевой момент здесь в том, что в случае с силикагелем греть всё до 90 не нужно, а значит подойдёт любой практически вентилятор на подшипниках, коими серверные и являются, и не нужно колхоза с выносом мотора вне зоны нагрева. В вашей же таблице написано, что силикагель сушат при 65 :) вот и дайте +70, что вписывается в паспортную температуру вентиллятора.
Можно и без движения. Греть немного ускоряет процесс, но когда уйма времени, то можно и не греть
Сушите повторно силикагель в сушке при 65, храните в стеклянной банке с крышкой. Да бросайте в коробку с филаментом, и найдите способ контроля влажности в коробке: 5% — аларм — меняем силикагель.
Я просто не знаю, насколько достоверно работают датчики влажности при околонулевом её значении.
Ну аналогия не совсем уместна. Памперсы и кошачие лотки впитывают органику, которая даже после сушки не уйдёт из материала. Например азотосодержащие соединения.
А в случае с силикагелем это чистый водяной пар. Что делает его идеальным для повторного использования. Испарил пар и вперёд.
Привёл их в пример потому, что силикагель добываю в формате наполнителя для лотка, и что после его использования по назначению никто ни про какие RRR не вспоминает — просто берут и вываливают в общий мешок с мусором.
С сухим силикагелем — не розовеет.
Я его добываю из упаковок многокилобаксовых свичей в пакетиках, не россыпью… Ну и из посылок от digikey. Остановился добывать когда набрал пару кило :)
Вентилятор – как вариант разносить двигатель и крыльчатку по разным местам.
Но ломать решётки не очень хочется.
Пусть остаются целые для грибов и яблок.
Поэтому если буду делать то скорее всего просто подниму верхнюю крышку дистансером.
Прикидывал его высоту — нужно миллиметров 50-60.
Если принтер был бы побольше то можно его напечатать.
Или отрезать полосу тонкого пластика и, соединив концы, свернуть её в трубу нужного диаметра.
Но чисто из рук и головы из правильного места респект и уважуха, за проделанную работу.
Или еще добавлю напоминает как народ тратит время на моделирование и печать несколько часов того, что легко и быстро делается из деревяшек, уголков, алюм профилей и сопоставимой иди даже меньшей цене )) Если че — сам 3Д печатник )
3D-принтер откатает её легко и играючи. Даже без поддержек, они тут не требуются. Вообще, проникнувшись миром 3D печати, быстро понимаешь, что одна из главных проблем — понимание того, когда печать уместна и когда — нет.
Плюс еще учиться моделировать тоже интереснее на чем-нибудь простом, но нужном, что потом будет печататься и в идеальном случае использоваться.
Да, и то что можно сделать быстро из деревяшек и уголков, моделить вообще минуты. Дольше печататься будет, но и за деревяшками нужно ехать покупать.
Я бы ещё добавил пункт «кастом», где можно накрутить самому температуру и время. Так, чисто для обратной совместимости с сушкой фруктов :)
А потом этот режим, я так думаю, и стал бы самым основным и удобным
Эту сушилку никакие дополнительные пункты не уже спасут. После выпиливания промежуточных решёток склеил их на первый попавшийся в руки клей для пластика, никак не заботясь о его санитарной пригодности. Потому как кроме пластика сушить мне больше всё равно нечего. Но в целом идея о возможности что-то перенастроить — годная. Именно это я и подразумевал в заключении: "при необходимости всегда можно добавить другие пластики и новую функциональность".
Иногда используется предварительное охлаждение воздуха с целью вымораживания влаги, но и здесь свое веское слово говорит экономика: энергия на вымораживание 1г влаги — одна и та же, а влагосодержание забортного воздуха в большинстве случаев ниже, чем выбрасываемого после сушки. Так зачем тратить лишнюю энергию там, где можно не тратить? :)
Извините, не вижу термопары в оригинальной конструкции. По логике повествования сюда просится «регулятор температуры».
Стрелка указывает на саму биметаллическую пластину. С этого ракурса её действительно плохо видно.
Добавить туда аналогичное управление, но скорее даже удаленное, через Blynk.
Там как раз воздух по кругу гоняется. Заодно очень заинтересовала новомодная фишка типа «закалки» деталей в мелкой соли, в кухонной духовке не охота пластики «жарить».
В начале сушки, когда испаряется много влаги нужно подмешивать много сухого воздуха. В конце, когда последние капли влаги испаряются можно делать рециркуляцию почти без подмеса.
Можно датчик влажности прилепить. Но это актуально если электроэнергия дорогая.
На глазок можно 95% воздуха отправлять на рециркуляцию, а 5% подмешивать, постепенно вся влага выйдет. А в сушилках полностью воздух выбрасывается, что приводит к затратам электроэнергии на подогрев…
А можно ссылку на модель задней накладки? (ту, где выключатель и гнездо питания).Все, разобрался.
cad.onshape.com/documents/48613eff7021a5a121f711d3/w/77d1ae8fef780ecb373c445f/e/9dde7ee88f23dc3bf9cb27f3
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.