Настраиваем прошивку Marlin для Arduino + RAMPS 1.4

Tags:

Так получилось что некоторое время назад я стал владельцем китайского клона Prusa i3 под гордым китайским названием Hesine M505. Это чудо китайской мысли конечно было далеко от совершенства, но с задачей печатать все подряд справлялось вполне успешно. Однако, чем дальше в лес - тем толще партизаны. И забравшись в лес подальше возжелал я печатать детали с использованием растворимых поддержек, да и двумя цветами печатать тоже было бы неплохо.

К сожалению, у родной для принтера платы Melzi V2 был фатальный недостаток - на ней отсутсвовал порт для подключения второго экструдера. Изучение матчасти показало, что можно взять еще одну такую же плату и воткнуть ее в режиме слейва. Однако ценник на это удовольствие получался высоковатым. И, что самое неприятно, при таком апгрейде терялась возможность подключения экрана и кнопок управления. Т.е. печатать можно будет только с компьютера, а это не самая лучшая идея по многим причинам.

Так я пришел к решению полностью заменить мозги принтера. Выбор был сделан в пользу нестареющей класски Arduino Mega 2560 + RAMPS 1.4 + A4988. Быстро сказка сказывается, да долго посылочка едет. Получив посылку с мозгами, моторами и прочим полезным в хозяйстве инвентарем я обнаружил что забыл заказать шестерню податчика экструдера. Благо на тот момент она уже ехала ко мне с али, где я заказал ее просто так, от жадности.

В общем, пока необходимые запчасти едут, можно подключить и отстроить новые мозги в конфигурации с одним экструдером, а потом просто переконфигурировать прошивку когда будет собрано все железо.

Ставить мы будем классику жанра - Marlin. Клонируем репозиторий в любое удобное место отсюда https://github.com/MarlinFirmware/Marlin. Скачиваем Arduino IDE.

В Arduino IDE открываем прошивку. Выбираем нашу плату Arduino Mega 2560 и процессор AtMega 2560.

Дальше нас интересует вкладка с файлом Configuration.h, теперь мы будем его безудержно править.

Выбираем нашу плату: RAMPS 1.4 с одним хотэндом

#ifndef MOTHERBOARD
  #define MOTHERBOARD BOARD_RAMPS_14_EFB
#endif

В файле boards.h приведен полный список поддерживаемых плат и их вариаций. Нас пока интересует только RAMPS 1.4 с одним экструдером.

Выставляем кол-во температурных сенсоров. Сенсоров должно быть по числу экструдеров. В нашем случае 1. Убеждаемся что напротив первого сенсора в списке стоит 1.

#define TEMP_SENSOR_0 1

Выставляем максимальную температуру хотэнда и стола.

#define HEATER_0_MAXTEMP 250
#define BED_MAXTEMP 130

У меня стоят термопредохранители на 252 градуса, так что максимальная температура хота должна быть ниже температуры срабатывания предохранителя. Если собираетесь печатать чем-то вроде поликарбоната - то температуру надо поднять. Со столом таже история, единственное что даже печать нейлоном не требует очень больших температур стола, 130 градусов должно хватить всем.

В Hesine M505 стоят нормально замкнутые концевики, а в прошивка по умолчанию рассчитывает на нормально разомкнутые. Включаем инвертирование концевиков:

// Mechanical endstop with COM to ground and NC to Signal uses "false" here (most common setup).
#define X_MIN_ENDSTOP_INVERTING true // set to true to invert the logic of the endstop.
#define Y_MIN_ENDSTOP_INVERTING true // set to true to invert the logic of the endstop.
#define Z_MIN_ENDSTOP_INVERTING true // set to true to invert the logic of the endstop.

Проверяем направление вращения моторов. Тут история примерно таже что и с концевиками, т.к. прошивка изначально рассчитана на Ultimaker.

// Invert the stepper direction. Change (or reverse the motor connector) if an axis goes the wrong way.
#define INVERT_X_DIR true
#define INVERT_Y_DIR true
#define INVERT_Z_DIR false

