Octoprint farma a gcode soubory v gitu

Jelikož mám aktuálně k dispozici 3 různé 3D tiskárny (tedy s nekompatibilním gcodem mezi sebou) a ke všem mám připojený "OctoPrint":https://octoprint.org/, začal jsem vymýšlet, jak řešit ten zmatek v gcode souborech. Na vstupu je STL nebo jiný soubor, který, vzhledem k tomu, že ne úplně málo věcí jsem jen stáhl, je také dobré někde schovat. Do octoprintu přijde nahrát gcode soubor, podle kterého to tiskárna vytiskne. Ale když tiskárny nejsou stejné, tak je gcode pro každou tiskárnu jiný. A další věc, která stojí za archivaci, je factory/project soubor ze sliceru a nastavením pro ty dané objekty. Vezměme k tomu v úvahu to, že v OctoPrintu v zásadě chceme mít uložené jenom ty gcode soubory (ostatní je tam zbytečné a zbytečně zabírá místo na SD kartě Raspberry) a také že manipulace se soubory v OctoPrintu je... spíše nouzové řešení :-) Takže, dá se to vymyslet nějak lépe, než tam ručně nahrávat soubory a držet v nich pořádek?

Odpoověď na základní otázku vesmíru, života a vůbec: git

(Jsem zvědavý, za jak dlouho po tomto nadpisu přijde výhružný dopis od právníků... no, nic :-) ). Git jistě poměrně dobře řeší file management souborů. Pokud v typické aplikaci se bude repozitář na Raspberry jenom pullovat, můžeme si s ním na počítači dělat co chceme. Takže hezky můžeme všechny soubory přesouvat, přejmenovávat a tak dále na počítači, pushnout změny do gitu a na malině po syncnutí máme cílový stav. Jenže co s tím zbytkem. Pokud nechceme pro každou tiskárnu mít vlastní repozitář a jeden navíc pro STL/factory soubory, pak bychom do ní natáhli úplně všechno. Což asi dělat úplně nechceme. Takže co s tím? Git začal v novějších verzích umět něco, co se jmenuje sparse checkout. A to zjednodušeně řešeno, checkoutuje soubory podle nějaké masky. A to je přesně to, co chceme :-)

Za zmínku nicméně stojí, že to umí až git od verze 2.25... a v kontextu release cyklu Debianu to pro uživatele OctoPi znamená, že to funguje až od OctoPi 1.0, která je založena na Debianu Bullseye. Tedy od té poslední... Ano, taky jsem to musel upgradovat :-)

Organizace repozitáře

Repozitáři jsem udělal takovouhle strukturu:
  • STL
    • nějakej_krám
      • obj.stl
    • jinej_krám
      • obj.stl
  • printer1
    • nějakej_krám
      • obj.gcode
      • obj.3mf
  • printer2
    • jinej_krám
      • obj.gcode
      • obj.3mf
Stručně popsáno: v root složce jsou složky pro jednotlivé fyzické tiskárny a složka STL, ve které jsou nacpané 3D objekty (v nějaké struktuře). Do složek pro tiskárny pak (v adresářové struktuře) ukládám samotné gcody pro tisk a případně factory soubory ze sliceru s nastavením. Na počítači mám tedy celý repozitář. Do konkrétní tiskárny chci ale stahovat jenom tu jednu konkrétní složku a z ní navíc jenom soubory s příponou gcode.

Git magie

Takže se přihlásíme na náš OctoPrint server (v mém případě OctoPi běžící na malině). Vlezeme do složky .octoprint. V ní je standardně složka uploads, jejíž obsah se promítá do webového rozhraní (to co je v té složce lze v rozhraní naklikat a vytisknout). Zvolil jsem v tuhle chvíli tu cestu, že ve složce ~/.octoprint git repozitář stáhnu do uploads-git (co s tím potom o chvíli dále). Takže například jsme na tiskárně printer2, přihlášeni přes SSH a ve složce .octoprint. A jdeme čarovat:
  1. git clone --filter=blob:none --sparse <a href="mailto:git@mujgitlab.cz">git@mujgitlab.cz</a>:ja/gcode-library.git uploads-git
  2. cd uploads-git
  3. git config core.sparseCheckout true
  4. echo "printer2/**/*.gcode" > .git/info/sparse-checkout
  5. git checkout master

