Skocz do zawartości

Przystawka do OctoPrinta (autoconnect i klawisze pauzy/wznowienia)


ethanak

Pomocna odpowiedź

Na wstępie od razu uwaga: o ile prezentowane rozwiązania mechaniczne (tzn. sposoby mocowania itd.) dotyczą Anet A8, o tyle elektronika i programy są uniwersalne - wymogiem jest tylko Raspberry Pi i OctoPrint.

Zaczęło się od tego, że muszę w końcu skończyć parę robotopodobnych konstrukcji, a z racji niewielkich możliwości warsztatowych (nawet imadła nie mam gdzie przykręcić) postanowiłem kupić drukarkę 3d aby parę brakujących elementów dodrukować. Po przeanalizowaniu finansów (i przekonaniu mojej kochanej że bez drukarki życie mi niemiłe) na szafce stanęła śliczna chińska Anet A8.

Krótko potem doszedłem do wniosku, że latanie na drugi koniec mieszkania z kartą aby coś wydrukować nie jest tym, co tygrysy lubią najbardziej. Oczywiście - istnieje kilka różnych opcji, ale postanowiłem wypróbować OctoPrinta (choćby dlatego, że mam starego lapka z połamanym zawiasem który idealnie nadawał się do prób - czyli w sumie mogłem próbować bezkosztowo). Po oswojeniu się z OctoPrintem postanowiłem dokupić jakiś jednopłytkowy komputerek, bo laptop zajmuje jednak trochę zbyt dużo miejsca.

Niestety - wyjęty z szuflady NanoPi po uruchomieniu OctoPrinta (jeszcze przed podłączeniem drukarki) uprzejmie się usmażył (nie będę się nad tym rozwodził bo to nie ma nic wspólnego z tematem) - postanowiłem więc kupić Raspberry Pi.

Po paru dniach przyszedł Raspberry. Postanowiłem nie instalować OctoPi a normalnego Raspbiana i doinstalować do niego OctoPrinta. Nie wiem czy to dobry wybór - ale wolę mieć normalny system, bo przecież raspberry może słuzyć nie tylko do drukowania!

Przez kilka dni Raspberry leżał sobie na szafce w jakiejś prowizorycznej obudowie, w końcu miałem tego dość. Co prawda typowym miejscem do zamontowania RPi na Anetce jest lewa część ramy zaraz nad płytą główną - ale wydrukowałem sobie osłonę kabli osi Z, a pomysł mocowania jej do obudowy RPi uznałem za bezsensowny (niewygodny dostęp do komputerka).

Natomiast idealnym miejscem zamontowania wydał mi się wyświetlacz - z tyłu za nim jest mnóstwo miejsca, nawet cztery śruby M3 mamy do dyspozycji. Na Thingiversie znalazłem gotowca (mocowanie), na szczęście najpierw przymierzyłem... i okazało sę, że albo gniazda USB będą z niewłaściwej strony (po prawej), albo wtyczka zasilania z kablem będzie wystawać pod ramą. Postanowiłem więc zrobić własny projekt, gdzie RPi byłby przesunięty maksymalnie do góry. Projekt wyszedł całkiem nieźle (pomijając jeden błąd, naprawiony zresztą w załączonych plikach - mianowicie zapomniałem zostawić otwór na kartę pamięci). Oto on:

Anetcover.thumb.png.e24daa8578539a783cc4409c8e465ac2.png

Montaż jest bardzo prosty - Należy po prostu przykręcić RPi do wydrukowanego mocowania za pomocą czterech śrubek M2 ze stożkowym łbem:

Rpi_uchwyt.thumb.jpg.77e0b71c5775ab2cc51f17159e9c48f8.jpg

Następnie należy odkręcić cztery nakrętki mocujące tylną osłonę wyświetlacza, odłączyć taśmę i na wystające śruby założyć mocowanie:

Rpi_na_ramie.thumb.jpg.89ec41b6ca6c205abcd5acb00661a74e.jpg

Warto również zastąpić nakrętki trzymające płytę wyświetlacza po prostu podkładkami 2mm (wydrukowałem sobie takie przy okazji) - przybajmniej można będzie całość skręcić zwykłym śrubokrętem bez używania jakichś wymyślnych kluczy.