И для экструдера повторяем операцию.

#define INVERT_E0_DIR true

Выставляем размеры рабочей зоны

#define X_MIN_POS 0
#define Y_MIN_POS 0
#define Z_MIN_POS 0
#define X_MAX_POS 250
#define Y_MAX_POS 230
#define Z_MAX_POS 140

Для начала можно выставить заводомо большие чем рабочая зона габариты. Потом все откалибруется по месту. По умолчанию в прошивке стоят значени 200х200х200, тогда как у Hesine M505 рабочее поле по всем направлениям больше чем эти цифры. И прошивка просто не даст двигаться за их пределы.

Устанавливаем координаты начала стола. Это нужно чтобы ноль стола в слайсере совпадал с нулем в координатах принтера. Иначе модель может вылезать за пределы зоны печати.

#define MANUAL_X_HOME_POS -30
#define MANUAL_Y_HOME_POS -20

Выставляем шаги для моторов.

#define DEFAULT_AXIS_STEPS_PER_UNIT   {100,100,1600,95}

Параметры тут следующие: мотор_Х, мотор_Y, мотор_Z, мотор_экструдера

Для рассчета шагов по X и Y используем следующую формулу:

(200*16)/(16*2)=80 шагов

Где 200 - это число шагов двигателя на 360 градусов. Типичная цифра для моторов с шагом 1.8 градуса. 16 в числителе - кол-во микрошагов на шаг. 16 в знаменателе - кол-во зубов на шпуле. 2 - стандартный шаг для ремня GT2

Для Z:

3200/2 = 1600

В Hesine M505 штатно используется трапециидальный винт с шагом резьбы 2мм. Соответственно мы белим число шагов на полный оборот на кол-во миллиметров которые будут пройдены за оборот и получаем число шагов на миллиметр.

Подачу экструдера пока посчитаем и выставим предварительно. Потом ее все равно придется подгонять. Шестерня экструдера штатно имеет диаметр около 10мм. Получаем длинну окружности на один оборот: 3.14*10=31.4 мм на оборот.

Делим число шагов на длинну окружности и получаем число шагов на миллиметр.

3200/31.4=101.9

округляем до 102.

Заливаем все это дело в Arduino.

После того как все залилось и все железо подключено запускаем Pronterface и начинаем калибровку.

  • Двигаем моторы и проверяем что они крутятся в нужную сторону. Если это не так - меняем параметр INVERT_*_DIR на противоположное значение.
  • Проверяем состояние концевиков. По команде M119 будет показано состояние концевиков. Если концевик нажат - напротив него должно быть написано TRIGGERED. Напротив ненажатого - open. Если это не так - меняем настройку *MINENDSTOP_INVERTING.
  • Даем команду G28. Все оси должны приехать в свое минимальное положение. Дальше через Pronterface двигаем все оси в их безопасное максимальное положение и даем комаду M114. Она покажет текущее положение по осям. Вносим эти данные в настройку *MAXPOS.
  • Опять говорим G28. Передвигаем экструдер в нулевую координату стола по X-Y. Смотрим что показывает M114 и эти цифры переносим в MANUAL_*HOMEPOS с обратным знаком. Т.е. если M114 говорит что координата по Х сейчас 30 - то в настройку пишем -30. Это значит что после ухода в HOME экструдер отъедет от него на 30мм и будет считать это положение нолем.
  • откручиваем сопло или трубку боудена у экструдера. На прутке отмечаем расстояние, например 10 см, и прогоняем эту же длинну из Pronterface. После этого смотрим сколько в реальности прошло прутка и корректируем кол-во шагов для экструдера чтобы он прогонял четко нужное кол-во пластика. Важно понимать что на горячую с установленным соплом эта цифра все равно будет отличаться от установленного. Если вы всегда печатаете соплом одного диаметра - можно повторить эту операцию с соплом и полученные данные внести в прошивку. Я использую несколько сопел, так что объем пластика корректирую через настройку потока в слайсере.