V prvním kroku naklonujeme náš repozitář, který někde máme (URL adresu upravíme) do složky uploads-git. S těmito parametry se založí jen repozitář, bez obsahu. Vlezeme do složky a zapneme sparse checkout. Dalším příkazem nastavíme filtr toho, co chceme checkoutovat. Tohle konkrétně říká chceme jen to, co je ve složce printer2, v libovolných podadresářích, s příponou gcode. A posledním příkazem stáhneme reálný obsah větve master. (Následně klasický git pull funguje na další změny). Pokud se to povedlo, máme vyfiltrované obsah v adresáři uploads-git.

Zbývá jeden detail: jak jsem psal o kus výše, OctoPrint kouká do složky uploads, ne uploads-git. A zároveň v téhle konfiguraci je v naší uploads-gits složka printer2 a pod ní teprve nějaký obsah. Takže smažeme/přesuneme/zazálohujeme složku uploads a uděláme si symlink: ln -s uploads-git/printer2 uploads. Tradá, ve webxichtu máme naše relevantní soubory.

Synchronizace repozitáře

Zbývá ještě jeden detail. Repozitář v OctoPrintu je nutno aktualizovat, když do něj něco přidáme. Když se při každé změně budeme přihlašovat na OctoPrint server přes SSH, hledat repozitář a pouštět git pull, tak nás to brzy přestane bavit. Nicméně, OctoPrint má relativně schovanou možnost přidávat vlastní příkazy do menu System. Takže si zeditujeme soubor .octoprint/config.yaml. A přidáme do něj následující část (napřed se ještě podíváme, jestli už tam sekce system není - pokud je, tak to tam jen přidáme, nevytváříme ji znova. Nicméně, ve výchozí instalaci tam není):
  1. system:
  2.   actions:
  3.   - action: gitpull
  4.     command: cd ~/.octoprint/uploads-git && git pull
  5.     name: Pull Git
Zrestartujeme OctoPrint a je hotovo. Nyní pro refresh repozitáře stačí jenom vybrat položku Pull Git z menu (po doběhnutí příkazu se objeví popup notifikace o dokončení příkazu).

Update: 3.3.2022

První problém se objevil poměrně záhy a sice v tom, že na nově objevených souborech neprobíhala analýza a tedy nezobrazoval se odhadovaný čas tisku, spotřebovaný filament a tak dále. Popravdě mě nenapadlo, jaký hrozný ojeb bude tenhle problém doladit, takže pro ty, co si to chtějí usnadnit, TL;DR řešení je výše zmíněný snippet konfigurace nahradit za tento:
  1. system:
  2.   actions:
  3.   - action: gitpull
  4.     async: true
  5.     command: 'APIKEY=xxx && cd ~/.octoprint/uploads &&
  6.       prevcommit=`git rev-parse --verify master` && git pull && commit=`git rev-parse
  7.       --verify master` && for i in `git diff --name-only "$prevcommit" "$commit"`;
  8.       do gcodepath=`echo "$i" | sed -E ''s/[^\/]+\/(.*)/\1/''`; if [ -f "$gcodepath"
  9.       ]; then curl -XPOST -H "X-Api-Key: $APIKEY" --form "file=@${gcodepath}" --form
  10.       path=`dirname $gcodepath` --form filename=`basename $gcodepath` "<a href="http://localhost/api/files/local"
  11. ">http://localhost/api/files/local"
  12. </a>      ; fi; done'
  13.     name: Pull Git