Rpi_podkladka.thumb.jpg.c53cad0c2724aa413d2272ae604f4863.jpg

Tak zamocowany RPi został podłączony do zasilacza i działa non stop, niezależnie od drukarki. Dość szybko okazało się, że OctoPrint ma wadę: nie potrafi automatycznie podłączyć się do drukarki po jej włączeniu czy resecie. Niby drobiazg - ale niewygodny. Postanowiłem zrobić jakiś automat, który będzie to robił za mnie. Jak to powinno działać? Program powinien wykryć moment włączenia czy resetu drukarki, i wywołać funkcję "connect" z API OctoPrinta. Tyle założeń, ale jak wykryć włączenie?


Po włączeniu pojawi się przecież nowe urządzenie USB, a wykrycie takowego jest już proste; wystarczy odpowiednio oprogramować regułki w udev. Jest tylko jeden problem - co prawda demon udevd potrafi wywołać jakiś tam program, ale nusi być on wykonany jak najszybciej, przy czym oczekiwanie na wykonanie takiego programu blokuje całe działanie demona. Czyli nawet gdyby udało mi się jakoś skrócić czas działania programu - i tak zadziałałby zanim urządzenie pojawi się w systemie 😞 Teoretycznie można by było zrobić fork, w potomnym procesie poczekać parę sekund i dopiero wywołać funkcję OctoPrinta, ale wydało mi się to jakieś takie mało profesjonalne...

Postanowiłem więc zrobić inaczej. Główny program uruchamiany jest przez systemd przy starcie systemu. Przez cały czas nie robi nic, po prostu czeka na sygnał. Po jego otrzymaniu odczekuje chwilę i wywołuje octoprintowy "connect". Nic nie sprawdza - to wywołanie jest o tyle bezpieczne, że jeśli drukarka już jest do octoprinta podłączona po prostu nic się nie stanie, a próba podłączenia wyłączonej drukarki skończy się po prostu błędem i tyle.
Program wgrałem jako /usr/local/bin/watchprinter i nadałem mu prawa do wykonania. Cały kod programu jest tak krótki, że postanowiłem zamieścić go tutaj.

#!/usr/bin/env python
#coding: utf-8

import requests, json, re,  signal, time, yaml

class octonector(object):
    api = None
    def __init__(self):
        if not self.__class__.api:
            conf = yaml.safe_load(open('/home/pi/.octoprint/config.yaml'))
            self.__class__.api = conf['api']['key']
        self._enabled = False

    def enable(self):
        self._enabled = True

    def enabled(self):
        rc = self._enabled
        self._enabled = False
        return rc
        
    def doPostRequest(self,cmd,data):
        path='http://127.0.0.1:5000/api/%s' % cmd
        rc=requests.post(path, headers={
        'Host': 'localhost',
        'Content-Type': 'application/json',
        'X-Api-Key' : self.__class__.api},
        json=data)
        return rc.status_code in (204,200)
        
watcher=octonector()
        
def ena(*args):
    global watcher
    watcher.enable()
    
signal.signal(signal.SIGUSR1, ena)
            
while True:
    signal.pause()
    if watcher.enabled():
        time.sleep(5);
        try:
            watcher.doPostRequest('connection',{'command':'connect'})
        except:
            pass

Jak widać użyłem tu dwóch bibliotek, których w systemie nie ma - requests i yaml. Co prawda mógłbym skorzystać z wirtualnego śwodowiska OctoPrinta (w którym te biblioteki są zainstalowane) - postanowiłem jednak doinstalować je do systemu (szczególnie, że zarówno python-requests jak i python-yaml są w repozytorium i instaluje się je zwykłym aptem).

Teraz mogłem już przetestować działanie. Po uruchomieniu i wysłaniu do programu sygnału USR1 drukarka została podłączona do OctoPrinta. Mogłem więc zająć się zautomatyzowaniem wszystkich czynności. Na początek zgrałem swój program z systemd. W tym celu w /etc/systemd/system/ umieściłem plik watchprinter.service z zawartością:

[Unit]
Description=Autoconnect for octoprint