После того как все это сделано - печатаем тестовую модель. Я использую пустой куб 20х20х20 мм. После того как он отпечатан - проверяем что размеры сторон у нас точно соответсвуют тому что должно быть. Если это не так по отдельным измерениям - корректируем кол-во шагов для нужной оси.

На этом основные настройки закончены. Дальше можно тюнить приведенные выше параметры для более качественной печати.

Работаем с самбой из Java с помощью JCIFS

Tags:

Простые примеры работы с самбой при помощи JCIFS

Ниже показан пример класса для копирования файла на самба-шару и просмотра содержимого заданной шары

import jcifs.Config;
import jcifs.smb.*;
import java.io.*;

public class SambaTest {
  // Нормальный конструктор я тут делать не буду, для тестового примера это не нужно
static final String USER_NAME = "username";
static final String PASSWORD = "password";
static final String DOMAIN = "user_domain";

// Путь к сетевой папке с которой будем работать
static final String NETWORK_FOLDER = "smb://server/share/";


// Копируем файл на шару.
// К сожалению SmbFile ничего не знает о методе copyTo из File,
// так что придется этот метод эмулировать руками. Халява не прокатила :(
// В обратную сторону все тоже самое, только потоки будут в обратную сторону.
public boolean copyFileToSamba(String srcFilePath, String destPath) {
    boolean successful = false;
    try{
        // Создаем объект аутентификатор
        NtlmPasswordAuthentication auth =
          new NtlmPasswordAuthentication(DOMAIN, USER_NAME, PASSWORD);

        // Читаем содержимое исходного файла
        File srcFile = new File(srcFilePath);
        InputStream localFile = new FileInputStream(srcFile);

        // Создаем объект для потока куда мы будем писать наша файл
        SmbFileOutputStream destFileName = new SmbFileOutputStream(
          new SmbFile(destPath+File.separator+srcFile.getName(), auth));

        // Ну и копируем все из исходного потока в поток назначения.
        BufferedReader brl = new BufferedReader(
          new InputStreamReader(localFile));
        String b = null;
        while((b=brl.readLine())!=null){
            destFileName.write(b.getBytes());
        }
        destFileName.flush();


        successful = true;
    } catch (Exception e) {
        successful = false;
        e.printStackTrace();
    }
    return successful;
}

// Читаем
public boolean readShareContent() {
    boolean successful = false;
    try{
        // Создаем объект для аутентификации на шаре
        NtlmPasswordAuthentication auth =
          new NtlmPasswordAuthentication(DOMAIN, USER_NAME, PASSWORD);
        String path = NETWORK_FOLDER;

        // Ресолвим путь назначения в SmbFile
        SmbFile baseDir = new SmbFile(path, auth);

        // Вычитываем все содержимое шары в массив
        SmbFile[] files = baseDir.listFiles();

        // Делаем что-нибудь со списком файлов
        for (int i = 0; i < files.length; i++) {
          SmbFile file = files[i];
          if (file.isDirectory()) {
                System.out.println("Is DIR: "+file.toString());
                continue;
          } else {
                System.out.println("Is FILE: "+file.toString());
          }
        }

        successful = true;
    } catch (Exception e) {
        successful = false;
        e.printStackTrace();
    }
    return successful;
  }
}

Если все работы с шарой невероятно тупят - то нужно сменить режим ресолвинга сервера и шары.

В конструктор добавляем следующее:

Config.setProperty( "jcifs.resolveOrder", "DNS");

Этим мы указываем что ресолвить имя сервера мы будем ТОЛЬКО через DNS. Можно туда добавить всякие NetBIOS и прочее, но на практике я с необходимостью это делать не сталкивался. Естественно, что если у вас в сети нет локального DNS сервера который будет ресолвить именя локальным машин - то механизм надо сменить на другой. Выбор механизмов ресолвинга будет проходить в порядке указанном в конфиге.

Если предполагается копирование больших бинарных файлов, то код копирования надо заменить с:

BufferedReader brl = new BufferedReader(new InputStreamReader(localFile)); String b = null; while((b=brl.readLine())!=null){ destFileName.write(b.getBytes()); }

на:

byte[] buffer = new byte[1024000]; int noOfBytes = 0;

while ((noOfBytes = localFile.read(buffer)) != -1) { destFileName.write(buffer, 0, noOfBytes); }

Если этого не сделать - то большие файлы будут копироваться криво.

Installing and configuring Postgresql 9.4 on Debian 8

Tags:

Install the postgresql server package:

#apt-get install postgresql-9.4

Create cluster (if it haven't created automatically):

#pg_createcluster 9.4 main --start

Next we need to allow connects from remote sources.

Edit /etc/postgresql/9.4/main/postgresql.conf and set listen_addresses to '*'.

Restart the Postgresql service:

#service postgresql Restart

Now we should create database user.

#su postgres
#psql

postgres=# CREATE USER db_user_name WITH PASSWORD 'secret_password';

Create new database and grant access to it to our user.

postgres=# CREATE DATABASE "db_name"
  WITH OWNER "db_user_name"
  ENCODING 'UTF8'
  LC_COLLATE = 'en_US.UTF-8'
  LC_CTYPE = 'en_US.UTF-8'
  TEMPLATE = template0;

Now we have database with UTF8 collation, it is time to allow connect from remote sources. Our user is the owner of the schema.

Edit /etc/postgresql/9.4/main/pg_hba.conf and add line

host    all   db_user_name    0.0.0.0/0               md5

after the line:

host    all   all             127.0.0.1/32            md5

Save file and restart the service.

Now we can connect to our server from remote computer.

git clone recursive

Tags:

В Git 2.6 для Windows появилась неприятная бага - если уровень вложенности сабмодулей больше 3х, то команда git clone --recursive падает с ошибкой при клонировании глубоко вложенных сабмодулей.

Как бороться - разбить клонирование на 3 этапа:

git clone <repo_addr>
cd <repo_dir>
git submodule update --init
git submodule foreach --recursive git submodule update --init

Здесь мы просто клонируем репозиторий, при необходимости можно сделать чекаут на нужную ветку. Затем инициализируем и загружаем сабмодули первого уровня. И последняя команда проходит рекурсивно по всем сабмодулям рекурсивно и загружает их на неограниченном (наверное) уровне вложенности.

State переменные в Perl

Tags:

В Perl существует особый тип переменных под названием state.

В доке про них написано:

state declares a lexically scoped variable, just like my. However, those variables will never be reinitialized ...

На первый взгляд это дает нам возможность очень просто реализовывать счетчики и иже с ними:

sub count {
  state $count = 0;
  $count++;
}

Однако, есть нюанс - фраза will never be reinitialized означает что переменная действительно никогда не будет переинициализирована пока существует родительский скрипт. И это дает нам вот такую замечательную граблю на которую можно ненароком наступить:

Объявляем пакет:

package MyTestState;

use strict;
use feature 'state';

sub new {
  bless {}, shift;
}

sub count {
  state $count = 0;
  $count++;
}

1;

И саму программу:

use lib 'lib';
use v5.18;
use MyTestState;

my $mystate =  MyTestState->new();

for (0..10) {
  my $counter = $mystate->count();
  say "Counter [$counter]";
}

Вывод ожидаем:

Counter [0]
Counter [1]
Counter [2]
...
Counter [10]

А теперь добавляем такой код:

undef $mystate;

say "next object!=======";

my $mystate1 = MyTestState->new();
for (0..10) {
  my $counter = $mystate1->count();
  say "Counter [$counter]";

}

Здесь мы удаляем старый объект со счетчиком и создаем новый. Логично предположить что счетчик пойдет заново, но на самом деле нет. Не смотря на то что мы удалили старый объект и создали новый, переменная со счетчиком никуда не делась и не была переиницализирована! И при запуске программы мы увидим:

Counter [0]
Counter [1]
Counter [2]
...
Counter [10]
next object!
Counter [11]
Counter [12]
Counter [13]
...
Counter [20]
Counter [21]

Так что слово newer в документации действительно значит "никогда пока жив инстанс интерпретатора запустивший скрипт".