A pro pobavení tedy k tomu pár poznámek:
  • xxx na začátku je potřeba vyměnit za API klíč (pokud se použije ten s omezenými oprávněními, tak za ten, co může uploadovat soubory)
  • ten kód, co vypadá jak zašifrovaný, nemá smysl nějak formátovat. Octorpint ten konfigurák stejně při restartu přeuloží, takže se to stejně zase rozháže. Zároveň je tam i escapování znaků pro YAML, takže prostý copy-paste do shellu nefunguje a je to potřeba poupravit (nebo si ten výstup klíče command vypsat pythonem, kde to vyleze bez escapování)
  • tenhle one-liner je hnusný a nejspíše za něj skončím v pekle (nebo za něco jiného). Nicméně napsat to jako takhle hnusný one-liner oproti hezky naformátovanému scriptu má jednu zásadní výhodu: config.yaml se při backupu octoprintu zálohuje. Takže pokud OctoPi třeba někdy v budoucnu upgradnete stylem záloha-přehrání nového image-obnova zálohy, tak tohle zůstane funkční. Script by se tam musel zase ručně dohrát potom zpátky
  • script pasuje na řešení jak je popsáno výše, se symlinky a tak. Ten škaredý sed odstraňuje z cesty první složku, což je kvůli tomu, když máme uploads symlink nastaven do podadresáře. A taky se počítá, že se checkoutují jenom gcody; pokud ten sparse takto nastaven není, pošle se do API na přidání něco, co tam mít nechcete :-)
  • async: true je velice důležitá vlasnost, která se tam musí doplnit. Bez ni se stane velice zábavná věc: OctoPrintí server zjevně běží v jednom procesu/vlákně a nedokáže obsloužit více requestů naráz. Pokud v konfiguraci tedy není async: true, stane se přesně toto: kliknete v prohlížeči v roletce na Git Pull. To pošle HTTP request do octoprintu na spuštění příkazu. Tam se spustí ten command a čeká se na jeho výstup. Teprve po jeho kompletním ukončení (i všech dalších podprocesů, které se tam pustí - takže dopsat ampersand nefunguje :-) ) se prohlížeči vrátí odpověď. Jenže příkaz pustí curl na API octoprintu, kde už ale visí ten váš request. Takže curl čeká, až octoprint ukončí předchozí request a ten zase čeká na to, co vrátí curl. A dostanete se z toho až restartem octoprintu.
  • V API se volá metoda na přidání nového souboru. Zní to, že je to trochu blbý? Je. Protože to co se stane je, že se na kartu maliny uloží nové soubory z gitu. Pak se každý z těch souborů přečte a nacpe se do API octoprintu na přidání, což vede k tomu, že octoprint ten soubor přepíše znova tím samým obsahem. Takže blbé to je. Problém je, že jsem nevymyslel jiné lepší funkční řešení, jak octoprint donutit ten soubor analyzovat. Ve zdrojáku API je krásný příkaz analyze, který to přesně dělá a chybí v dokumentaci. Říkal jsem si proč, až jsem na to možná i přišel. Potíž se zdá být v tom, že octoprint si ty soubory, metadata a všechno možné cachuje. Takže zase, když se mu zjeví soubory na FS, tak toho o nich moc neví. A ten puštěný analyse přes API se pak chová divně. Když se git pullem natáhne jeden soubor, tak to obvykle funguje. Problém je, že jak se tam zjeví více souborů, tak se začnou v té cachi objevovat nějak náhodně a analyse úspěšně uloží data jen u toho, co zná. Což suma sumárum znamená, že se ty metadata uloží jen u některých souborů. A nepovedlo se mi vymyslet žádné jiné lepší řešení, které by fungovalo spolehlivě... až add volání, které ten soubor přepíše, ale udělá vše potřebné.
  • Neřeším soubory, které z repozitáře zmizely. Sice by asi taky bylo dobré, aby se o tom Octoprint nějak dozvěděl... nicméně, ve filemanageru to zmizí a ty metadata si to stejně samo upravuje při startu (ostatně i teď při restartu octoprintu se ty metadata dopočítají všude tam, kde chybí). Takže pokud to není systém, který má uptime v řádu let, je to asi jedno.
  • Elegantnější řešení by bylo tohle celé napsat jako plugin. To se pak snad dá ten soubor i korektně přidat, korektně zanalyzovat a vůbec tak všechno. Jenom pod vidinou toho, že napíšu za minutu dvouřádkový script a bude to taky fungovat, se mi do toho nechtělo. Script jsem nakonec psal týden, takže ten plugin bych možná za tu dobu stihl taky, ale co už...