[Service]
Type=simple
ExecStart=/usr/local/bin/watchprinter
Restart=always
RestartSec=15

[Install]
WantedBy=multi-user.target

Po wydaniu polecenia:

sudo systemctl start watchprinter

mogłem już nie bawić się w szukanie pid-u programu - polecenie

sudo systemctl kill -s USR1 watchprinter

załatwiało to za mnie.

Tak więc nakazałem poprzez "sudo systemctl enable watchprinter" uruchamianie mojego programu przy starcie systemu - pozostało tylko automatyczne wysłanie sygnału po włączeniu drukarki.

W tym celu musiałem znaleźć identyfikatory USB podłączonej drukarki. Przy włączonej drukarce polecenie "sudo lsusb" dało następujący wynik:

Bus 001 Device 012: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter
Bus 001 Device 004: ID 0424:7800 Standard Microsystems Corp.
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Stwierdziłem, że pierwsza na liście to moja Anetka. Tak więc pozostało mi jedynie dopisać regułki udeva. Do pliku /etc/udev/rules.d/55-printer.rules wpisałem zawartość:

ACTION=="add", SUBSYSTEMS=="usb", ATTRS{idProduct}=="7523", ATTRS{idVendor}=="1a86", RUN+="/bin/systemctl kill -s USR1 watchprinter"

Jak widać, idVendor i idProduct pobrałem właśnie z informacji podanych przez lsusb.

No i po restarcie udeva mogłem już cieszyć się działającym automatem podłączającym drukarkę do OctoPrinta po jej włączeniu.

No ale cóż - apetyt rośnie w miarę jedzenia. Zamarzyły mi się dodatkowe klawisze pauzy i restartu przy druku z OctoPrinta oraz jakieś ledy sygnalizujące stan drukarki (a właściwie OctoPrinta). Zacząłem więc od przeglądu szuflady. Razem z Raspberrym kupiłem w Botlandzie wtyk do złącza GPIO (kątowy - żeby się nie pomylić przy wtykaniu) - tak więc połączenie z komputerkiem miałem z głowy. Do tego znalazłem kilka przycisków tact-switch w różnych kolorach i płytkę uniwersalną. Klika diod LED i parę rezystorów - i całą wielce skomplikowaną elektronikę miałem skompletowaną. Z zaprojektowaniem obudowy (a właściwie panela - bo pełna obudowa nie jest przewidziana) już nie było problemów. Kilka linijek w OpenSCADzie - skorzystałem z tego, że w poprzednim projekcie miałem już zwymiarowane przyciski - i gotowe. Otwory w panelu pasują do rastra płytki uniwersalnej, a wygląda tak:

Frontpanel.thumb.png.5fb8a6d741a14834372d9ed2ad812258.png

Sam panel przykręcony jest czterema śrubami M2 (wkręconymi bezpośrednio w plastik) do bocznych wsporników:

Viewpanel.thumb.png.65679f8b00aa095d6377b17e558b1895.png Panelview.thumb.jpg.149efed81feadc8202747398e0aef326.jpg

Trzeba pamiętać, aby otwory pod ledy poprawić wiertłem 3mm (ja użyłem 3.2mm). Kolej na płytkę. Wyciąłem kawałek o wymiarach 35x32 (gdybym ciął wszystkie krawędzie wzdłuż otworów, byłoby to 35x27.5). Otwory pod śruby nawierciłem po prostu przykładając płytkę do panelu. Po przylutowaniu wszystkich elementów wystarczyło odpowiednio podgiąć nóżki od spodu płytki, aby wszystko bez potrzeby prowadzenia ścieżek połączyć według schematu:

Octopauser_sch.thumb.png.475baff3bad7e8664006b0aa20f90f33.png

A tak wygląda płytka przed przykręceniem do panela:

Board.thumb.jpg.7ca959cad0f3dfb9f010978061b6ab79.jpg

Pozostało już tylko przykręcić płytkę do panela (cztery śruby M2 z łbem stożkowym z nakrętkami) i zamocować całość na ramie Anety. W tym celu usunąłem prawą górną tulejkę dystansową między ramą a płytą wyświetlacza, i w to miejsce wsunąłem prawy wspornik. Po skręceniu i podłączeniu wtyku do GPIO całość prezentuje się tak:

Panel_widok.thumb.jpg.50deabfa620b596a717b58c38123b342.jpg

Teraz zaczęła się właściwa zabawa - czyli pisanie programu. Od razu założyłem, że obsługa klawiatury i obsługa led to będą dwa oddzielne programy - odpadła zabawa z oczekiwaniem na kilka zdarzeń jednocześnie, czyli z wątkami czy procesami. Zacząłem od sygnalizacji. Zasada działania programu jest prosta: program periodycznie odpytuje OctoPrinta o stan drukowania i zależnie od tego z apala odpowiednią kombinację led. W moim przypadku są to:

  • brak - octoprint wyłączony
  • czerwona - drukarka wyłączona
  • niebieska - drukarka włączona, nic nie robi
  • niebieska + żółta - wydruk
  • niebieska + czerwona - pauza.

Dodatkowo przy zmianie stanu OctoPrint sygnalizuje programowi że ma ponownie odpytać o stan. W tym celu skorzystałem z możliwości obsługi zdarzeń przez OctoPrinta - każda zmiana stanu powoduje wysłanie sygnału do programu i natychmiastowe odpytanie. Dodatkowo musiałem doinstalować bibliotekę gpio Pythona:

sudo apt install python-rpi.gpio

Program w /usr/local/bin/octoindic znów jest bardzo prosty:

#!/usr/bin/env python
#coding: utf-8

LED_Y = 40
LED_R = 38
LED_B = 36

import RPi.GPIO as GPIO
import requests, json, re,  signal, time, os

GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
GPIO.setup([LED_Y, LED_R, LED_B],GPIO.OUT);

def led(p):
    p=p.lower() if p else ''
    GPIO.output([LED_Y,LED_R, LED_B],
        ('y' in p, 'r' in p, 'b' in p))

class ledLighter(object):
    api = None
    headers={'Host':'localhost'}
    def __init__(self):
        if not self.__class__.api:
            import yaml
            conf = yaml.safe_load(open('/home/pi/.octoprint/config.yaml'))
            try:
                self.__class__.api = conf['api']['key']
                self.headers['X-Api-Key']=self.__class__.api
            except:
                pass
        self.getStatus()

    def getStatus(self):
        code = self._getStatus()
        led(code)
        return code
        
    def _getStatus(self):
        path='http://127.0.0.1:5000/api/printer?exclude=temperature,sd'
        try:
            rt=requests.get(path,headers = self.__class__.headers)
        except:
            return 'x'
        try:
            rt.raise_for_status()
        except:
            return 'r'
        return self.parseStatus(rt.content)
        
    def parseStatus(self, content):
        try:
            content = json.loads(content)['state']['flags']
        except:
            return 'ry'
        if content['paused']:
            return 'rb'
        if content['printing']:
            return 'by'
        if content['ready']:
            return 'b'
        return 'ry'
        
lt = ledLighter()

def ena(*args):
    # tu można dodać kawałek kodu reakcji na sygnał
    pass
    
signal.signal(signal.SIGUSR1, ena)

while True:
    time.sleep(15)
    lt.getStatus()

Jak widać, program sprawdza stan za każdym razem kiedy przyjdzie sygnał USR1 oraz 15 sekund po ostatnim sprawdzeniu.
Znów wpis w /etc/systemd/system/octoindic.service jest krótki:

[Unit]
Description=Led status for octoprint

[Service]
Type=simple
ExecStart=/usr/local/bin/octoindic
Restart=always
RestartSec=5
User=pi
Group=pi
[Install]
WantedBy=multi-user.target

Po wydaniu poleceń:

sudo systemctl enable octoindic
sudo systemctl start octoindic

pozostaje jedynie dokonfigurowanie OctoPrinta. W tym celu do pliku konfiguracji ~/.octoprint/config.yaml dopisujemy:

events:
  enabled: true
  subscriptions:
  - event: Startup
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: Shutdown
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrinterStateChanged
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrintStarted
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrintFailed
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrintDone
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrintCancelled
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrintPaused
    command: sudo systemctl kill -s USR1 octoindic
    type: system
  - event: PrintResumed
    command: sudo systemctl kill -s USR1 octoindic
    type: system

Po restarcie OctoPrinta możemy cieszyć się dziarsko świecącymi diodami 🙂

Pozostaje kwestia klawiszy - bardzo przydatnych np. przy zmianie koloru filamentu. Znów prosty program w /usr/local/bin/octopauser:

#!/usr/bin/env python
#coding: utf-8

KEY_PAUSE = 37
KEY_RESUME = 35

import RPi.GPIO as GPIO
import requests, json, re,  signal, time, os

GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
GPIO.setup([KEY_RESUME,KEY_PAUSE],GPIO.IN,pull_up_down=GPIO.PUD_UP);

class myKeys(object):
    api = None
    headers={'Host':'localhost'}
    postheaders={'Host':'localhost'}
    def __init__(self):
        self.times=[0,0]
        if not self.__class__.api:
            import yaml
            conf = yaml.safe_load(open('/home/pi/.octoprint/config.yaml'))
            try:
                self.__class__.api = conf['api']['key']
                self.__class__.headers['X-Api-Key']=self.__class__.api
                self.__class__.postheaders['X-Api-Key']=self.__class__.api
            except:
                pass

    def key_press_p(self,*args):
        if not GPIO.input(KEY_PAUSE):
            self.times[0] = time.time()
        elif time.time() - self.times[0] >= 0.2:
            self.cmdrun("pause")
            
    def key_press_r(self,*args):
        if not GPIO.input(KEY_RESUME):
            self.times[1] = time.time()
        elif time.time() - self.times[1] >= 0.2:
            self.cmdrun("resume")
        
    def doPostRequest(self,cmd,data):
        path='http://127.0.0.1:5000/api/%s' % cmd
        try:
            rc=requests.post(path,
                headers=self.__class__.postheaders,
                json=data)
        except:
            pass
        
    def cmdrun(self,cmd):
        if cmd == "pause":
            self.doPostRequest("job", {"command" : "pause","action" : "pause" });
        elif cmd == "resume":
            s = self.getStatus()
            if s == "disconnected":
                self.doPostRequest("connection",{"command":"connect"})
            elif s == "connected":
                self.doPostRequest("job", {"command" : "pause","action" : "resume" });
    
    def getStatus(self):
        path='http://127.0.0.1:5000/api/printer?exclude=temperature,sd'
        try:
            rt=requests.get(path,headers = self.__class__.headers)
        except:
            return "off"
        try:
            rt.raise_for_status()
        except:
            return "disconnected"
        return "connected"

    def arm(self):
        GPIO.add_event_detect(KEY_PAUSE, GPIO.BOTH, callback = self.key_press_p)
        GPIO.add_event_detect(KEY_RESUME, GPIO.BOTH, callback = self.key_press_r)
        
ks = myKeys()
ks.arm()

while True:
    try:
        signal.pause()
    except KeyboardInterrupt:
        break
    except:
        pass

I także krótki wpis do /etc/systemd/system/octopauser.service:

[Unit]
Description=Pause keys for octoprint

[Service]
Type=simple
ExecStart=/usr/local/bin/octopauser
Restart=always
RestartSec=5
User=pi
Group=pi
[Install]
WantedBy=multi-user.target

Po wydaniu poleceń:

sudo systemctl enable octopauser
sudo systemctl start octopauser

możemy już cieszyć się możliwością wygodnego operowania pauzą na poziomie OctoPrinta za pomocą klawiszy.

I to wszystko. Oczywiście - całą tę konstrukcję można rozbudować. Przykładowo - czujnik filamentu można dodać równolegle do klawisza pauzy, lub poświęcić jeszcze jeden pin GPIO i lekko przerobić program. Niestety - na razie nie znalazłem czujnika, który zadziała zarówno z miękkim TPU jak i przezroczystym PMMA...

Załączam komplet plików (scad, stl oraz kody programów). Octopauser.zip

Przyjemnego pauzowania i wznawiania życzy
ethanak

Edytowano przez Treker
Poprawiłem formatowanie.
  • Lubię! 2
Link do komentarza
Share on other sites

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.