From 41a018febcd0a3f9fb67c181e210fcfcf5acb980 Mon Sep 17 00:00:00 2001 From: Andy6M Date: Tue, 24 Mar 2026 10:52:18 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E9=8F=88=EE=84=80=E6=B9=B4=E6=B7=87?= =?UTF-8?q?=EE=86=BD=E6=95=BC=E7=80=9B=E6=A8=BB=E3=80=82=20-=200.10.18=20?= =?UTF-8?q?=E9=8D=A9=E8=99=B9=EE=94=85=E9=90=97=E5=A0=9F=E6=B9=B0=E6=BE=B6?= =?UTF-8?q?=E5=9B=A6=E5=94=A4=20(2026-03-24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Made-with: Cursor --- .gitignore | 5 +- .../bioyond_cell/20260302-1.xlsx | Bin 0 -> 13932 bytes .../bioyond_cell/20260302-2.xlsx | Bin 0 -> 13453 bytes .../bioyond_cell/20260302-3.xlsx | Bin 0 -> 13583 bytes .../bioyond_cell/20260302-4.xlsx | Bin 0 -> 13441 bytes .../bioyond_cell/20260321-5.xlsx | Bin 0 -> 13017 bytes .../bioyond_cell/20260323-1.xlsx | Bin 0 -> 13112 bytes .../bioyond_studio/bioyond_cell/20260323.xlsx | Bin 0 -> 13352 bytes .../bioyond_cell/material_template2.xlsx | Bin 0 -> 10885 bytes .../bioyond_cell/material_template3.xlsx | Bin 0 -> 10705 bytes .../bioyond_cell/material_template4.xlsx | Bin 0 -> 10841 bytes .../coin_cell_assembly/coin_cell_assembly.py | 36 ++ .../coin_cell_assembly_20260112.xlsx | Bin 0 -> 18292 bytes .../coin_cell_assembly/date_20260317.csv | 10 + .../coin_cell_assembly/date_20260319.csv | 2 + .../coin_cell_assembly/date_20260323.csv | 7 + .../workstation/implementation_plan.md | 88 ++++ .../workstation/implementation_plan_v2.md | 388 ++++++++++++++++++ .../resources/battery/bottle_carriers.yaml | 12 + .../registry/resources/bioyond/YB_bottle.yaml | 112 +++-- .../resources/bioyond/YB_bottle_carriers.yaml | 116 +++--- .../resources/bioyond/YB_bottle_carriers.py | 303 ++++++++++++-- unilabos/resources/bioyond/YB_bottles.py | 110 +++-- unilabos/utils/log-origin.py | 385 +++++++++++++++++ unilabos/utils/log.py | 15 + 25 files changed, 1429 insertions(+), 160 deletions(-) create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-1.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-2.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-3.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-4.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260321-5.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template2.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template3.xlsx create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template4.xlsx create mode 100644 unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_20260112.xlsx create mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv create mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv create mode 100644 unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv create mode 100644 unilabos/devices/workstation/implementation_plan.md create mode 100644 unilabos/devices/workstation/implementation_plan_v2.md create mode 100644 unilabos/registry/resources/battery/bottle_carriers.yaml create mode 100644 unilabos/utils/log-origin.py diff --git a/.gitignore b/.gitignore index 838331e3..3a52c0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -250,4 +250,7 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2 *.bz2 test_config.py - +# Local config files with secrets +yibin_coin_cell_only_config.json +yibin_electrolyte_config.json +yibin_electrolyte_only_config.json diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..52a18b738a2ee344c1ffd547645373db1201ec77 GIT binary patch literal 13932 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 af4ql001EU5*^e1|WUyn9I+`;7xch%we%}26 literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-2.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6243a4d6d19aef37502c489a959a48e1427ea60b GIT binary patch literal 13453 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 N96;z8%<}W>{{R~Z=28Fv literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-3.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260302-3.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6749fa8546266a96f0a21e2fac85c299bb7270a8 GIT binary patch literal 13583 zcmeHuWmFtl*Dmhv8YBb_(s&@aYjAgH++7mfCAe#FCwK_%7A&~C1ZzAvw=*;EH#5xK zyVm{vz4fEIYE?Z?SDk&fo?Qwu&@ea036KyFhHwxN*boR%+9Gzg z&OlpdeH9OTpp!0>yNxw@E({cH4g?hV{{L?Oi$|b5anQDl1tS=gc#9TWg$uY)P{sWe z!qkt4cneX49xi_I8Y676)0M$It1+DMa z;#FH8C{sd_oX$=K`>l!*Ds2{gBiF+z4-_8BQ8gBa;`=x(7gTveLP~eN)W9vAQ{%Lu z;tN?hp%u4gbS3zAYb7ET!2}ojhPgYaxONQQtqx~4t+aZacUmg1R3f9M!{>^1+|SX! zBgThK8~A7OCNTl172vEO9OVe(fks4-m^8t{$3SZHlec@EWpOWS0p&J5yz&nahMf$_ zCcURsQjBjxpndT^5Pasi8hntS5d^4&@h5C<45_|MrWk1xNlEM%8?E?oEc7Gcvbl?7 zeRqWRyuuCm(#Bi!IuLzB1cgi4&CH5}XDzPInR3Q4tH0u2{|GEhL ztarWpQ4+{GdfmeWMLBdpnXdCjZZ67~^DDL{0+!}ADb(HQEpx2ILa%O!t0@u7Zp|eL z-Is_{;#5BSR3D50GVnZndV+>f_?t3g>$L%)z{UM%GV416t(};ee(nFO#Q%#m z_b;zr9w#RQ4FD^#{}aWxR^CN=%J0_9VtaBX50HS=2LrCunEbB~KgETdKQ+AY%&2n9 z@<_cjt;uuG!|-y&N)ki#&oG^AR_l^^`0@oFGu}}=>QJ)FkJ7sIpzM+@ihldk+$Dc_ z`9^La)2k<60v8FjUVpwO9Sd%aL`MRx3u*MyS2~H?q`|913M1^jqml@`PSCmh*DW_K zoLq8M7Q>!)g9R@cVhTk^V>GX0=7smqIdauL6;O44_8$5XI&?>A4E=&e>mhp&WHq6r zB>pNW4LWn@=8zkDy zm{uhu<|;kf&UEFQ?Q%M>#8qBY=*_{tdG~jYF3K0mt_#y-FM@-Mtb%%8_tBZLhVN@y!HjrL_^TR?P#S7u43-W6(!A`aSDcRF<$n01K-zmn}O(cJQ1`yz=Dt7*BA zw4Fv+C|ia3z8VcKTh?3@>O~@@eR;>ox+;k3)A}H_hK(%>)*16(>~&pWpM3 zs^lx_$~aJj01~N=_ia+n=$bU`o#hfd+VklS7cv`;_I9q6M(?1gG4Ax}!wfXyAkDDy z8*&~$)s%jQM})_;LLV@Dg$v^dPX)6h81a%UI^x4?>B8P9qbZeiQf0hz3*dwpTLUR^>2BS^o5E_@j;@9yzlqBQn!qe- z!N$o`6%Nb>qv>h8YD7y;inqa*w$*^uPs-D(IjWaLWsZiAn|8ATIutgKliQb*c2(`~ zA9tGda}q>&+dUty25S%WeID;Fx3k;ZQ+&>+7svERv%LX6R}bk``VW`=)AwgmA0UR> zOXpPG$(`Q!DjfC{Bl(<9AQVlsjjO1W9+D3q7h{`HyCG{{=9nY(U6NMW*|=Kg;pUE; zfBFc#xU(I$_qU(4$TMJZ7RJ?dDZ6G1;%+joKKZt9;}1mCEW|2LyDLS-FB|TaYW-}# zR3Umo1~Zh2MBcZ;%3QYQ1Fl!NbmurqncfCQNjU7XW)ZfdhCXx&-( zZiudZ@$B{R1;})3uzz|LEa`v7Na`^pGOl1_zyAsXf&hHS9|qsa90+uFV*c$1@XPXN zq>aSo4Pf?zKA!N>a(hwvO<77&SQx{9=tMy5ba$&_7iKhyiSc{X3W?_Dx%ynShM-T% ze@J}9bB{Ngi&Qc|MR4i*4dI9SlWh36{G1%|)s`1|eNHj?hVO|OENd&+{o~0%U6Y#{ zH|M)IA#L9!*+6yYxz{+6iH+o{ZK?OC=y}Q9H*qig$*p`tlzG(9G+kdbXqEUNeAHWr z9P32Xx^i&5V7*R#+wk29%e{GNkD$KJwRx7ub6slvMeraa|HcFVMS#Vj0BL}7UnZ5% z7I6IO0wZg(Yw_Ti+NZh4%g>nc_N9x%kT%of6w{AYz(qWF)KAUH=xD|j4$p*unH=W~ ziKirJKF`7AqH}or_rU>P2ze}C!|3W3BoCiX-NUg#6#kJdrMhEwEz~FP0-i`qx?e0$ z9_e1)N6-)rrY{vQt6{wjDX*)ntXstwyFA{IY34b&mp5yTV8g34xyCu}8`59g06|-; zUQM)l!*xEOu;2vFc~oM2_R=f^Mz8LECt1bnBUVZucM#;AYGI>s3E@bsvh?6?wXe} z#y@N=qMh8_yR%Ow?HdxLOTlh`9dL3QBjm#)x8{M0>Si}dez{)f&(G0pt?o_Yr+;lt zBEtDr@W`X;p8U(0_qq4@!Rjg<>J#0_M&4W^ePk0M)oZSRBV;b+<`KQB#bb-h;Fydh zjD{e?yChQKIY-_(JlD0{!jSQ(C5VRE7gt`S6;<8^5qyS-i(NHtGWeh%Qfp*h0t>6J z2o)aqhS~&6T)df^KcTt6v<5a|Htk~e%kMs{*)us*@0M%blBS3?0|aMQXp&)-@^YmM3CT??Pa z(ofY07a>)Yr#Qmmd;>CP))ax%!$<-2x4@u5+?dwk>f(XH zA|%B9gHmCC9xGKz1^pS^IH9KI^zf}5l*OrtV5OFdm>d#bT&x6>#ywA~j}Z*EcF-*7kR>5_OfbXqNSwqmi73U|F$Ed?JYaoC8Z|%5Z&)PJp9_8U^Z(n&; zxT)*x%;S~J4y1dkE9J>r5ABcYx{*J9a_VL(8K&uNI7AHTC;_;5XbZGk#W3K&HU3nm zWfR5?7rv+#_D53KjY~t?_Fq6^q)No4=h{F@T(l3GsUzgDp|;z^U9d%iD@G7VT=ZI2 znyynsR|nP?UT%e3=TX)U?b|kL7wNrRJ#v)Vs3y3Xv7C05ibDEQI6?=4#cv!=w}7lA zHomdlD5OXttcGF|zlbK-BN24Ki=(4yMNTnf={NA}#Da!~jt_W`7OQW?Z}_>Zqst(w z6Aqd;FFtnP~zjT7Zqdmkc@+`psj88WZ^e|Cm=4 za1@2)N^l68m`0d2)FvXKasx>iAsFg0;VPeZq0Tf^vXLHfgydnVzYc|EAjxm37_j22 zsgL{z=}3RwR%s%}jz?i7%o#WrXpQKA-~d}937ZaD$wK1iO;Yv?t8}C+%5~RLl*Kd^ zc0#?+h=HY$5k?V~71~n4seLd4G2~VwXkyn;^iZ3~go=bRNQ%+VeLK2RIq}HKK$3M% z#D0U@TX&y^B)iik&Cl@JREquAGdt3UmmF~5rfBq7$_)f#q{lE)pixwk?pB;v=~$hP6};d5@8)7dZo@C zr^X`xu}APL=9Xm)lyuZCrjCPu(qJcEBLWZru(8TQrlX?9{}`r{<)e}{Y@s)}DSPxZ zj`Sy@Zq)3K^q)b%i9AsoP>XY zPk6HAIKa>Q1_XXy6@w9f^Q1t6z!d+167qA4XJZtafYes(+g{B_W>E4D@Ah-30a-R{ zgE(ZR_532CT6%^NAAi`T@ZqODw3SouT`>yld8CYBGJ)z#XnJjGMN6zJ#S~NN?ugv$ zZR4%XwiEpBkZWKPm4gwi#Xet`?HnnM{x!>Vu=33EkSdYZ2t8`U*&f~5RM=U2`_pEl z{^R+9z|-Yww$Dxc^(iQOEriulS0zuasr^KakaW?+Ik-(z?~I_!PrQ>~p9WG_=2{6! zBMm|^e9Zb1MjP`K!|XFa54KZiKVua_ z6@ju9SO^Fq!e7PN-(nSKbD#~7`M3RVValPVj2$r-MjJRm)i`X8H95c}jD^;Y{!*U_ zQyoVsVVS%D?9pYC9QtT0!r@_u@@`GBo}-ATQxucF}}_ zFPmeZPnd1J?O zzzQuJH7rr3RT)Jn%c{tJs*aV_=qLB9?tu2=_lQrAJ4PS*gTj>PrUV6V{L&z`{fUU> zEs-cpb7h*`Le2X_eh9m=_cRuO6sf3M_O4YQP8rhi>CKIqM7NPNMQn2foSK>hxiu%9 z&dgP0wW_%GM=FHd;S`!84;pVrylcmcAZZZ{3Yr)jMrX7d`TL1-?#o2ns;0H8G~Gp> zZsSI@Q7_HbIcR6~^}EpFbWd`FW^+hZHM+PO78m)n+u^pacd*)2m-nZ&le|&G>7;06 z_AK+7R&>i7d?N{$9l92yZgc86)%9}8iNg<-lp{6i)wi|$Z2|lczEDb3Qu~9N!Tq$p zCdZW0;u-EUQFjz9h!t6CLkf0E%n>T(}#$>SVXco7-SGD|5;@<3|?`o@nV3){WMiDfHDatT&PjRMZXX4bH6|9Xwz*g4&y2-k#I<5 zQ!{Nn)Ka#oU`|g#4YBd?w=$dpEHUoh@5-P#7~}qZTP&j+mu#2}M9!oYxec3_8_t6< zk>#TFL9UWe4yjEjfePAf^X=<4Z2MX+=wu6$b9kXVkz_`eO{Z9C(g|Nw&=iH`LO>LR z&5>_YPCI%og90TbQdq0Vj3{(i)w#=O5pJ36LwOJHoRj7xnFf0boX`QLlwQ`~qZH%b z*ic(o8wucIODb;Z0>8tqnq8qOj$yzjRHFPu^{=qcmWP@3E?9T*4fdNtwTM z5bsB@N`iE4l;oxOQG)W=@MUqN%dBI;w%4v}Ry%{+ZDbTNNiWC?y{Rdr@P{hOqG1!~ zhnyYsslz8wiN~}azn3u68-l&2w{{1UZZ&5oeAEdq1_*n-gtOaWRkG|M0=@#qmklQ( zfq?g!WS5LOJ+Vor-0|7igk(Lm0kvLojd8IlWgi2R0g!xoFJ@X9`9Ep9K}?!t2YHNX zmX$oR(xCg9ywS74*&JscAX&Cf{Gjql&#f@_*bN_!c3vIOmGX_#XxUKI3V@*K9*gM- zG&2%gH0<>#@Ww0^!$DT}wGdNTGvDg;E8XPU_h3es!_MVaaTr>>6?0a#A7%eumLWBQ zQrz{>_ZE6cvN5rOA-PB`xoAN~bb+-?l4}v+C8{ybhKFlfGMO3Sfh(cX@Z0V1UR&A` z^GqM&YBefHe`0e>R#>^#ypO9ob{!n*zBL~Of>%h}s0p_|DoALFzWf4@Of|-Jf50Ns z7j#I0xr;WNA73jZOttgkl~lj_FmsJxVVX#tO+5#BpxewB&hepUQxceEG}af<4&)=B ziC^BbibMOZJ5}1ZO>b18tHoI(t@DR=zqAuAx+n6-v8uBi@}t1C?^GUcCB*s?|2nk% zTRV1;g?#f)LC)x>_aqaq#m~cy;ylo1skBn^vd_d0=;6`obPNQ>kk@IIV6URej(-$r zd5n^e)R=0O=av?x%dYCha+T9Z6a!V|m%rLzyqc}Y*MSe)FUKlUSx%-!2)5WZNwOH) z01~NX@R8Na@peP|t%vO(wc~Ph{Ro?_xF>gGwNmxd$Y8kS9B(&FX=naC!AKtcVrfgh z1m|Y?$mUC%pNPIUzRq|s-tzswx&ve4Zvv;_TIB-xD(dgv zE{SorSrcR%W6g|3)}3m{{Z9Txm2@yuSb51>HMaA6c+9M@j5CkJd;8e0BJk5l=Ok>M zQitVIM{niZ9xav1I9~1|3*$iHwS3GBQktGS-?tW$)^QUR<37X@PSWU98EyR<2eVtx zufocF@zADjG(CLk%yg>+ET$U5UR)=)U zD#|dA04RfZlunaUj1YG5t)dC{H{HTG9w7GMUUYyOb=$-d%_2Mg(H7^nHFZwal)vy^ zo^^egl~FZ{wR(%*_3Pp*6Su4#gH54Tp&5D_$`G$J-Zl`go|9*^vpwvOGKtm43%)@g z@0+MMH)j(ZW}QxGJuSj%bBk-T)In>!Uwnr(%d^rdb<<)fL zkH};X8n!$>K=OlSqSMy)SbpcXHatW|xaf_>*M2P*((g6h8g)q64&PrGX_Ln>m4${p z2!oYQ8CS$2lz!U#6-r=~*xcg@_sgWvqYBma|fg8E3ae!%Z9OBC$ zfAU!8wsD_V^w*<-N-_BHQu3!d_TQW_xM(<`4bn0DMqPBTtLHXb>9wR_=_6M>O`S(&x+q-3RM;nDvpW;!>~(Q{a~Q{@?T}}*mi1V*a2tK?(H{}iakT|3 zrs`6UUuxxXVX5Prz=KR7CmizifS3#>!=XZAF z7k)_32jUC z#$`UCM9j@cW&da+-#eg&k)Eg}x^=wHE6A@tAD(w+;o*}Jaoc+rVp}Cc>4qcsZn@Zv zm5)zO!h0|Ke!(+zd6Up|B}(DRf8~qmiAFpzkc&PGG-Fi6QGFtZ4EuR;PQq~22Me|> z-Swg4=<;-&^r|^FU0le00VFq;%Wi2HeSEER-b(%%T>zDzh(QZW+{3u6Pxwfc1xr>S zH*i*?Ymsj#-UnR6{AZ|0iw_G~3ck_;0|9~ahix!7bOf3xJ3Cs~n*C-X@>Rx|djttu zW$w{OGPqe(twk4j4x>edzxO!Wx%S(}AIZ9y2Bin!`g51p!wZY{h$V;xZMOp^FZBu5 zb8)fwM!i;lny%cnMe^5g%p?G}ltT=lBT>o(g}#Ump;ZzXv}q8eK_CG+nHD+xVNG9XLpy_UVCZLCi6R(^Mb zd-d(sGR*`(z66cHWngA8p*WYWcMEAzZ#VR+Cic-u#Ro1yckA*HXDg^K6w#$a|0mspuyZLCFqKL#*?k0X^AKc=%_e5ji8`Mea;vIS*b zJI~`gHSEMtajRCLhQ`@RyeY&&Vbb1aEL$6MHJBtR2R*xdJ^?J@cGQsTnJeHM>LZcAjOS-Jk zBkht%SMDUFkjT7u8oo|n1*_OL88aWJoK!ms(hj7*a!b!-1a+WuZNp;L!tn&aWyCb2 z9b#V2;HwV1+AVeMV~N#-B|FsE+Vg5+5>+p`cMsB*i)K&_-(f0P)K)3R~m^*=%(X^-Ub+8tt9KA3Q^|xRK@3G0-xoz3$ns zGW(8rqBv;x!`@Qr@El25c=n{-XS(XZtvc@4_$zmq-bXz_32N1lqIi1{$Rz%dip^gB z#*6notrfdXs`jmJtXoe(nVwF6LBO@;gKYZ*Qw>=&e%k7oL)WDv%VZA}R| z<;ay%pk6({;J4A^U|VX6u<5o|Wf&KkUKzX8n?WwJ?$_#MhFD(TdL>wMfY4Bby`z%` zYrs2qC-htunaoVePkzvN8uhd4!!#Mz+wXuEB?yHI0G64w7YX#XJq8hxh+*;kI|dBN z9twkBGf5Q~({|*yS5eH(WUGImY{|JH8+l2-z4389^a9zFE^7;xnu*imcLV6Z)p>MS z3J!q^lMa#gTlzNQi0)V_YCr6fj){J)(o+;@O1gg>meT)D2YiQn&NefocaEBAvY$V5 zHa)4RH1aX`0c=SBIbeb*`8>5?Gs}((0YUTofZ2oXt25A18R+c%n?^o62QV2~*d8Vf zOLq(61V6Fsl`^Sdh}u&y6^O}f0xc{`)rgeS)V^YcM`4WT=hNkG2y@dA6LI{`AUrrt9d0TLBN`b^e8SmLHzXN*oLpSt_{T)VnL@EeZt` zoQL8~X`=2#4(=W7jn6z!WgmP>nuE8zbQ8<&n<~{F>4Dle#^fQs4}}5HUmQY8szF~4 zxq$5E^$r7<7;@A;qjgO?KU31pqF&a`b&?z_?3R4Q15k?~AV5-(!vy+bU_c2A{^g_< z!~I{~YpLQJlVb3dPhdsC_+9SCb~ZNPMmeY7$}o{i)s{kmj_Ubg68BDLxGNTp+kv1vwCHDnOVbtk_$@Pu(f9Rj=c^!IRb3|Bs3A6Fqe@-*NPnfV@HeXm%3!3?&Kmy4S$ZKW-m>AZ8 z&DYHh54(j&)yvQf?;R!ZQ`kAPp%>G40(NRT#UQeB9R)>M%eH)I-f)kG56%$aA=Lskmn{UmF9`@YTV%gxR8?!n{A zLnA2tUAySe{IX3l=}CoRxqh|l^iw!dhVhyJx*G$17FcHg)MohBSr}2UR5ig;#RM-O zo7fpEINI4eF&o=C0{>`z{O_tUcC1Gp;PkG-wu=mUrPl#PgctpWvPfvvNZNw7 zY#BH$By*SBT7P7_B!)#d?JlkI{fx7ANpLwq?Nf^yQY68|@!g0jp5m9V+vaHz@pF+y zRO3Q&bcTvh>pha|$QUZ%7>%GKOWwqn{ z2YKVAH`*sc8A}q{a(xXpd7MYa*Y|)G+bq31)oy8*#tq+aS^FVWmf4gIAhtEzTL$O; zvQ!shpIu$pjYoFR%7d)Q{uc)1P-KB<)nh|mw!m~lW!JYdGSi4@|^Uf<(B z$R8E<4cb%oo<6H zI(otswEmqBN26b*D_;#G51>&eZMRBEn#aZ>?~D3Aum(WHom>xScn=|Bs;#G{Csj|# zx*J^>?UIIsc8~@_5qi@?63jp#j-~=NsKkjb>SDy}_C%a!u6H(HvBG`ck+3wzO2_T= zm~>ISCa#_Nb*(crfa(h!*1K8QHse6z$(XgBSm!tKpLYT*hVo(ktB6gYHQv;rVfQJ; zHM^oMSzv3Nev7stgkAh59EP2jLD-pau2E3IuTuZ5oUQKK+u7CA{d7IWplY_Y)LJXO z{)tI=rEtMo9=JKCf#F81FR-C9V%pT3}bSibmA*+^>Zeq8CtM! zQOz$9+EzSo!AZX~sUnekkZHQpZRvGV_nY@Oqpwoi&sOFE1-x`h zzI-qw)Wz03)l!|4mOsQhCJOQ7mSsAXKARDJt34>&(i`-nn*!k7!CA;`GlCC#~Ih$jgOY$UYmKtSQC3;P&8ZiVBk`_ zh27?+W&4#H)()tV01OpQR&iK23UuPS+AN8n43v})2euTi__2|MBif6O*c@>6F4VZ1 z&Y&|d6r*5AmO1+0cc2HjuSMmV-O3KKB?DP!k4Vb# zyMCxO-47uXqHqA59xCx+VPQQ+$k{j0?Pq=bU~@>p7sGc>jL%FP}yz$o!q)?+w_0B^UwQ`M^`0~r9KD$ zy$kLyP>439-@yNup19{c&s(GZWI9FppNIG(H~cdze9rQG(eh80OmNZ!ewF7-n9m8G zm%0BWC+Bec5-6G{|CEcCU9g1{!w;+=5>V3|0d*gZD zf8aUu!|u$^T%Vc!ULB(#4F!z_0Sf^S0Rce&!59sPLPN ztl+Y93#t;F{A!VKSs>oGcltTo$k;Y?UTt>gR&CU}?6;cAl*-`|Q=zkk+AbHUUl8I# zru6(Wc@h}_R0^;b5cab8aX@b-?sqOcC?gVQOVY{itruIGFPtP{nIFrZ&!jF}7TN^B+|P3zwBR z8VUje0qhP@!B@CgGCJGXTNv2bSp4*Tc`D;J8zNZkFVFbZ+gq+UtK!sU)nv89q>hxV zT8=1{*s5M#7G8y?;ByqIx(kKj>>{Gr3akf4bCS}Mbv{dc3iv;?;Q$4Ze3(|GB&xT-EGHSc1&xv{`i zFj9&RS%m_dC1WZh^dgB%zZe#sq8o8{I3A~3aCpOU)t-KAxGU6E7osQGSXoCkyC9~& z^B8{vGK$64J@Abo)#^?+3D-p9Xp)}U)?2O!m?V!Jc8K|M$%^YJBEk z4pb`hAgEC6r4c8e(=@TU6p#%WD11RuUDEBplySG9*@1e78GP?l5Ui&jLuP`QSD*b* zUR_)nL<5IT7&RbHi4AQJM*)2-5Vipkwe?K3NHMh3Ca`lCkn8S}9{rrRTF=zeg_9P4 z^fY-GefF7DB#2!ob)OY4f~I4Wo-3hPhWeeHVJtA0LnKU%Wz%~Ar~l@wC1=rmn%mt~ zk?&h_(>&MgfM**^wy0eW!HlHpq#;Hz!!2mYU%7!*@DqEO9UG3Hoi;VgG6aXsZGt}3 zEjb0M&f0xCY!TjUk?+J`g9G&EOW|@)OnBBH3%=8s@2@e2;S!XL(;|>h?={?eC@au# zFD9PGK1q%(YE=o(B&fk0!r&`=(`?ySz9V#*u1fccRguPJ)~;n0WOeH^QG>O6<))(} z8&INgIm%<%l`0Dzm7m6AdC0M=?1*iu($-n5o$P(PFv>AAko*X8yto@qlkaf9oMKcM z`jGLi!{h$2ceH~4@&0^&e5E7H`)uwY$~&3=(dc3QKrQR>dWK}?qQvA9min>TKyA%; z1c6n!DlMD#ZdBYby%(-)LZ_}Oh^XPCb9BmlT=Bl(XRLHWmnGlH{U%i&p_nN#-hKk4 zyZWv7io*xlwv8L2+|@U#2*w0<_TvVh>q;6BR4I*8#%XU_VD>3)&09uq?~*XE&Jns8S*npH*S8fy+GZW~X=z3%n?To-(SE1zY$)qcn3tV&g`pVGZkU!Q zJTNw+7o$+{21jKt{KR1}F%I_+N{*hRq&i?W@x>0%XJT<~UsF@!2jv!!i+pSP63A*k z#^lIs?l3_-B&)GJT)XLW6Y%!1%&)egfi#uIceM^94SF^>-MX%t`TknizUB@gsb8nEgh)Sei6tnSq1cFF|2E<9cX<{9=a#C7UPS!v|w?i}? z3+MF1-%gA-2m7a1z?}XwM7kdoX37s zBBzCk^6J)+@>sy#{=0ic73)RT`-X9zj`PjH7~Dg+Sfzyg&=zOEx(06|iIsMejy;J4 z>iX{%3#eDe0&aFM_l`~vDsDcK-r-s4aFh!?zcnSb+PI!5nIbniI6f>SAX?;AXBTX& zF?1p1o6Zs5oj>R?+nL#~UMxSdv|&8Maa&`vf=-LrY0Uk^b-d^~LWrQV5~+Xpd74K= z!(#lPWL_)i3Sm1nLL6^BSZnNvMwYofV<$im%W}OK^U&PG6qLB6$-yz6;V>%V>~wwR z;sS+CqGykqdD6=`2+AKC+cS&0C9Ng#7#OJeQkya40RK3(_87>dmv#lZkG9%b#E95b z8KoZ8+~spEt!0p%>YxvmI#^V_|DwL}oX{eR&~OdWUEGE@+ADt2+h(=6lK*^5-uPCM z;A-Z@*B5CjlpqeMy^JsJ6}5Tum09suaO1q9T?bN@{OaW=+*QvrsFmI?aiI#H#vrf> zFtg%QLQdx$^(@DgG`I6dTidK|-}3E2>k7fuIiRi|ls4GeSK#XdkE{=KNlJVlGa4cr zpo-{#BS>;@XWSS2nBv8-U6lCRE{6vqh#GVCIH#x%h7deVaUWEjUTEQGA0C?Uc`X}% zbepttw$@bY14a` zr;-wb@0Q^9I{*e`IOQ%Y^0D+d?IX;L)Oe1fkrhcs*^b`n#k|HRSI2~FG)Cc=63b=a z;NuV(uvtf#3H$ivXgXn>Qf^RtE^I`tC=sThPnwG6a}4o9w-;VIZKadIkqBIxrx z6Y4lHvDRLV!+x9DNQAWs+)`)70)F%Et1<%s0C%lYIjMDS?-GTY6pd1c3d55d=Zt!uNaEQV9eXy zUn1rEPYt&cI2&({GB;lSEKxSVU?|(uCdGfH zz>S|aqM|94{2XkdRJ)c+Uu8yFkKb^X8URPxl);NjrH1vvT@e|ZK1t>D%?e0btm^B< z6?f_pWXfmA>XEdCGS#_H5$W9T{7Nv=Gz@AQBNAVgx>qpDZ}$wbXnn8&C~*0F8K!tC zH*aDa)QRVh0Q(k+70H;0LjGXTq@Neo0SSbJgGUcwVp7-FGtjdW*g(yatBB$Uhj}DW z2~dECbojG=z3@(Ge;QZu|Eu~G!hb$Dqb?4I?=>+4R3uTPt$%(1lQJj zl<$x)p10gj>^?oePAn*7e|$KhJ3z8wwqfNby%1mDPDCh7s4yr3?<<&GyvHx%K&r7` zVyFcF&4Xm4dty7%0OH?o*cJKb4J*6*PH!q%>=+L53W?zLcLd;hU=5L|@MHiJOv%3h zxL~J|0b`WO;XllmTT*2fwU+P$^CWfP|GAYNf;{bGflK=iXKuU(O}8o zgjhf2%GV2*MXQ8w0NBZeh_4K2PhqbZrh=Vere%VSrGmu<=qU|o2ey+noT34(I^>cAKm zwo_~xOa4PUZm&}B)%_~yVBt3E4#I*k{kV$>-jO5pP<%Jgv;MqhPO*){p!@Y3iSi^O ziW(AeB2O)RsmJ@fVg9U#o7?aF5BEot9D8Z}-tRe-I-FE5Eq4*dx=mr>CmT?UYaP~{@x22k>Z(fU3d34ZH(!J~*;=V(uK*b-HL5&Mw6Zcy^- zol2Is1oIuZjPhrQfUnG7vJ3+OA&CF8B>F2v0GR@ET2YO48PHX+aZ9Zu!@?wrW#5?=~8 z-jSg2ahMOGc7*?LOF9pGUFWSCg)(&{-8Y}C)S>Z{1ZhAiq_q0mHRWgG+bI!Lk5v8 zL=9o3Nr!V&Wf{#1j{VUxp$=GurtpJ?n^CWtiI2~y;q~&HUe}LJYc}xp z6J+0&2s>9yX;o;r2tVG$j%p!anyj%=&%Arrg$k>4n)7}pn`lM7i=%#Ffmf>ocI$c@ zqg`cbe@ZLS3ptcV@|E;4gkRMG*)f zPlOH?Y!G&Q*N1a@j--fdO}N0&+UYB<{YiRKE3pg?kR`RA?8+fWzshF_O0)Y2sBwkiWqB65I;K#<4P~6l3et}srxk6DKM}vzmNBWNJS7w_f2R-A< zCmS~et!PuQh(0WxJa=m+)(>xy_{^z6f`@Fc2}}MqhmR*ku2Z^v?m`S)c)Lw+Yz`)!tItn)sp4M@;P<)< zWp%(PXWByeHv=Y?^go9K0UjA7m#?*ZViM0d?b-gR7~r09Y96{@chm#!7o#suR4(UN6?r}76~=Yp7HXIxn7IW<66 z@(8=ZlD>!q0AA502Hg#4Vj#Mp-|L$1gisU2+tuu4b+fCLqZu{(nr;#kMq(Z^UPfm91HL-kqxodU7b>s zNKEh#obZ)~-)x2UT2qgjW_S};sZ!Yc5t^Da!^pPfMy_bvd}34csgC3iTqbU(!rzQk z5Z4rW*$DTXVw~gdfJwUV{UI6p4$4ekT#cX*#r6wI$$qtA#%kY!RN-2yIyTY(=jlfF ziJ=x_BIqR)<`+?Rq@$IDFK?K|pnTRG%5B@H)+h0@ZY0k~&$QZYbZikZr^cY__hwkeRYOQfFoh6<_sqx=Z$n z4*lc~#>&sHNuypYZpsy5{a8A(YHarve&>a&JrRhrbO$Z~{^<@32}k(Pz!k!I@LA-) z+<^ng)e`9NGfn)eCLOiF{>u@Ng3M8>U)0|f=aVk$JbM;PzH99ZDT_XV0mS?u;hV$Z z6uRs-$hcSZJv-?kmHZx`I5zS1OpC^eb+jcT21$2{4X3=^iwdbghLF;t)hbMo2ON54 zNcy?!;hk+vvoPEg;sp^)r{rO&6N@+Wb zh;klc2_>p`Dvz}_$3pMa@hLMie!Fj1GZ-Qs*4H-fXQHX{Xe(WmiN{t2iQ%hQbPo#x z*}b@Tgx2w)3QMwsSZWb(m__L3-~lCY_EM>mieW;IJ{7M*{fsx!jt2-`IT!38$DB8? zL^4T^_u6BfH@{v`H08~E$T6=CGrv}eV6NJvb!uLqG;+?|*4q$V5uB!dMIPjS&eMLv zqwC-n1+s&N+LHj_>Vl%7_h)Y*kq8LIcIJdHlX>Lz!oVH|I! zv>Yy#m$D4=)E#M0hmp`NA?--w&G~A0Ya|Fwkgexh~23t_}~8;4gTg@OE6w2KBp7 zwM87_cfj=*gj?mZea%F9J_wDGMjrc-NiglKw;7UuC)&TPfuNbNa0jEpf5N5GL~^u3 zj*5lMVc>^s<^;evDHh@7dq2_`Q2T^8CGGWCfKoJWoTS{Dw(STAjpG$NlwKNo-ZG1Y^*cbQhUZh%)PBOjc+7tgQ~VPxk9^DVyih znoGLOn%E6Kwx|#AYS>!*W?ySlj$dl#a$u<88o{_OvTN#x3F<4$x5iGDK&P1 zN_`*w?R9j0sjX>J8oIeU;Q~p-X$z3+B{Q2<56YfNpOrwH;WshV^Tdjt z8z$m)^FpXeBu^z0PTJ9yeRYEppR3SpGGc1^_cURqic~1R zOKsN2lP&f2jjf!lJYL+|OrY5mqo=;6c^khl|CNcg7sqA0M5mLjto4gLY0CnZE0dTU zVyn@)iajs*B+{5ED1A zw6OE8e2{g8G`TaDto%};Gczx*thm>1*4?~Y@X`jp@p6R1qu+9)@u_+oA&`SM^JLoK zBU{y}>~omPg;{a^6>kif_B5yaPe+$$6U0|7F==9gF7qd{<2kJ6`ccQ%+81r4m8ks4 zd<1lw7-Fu5C4E9iDohwM{5b(L>Rk)GLvh~V(&3-BK~YY?q7Zzh6&eBp>tD9PRNo$G z^cG}qW^MAzMC2(COLYriEj@n#Hx3yNd(^`uFmOG6ve7D=QIBR}Q&W;tLXWl86_vHW z&cDwI4kiTf(P`cjnkYSXomBzQVQSJHj=4sfzT01hP&O3MlV@>56Y6p1L&B_K(C{=G z_{C4Ei($zm-+OZB%fx8PAS(bz2!o;%iwEk=LS5jcjWe&yM>i)c z1onRnFig@4jg$$>s<2Gd5hKN86d4*rSYc5+9@2kRym~GH_4Z?A5erMJX;@m~tP9#s zKetf<%F3Diab4Bxs~1!bis=<67st7#S)u;9S*#fcm2(Ic&3CpeQen65?RDhy88cT$ z5X9l1316)p>&)sLWw2h~r0!~Wyy57sA=R95N4ZwrIN>(y^M7U6F!x-u<45m-5#;CQ za7xrnmkriK_TIJ5@xdQkk?~?p23`f6tQevWi3R5PY-Q%=;kOET-ikeq9!}Zka|#u- zNg-U6!)n_I#&JNXaESM>-R--X1~KmHVj#Q-oxQk=y5kM8nGw8Wq|;v}aRw#SqB}?; zBZjo-jo!JVDd>`1fQ|Z}oSg|>v?2kQmG8lppYSiv8rkbR|70qola-~&U;6+C@P7E) zg~&7}G|!5!-j$0%tEO~x#hZB;(+HM*Z&TS&kPZ;Twlx`VZC#ILLvPz&C=Cn<|ys~Rv zqvvdIkz@>3ubm)dT@6_Lt|wpU%vU7l1hsz&|As#@xcY*>pc?g#9WvK1G0M2ncb zy?rmpuk%FaSc6N4t3|qreKfqGMjXk@hY@TpICY9rEifhFb(HG6_SV`kSwPn}AY1#; z`x`WVTbxk#mqeVhw}aiv&gKmVZ0y;ERObP3{3)RxcP42=zF@S+0zqCQ3gQ)x?@hEyZNeLS3I{B<#gKaKA|~j`3XKSA6u6 z%eaH(9EW>3@k(ua;=RJ5XHPwP*vmu_OX2VRX4obNT0~9vK05~U-@~%wAeHC{)fQdv zTCWQBzUNR9g}4S#KjXgWwE;`bpK>+D$-{dJKBEJct(Skv)z4S{nfHPLEE;jE+Qo#y;2I$t>Y6I89NTNrXmZOYHxcB?I_(VZIkk2kb|U~UX4T_ z!+oSzoH-i^1v%I|HmSD?ymfIv%~6)l$RPjV3x%UzH={QED&2BR9{8;YzCZ!MG@bea zTm|gW3kydGiR0VWqf2sC7;MfUR(PGdEw{CTWNIQ)wTHAR>-^lnUE<9TZ>K}|6I2gy0PRSv>nC%8p_78dL&W{ozV%pwTc)xa&mH1%k>(0rMgFG5yN6-PcVB3L z^4J$F(?hx!$QefadDG`plZr~CkvaEZBl_op2_)xn*MLndD>ekgtG^b^7HnHVK>N2q z5a^daJ~;+37?@cf#{aUqfsd@Z#SF@5BDQ1<`J&PrKr^#qRf4yvs?8Xo5oi;6c{Dle zLfW^G>)qGzd_HuzW-=I3)Ip&RQ~_?|huR+2}25oF>s#k-C2gX>(GXM7P2;SXG=x>x?wR*$Jt7 zJv39ulI|dp>qBXTFa1`A{z97N)v$~2G^D4$S0J}MV_5Gj53pCN6Ch3b)q@eVFht9t zX}#;x&tMOGIN@+#i0n@u!E8tfE4aFJ1intX%xU;i0Ja;>jx%Vz=3EXtoNhrB%Dp!4=1#0~;Bn|Sp zFYt$IvY5%NP2 z@U|tv($J4{P?nRa(Kg28)AI6et$GWc@fAu&#DW0g26>*uzCbaw8jTR?JHvN-?@o8~ z!Vs+KdrYs64%0qz@Nk*wyjJxw@*f9s%jH4XbS>V9GOTo@MJ`JSD&97g`v+;4`)*2B zb#MSWxlUuCFt2Ti?QhxX&?gLkXaHKO&MgYZ6loR@u+H&9NAb zwFdT>wW_nfJYL>4HL0(+Yuk$LN;w3rLG5dd3TZ8(H(dM8YWVvK;rGkr32A6TF@DPz zN7M)Mm>2;*539rE>Ewil+`u90rfY=uj^X(#Y@b`vioSE=9;Zx`PO#P+&>BfhTq`kh z5xe6(7CI-2tzBs~Mn>1P_PNV+$d<19!iM@frnE1BJNr{FebS_vcb>l;fydziJ3pVb z*W=MAB_6E`_iwiDjb9{~zUBvs6W3=@d=nk3t{^U~&oJbu4kxghHO-`bRxW_^ouvJZ zN4tCDj~~}N2M^2l4JY6}v&higl2sD%X_;c_yDF!t$54WF!&QD%XFA$UFwg$f=0gRf zMiDSqHNad&2X}0ZYz!6bZEPJF4Q=d!{~9*@Z=)8xbRMzqq`R50dRJgNga_TzYJtK+ z3%)~{MAWK8?e90O>DbLAa+ccL_OcukLZX^>7FT$`$67kZJDwuvO@5vNaL84jb8rbM&~ zyV}j*4W@gi@=k|)SM#n0L{lcNChJA6q2TR@1dHXZVk&v3G{eD1et<<7(jXECw7U@F znLS$WCZxPoJfWk!OF1^I`-IBa4k0tm zB(DQ8Em_{sf%;2Q90|R5bYRvWSl!AGGAH|A=#fH_1fW!n54qo*s+TLqL>r;U0wr*O z=@Ukh2tRiF9`;ZCkYTt`9d;^38-cKw@v8LXqBi2CjqqyMKz_jCF3~B3Sfz zz!4qV@1mz~Yx`fx1B=|hw)8kb3oHOu|B2s<{yx!Wld@Ng-NUz!KQ4xc39GpYhDhNI z->U*bHOUYK>@wh|qCOj+tbO6dQtwyp%2P$l1*q3b*{qNg=dv)#`5=1)R09Y&lj;EV z9uUIDTDq#bl6CmZJ5dEuj;V+!2dO7Wf?TacfpmCcD9Vt7O6;g2js`r=kA$hFy61Cc z%bXXV;upu6X*eAo62DcgifLsuuXYCeQ#8_G$j`vE8wLqbghYOlROPHrw| zYsq>QGi}9|nrU^7(z!#cVWNik3Jxy%F_o{

HXUm^L1s5wZEw1rmHmU0;iu>tsa? z9gmw^UYHw>+Bs>zAwF9%aI99d>vSy^<*W=)`ic=eWIUei|nGg_sE{#Jl82_Xwd;_xR!fvys&&Ws+FBaoG>r zab-8}vOi1FwPM~Nn|_0DUv|3zr!aqNgfT@4v;H)?Jp28dF?MtS*;xHoDgLbz z5D>MoT0hOP&kD@rPjf82P)2Ok?}i_Y8amb{B{^9(K|v|`QTXn}IL+av_&Twh%j3rw zWlG2S@*E(ahepYV7n+Ew(2~1KvUAdWPwdm@0vy>T=}x6e6M~VNgOW|%L0_6lz>#{u z1kEe~%LwUL(6CDxZyFB!q*Skkp>UmVwH42hl%)7rfG_k1guGn#*p_ZwlysMxj32M7 zW4`HqoGH22bFA6KYi z=KH@t`!xK+=r`oZ7p{ny@qco{)K`kw&*9C`j7U=(cUe;a^)O89i} z^EXnk`*<=2`jq&egJ-{yLbM_NBK}{-(4OKvoksc_=@jXI9^#MLq^BrP81FpQKOgVNVgBR$Bf>z$5#G@W0hv zo&r5ht^WqH1!vm-;^_Y`(S8c}H1+%&@B_s!z$b|(v~D zJf;3;{QNg91cVJW1jHZF^i%SG#z4Q5@6-N9{;!DWDfK`7#qZR2uYaR9{2%Y3APob4 Tt?1`qH7dk0SRFl?ejfclEob5Y literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260321-5.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260321-5.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..eecad6f688f7e62e84ef0592608b0e72d6cba598 GIT binary patch literal 13017 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 F{{b55Esp>I literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323-1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..d2c41c5a1a9fb3d31d5439dc4eca3b0c5120eac5 GIT binary patch literal 13112 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 T!+!PyBS9R3>Bvm?^XY#8YHe09 literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/20260323.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7c56216a4ffa5567e39c53a4d9e16a1a8096dd0a GIT binary patch literal 13352 zcmeHuWmp~Avi8Q^Ew~e0gKKaP?hYHb;O-6~!QI{69YP3BAh^3b1cH7$nK^ednYqvN z{lBMw^ln-2yH|Ius#;a6T0sT^5)%LofCT^mqyQ-d)u(tc0KgC$0Kfpif@_P|**XDj zo%B`Q?SYQEjBYm8B)O2_)Y$-VQ2YOG|A*f|dBUJ=FEeV$X~G?HOcfT(m4Yf(PAKC5 zHrySck{>nFEyiqar4s`}Rka4Thf>jK{P~e>Q}%A0ZWa?k(mb$AwCX4VydC+|odsp* zr_&4xMG_i26%0NVBP8lfm?qB0a~^PPg5w%=c15pPbY~=aLtHX9y_BFW%yZ+^q2eo9 zxz{VMZ751GZ`MjgDnedd=^N(mAYs|j`E{ z?Ktt&>HWI938He?XJwk6?{ahR{5V=MG-1&-ZwbNg$L^S7Bo_L0LtRYqn0ITh322%l z&+$|E?NfYF1BpT7@c9`6pzt?k#?b2of`gQK6{M>OAZ6Bf09rdTG5*;9SBd`*Ywj

k2m74opKt9JkqONGu>0J zO>6Vq@=!fp&=bYr0@6*V+SGbw9-EtC(Bd4#-yKQz`jc6g9+q9Ry`$aEnY$KKz`>q;7>lu{>Qn=oV*Phs>`|Cl5!uOmdRz-{~Yb`DOt zDvMzcyTJl-x(|h-V;?kcKglOnsaaeQc%n?q5|#W$V3Z z90W*RK<*F))WXf0$<@xm%E->n>WA;kR~@%ot zZW)neQ}U3J#o&qcBS&P0AnT?X@mhz1wIM;cyudc-cA0B^HkF^9dJ5*3Zd9J_TPp~V zyNG#{)j4p(?{L5JYP=qSOnOMuubctHBB-A)=ZIeDeKc7|V7-!2;#w3NdbW&Gt|u?C zPc{GghS$gMEzp2+LjL8ipY#Eh0PcB;&VwSRNyn;3VR&@-Awj#k7d- z+svX6_=ZT1O13SP!30sB65-x;+@jN0c+(cutWgpVGx%}fOd318dY^`lQhY2bWidn;4dvi+gErNSD6(WNa%Y@J1n0I{=Q77w)!i--fLvxNtOOmC`UND;US!UE~+|KM9n=v=9o_N}HV7TxtTig(I z;PZHQOdZTe<2QQhj(dUkz>dG?t~BA9^$$S)03oHXy)ZD<9|63>lC!noN&2t`2lM>$)p;*YlpF_VM2iVu;VrEh=m+Ej5#z+ZR({e#LC{4K!o2bLQ)vA!k zH-U4`af31selvF#zV=16u6MvKQo*|xyry+lLl>#e&bRIR&cdXrpzbo?{l(7Zsh;mO z0>RG2Sfzf4``yLPY}T`n^Y!lO>UgKy)6NvJ>9{~z$HU!qB=PgzriaUQ{aZf-p^SOw zfNU49euX0{H3q+a3FGvB7-41I-_jMV z2_mXeUp74tpQE0PMp^F>5Zdj{`BwYZcjcU}tnFP%z~L3dg~E)Nd?3kK5PKfKLCj3U_H$u6)Bs~)ha%1tFFe33=@Q1i2Ld2fXfX#@&*LPbq`%}1UjS# zhV>~gl-=m%CX`BSwor>}UE+Os<_6NE!@nk&QgTYh&z%10Fe^-WDAsWhhB&n+U0-=MPs+=;(j;=)PsA-My- z?0qGryX_JC27A%|U@GT6X4R8-hqz5pCPP%p&hHiCL=yU0g4mZP)^R&GrA_s8-AVX< zh^A}tf`0hw)O2fbV0slK>3I*N zqtP4Ur~{`l7s!d@n%&)Jlm-pPi3ZRj@p2qy?^y!bL#P^YP0_cLjpIf1NR=uTk?+N& z;+rHJ+G@(mI*3g3Mql_mRvnxOZohXy(HA@++awK(`*18#sA`ogUVSsqv$Xnd#I7r) z^B_e$*5kebr^cI^H7nqw7yr{k-FkTc%^(M?5(Mi}Tuh7HRA08KNKfBRaS!B+B-O=p*5TEN4CC0M* z`Q!N+JUX#~Lz5w9;hT9UIpbZF^fvy;jzw6X7>Tmu@1KkjpHt6#5s>DlJX^g!XJ(mA zX0%hko30$_5&0}rCOQ4^3a6rsPrXiZ=I*0O1|Mz{9T7p__2-8g(q~U7iO6RQOy4mZ z!IaH)o}h)zI@S#XHr7>*zyxRD%s^jx{c30r&FidpU)#KKME$d%C&Gbv4Og9|lxQeNxz2Hq^{kvoj}@3FuM@k}3>*FaPdsyO1Jn(gSGvve zP2_cUne($puMm3MY~TYerv1#IUTFx*1$+#3gojqKx~%lVeyw+FbtLR#1S zrdP3edey34xK11we{76w+K!?m|_D=g><-d29(WD)lYzzeoU0Jl;|=E zbCj`#v?-Es1Q*d-rCT=P`4!TIO5jjgW-UsT`iUm{#j%><`DN1S=u&SIMhYP1{k-V0 zS#}C#Mbb6nEyCDsQ$pP8=%-K})qttJ89bP3db$r6VP$k}#jg`o6ERQ%90CqS*5S^N zTV+L#nhJJk={~y*Sbi9k&j(Z@yGwX0$+raIIKNl9R%vvpBT8l05|d^Q&roIOQGMq) zKZo&Bt3s-^gY+1?0&AgCeqJ>-tpQo8OJ#wf;vP#%|G_~TDAeE8&gwSdn4nlly0WNr zc|P8%sd!Q8TSolx1j881PSWFgW^=CZ+qwlEnpQC~8%h>U zd;8U7`&AJq2}6FDPumGSf#Rm$AQPA4Dru^r_DseF9xB<NIdUGpj||I0>G03D52;eF%NGa@B5Rq-%d%)C>kL6m16)jC3;iul;3uVgsK|-8Udeqz&I%lYKn%T`TwDP*hUgCq|1tI)~JaB*Y;C;`wr9%@gLp5{ZBXOf9VF$%h0g~ zs8g;X?#}?Il?AneQU?0~RAI^M&M{yF)J6qBK`}zfz^lPhC!QzFZ)XB6`(M4AizQj4 zc-?7XLJQ|0KQ;1d!`y@s&fyI?!n&p^5>qO6F(L_a#~&Bpyv9!bH2qnLoK98+Vc)3wE+arg+yH^5rmo4397CJ>)ts<& zAu9;V&Q{Wctu(4*Ssg4K$dLbWIyVtd(=b;{QeAEuI|9bmnv}AU61GtR6?CAC8_bZO zn)1~E^k0nZbHX_w)S>xpapdD|b!<8Zre4*D9_u=dGEV9&b07@K)wKZ*ozi zy~sz;=3`DFHN2WcEMm_Ulk*GL#n5%z-vQrl0$t&h|)C#txqowG(I0 zSL3K7#^ewUHwHrceRD$wWKArYgk{nK@S`rH_Yld);S3ddixjQu8~=Af(i(`Tq+MdxsWs@ zCyO`riID}kPd$ZmUx~&SG;fYqpE23`JbZzwCpAttK-p5R@#pcO2R15}gaJZgxCU6$ zd)rtx;bPA|=ShG5g0?4T4y@3!Q9~D1T9r`*x2%d9py*y%egEwCtuL_a#0&2EX~!s9 zAUIr!=BtpqL4Zm)C?J*_A@yllNL{R^Q4FTEEuD)Z54HOD(LF9wb{782o5XJ}PI>w@0b{4Mw?z;x z(>fucp;NCM-i#Gdo!|+FR<^WJH-Il?nF?ipEU3jd9v&&fEI=3I>Yr3Tor5$UIIu-G z`tF$qBM*A?{qkE^s zIZ4LBj|Gk>ET&|h)|2lPW4UdpEUb+Lu`nbRw{(G%P^)I&kQK*KVd5(hZ;%2i?6c${ zXZ;1_;)Wm%@ti9z3j>$jZC=96i^@ zQj>_ya1UK@m4^AYBl>NrN6j;Q@oUs590KsoEm@)DI`X1db?my?)%|Lt1w&Q{JE?HD zq7@{xM9G_B5Gcku9}byiJ_R3qzfb1LcVRMAbTvwi{J~r_+^AqtUK~7g;sK2)`i$1EevwK zj)CAf!aB7Q)VFtKC;J6j?qejQwWeC-xuu0^va5PAoaMBU#Xwd0dymecv3}bB|FxgpN$N$6nA| zY+x8M%0%TrI6ke+Q(J9x2-gUpHsh*S=W5D&FZgU7KaAC{?if`7MWI2o;7EfbGBCKw zJ*Kd-gD|otGb9#5A=^JPhy$KSp&rV5a9g?P$SRs_LycJHMofl_ZkHITHc|%_Ya&e@ zLnk-fSnGD{@Tply8D0UQD$=Oz%%isnzQ#0Hz3JA30-JBQ#2P|3w1_7BoMXwK&0axA z5qt-2H`=wy#lW>;TqxH>vXP0-Q``2_Q>4{CnA%YP82_)8=hVJt%Ovo0nHxloL&20iXF^0By$T_t(I~zT-`2;wt}40Fx#!Bb z=k`n4Z7R0MqWq4F#Rkr9QC>C?sN?kZKu@BkjczK$_BfYNeSoofD#-x=&3wif-~LBEzPhSBMV1rNH34LqC=y7gsT z2k>|g!w*I$Gi}$loalIv`b>2+*fBoz)RU&F!Hz4_`H=2!4h-y2f{0u4V2TTE zNqP*O9DOkxD5{$z*hMBqi>gI}i}?c%V~n~7x4BZ|(sg3VgLvW|Fj>F5!2Q)o1vh2-lO#RR#tr$QGxPnM1pupEe#F*^Z@K} z*J{`E?u~v9xsn>xjFVI`#mHCK18I(zex^z)!S8L4B&SiQD8GatfeaOtk2%gH>gKr95)2Ge>YM5?MVpY5?h`HGz{B8!9W6pfz^c*DnT6U#}Xb#1UH0sOB5F^4@|AGYNkK!zOg@$(VQfRID8uC7dUc5}F$p z#u)R>H2&IVLGuw?4p&qg(|o|)XZ#^K^qKA8Dnd0jF-Al#fE^Vt$v?=<{^%7oRE4+Ntx^oBmP6-SQF80e>i+f zWp9&5)-~;-*E1>B&Fu0hMAPMGe6xqM+alg$E4!v+UFW5|QsT>d&!)yt_{Vyi96|$~ z>lMm&r(=iZu5aj0cA)L_yZBB$cIABaZ(8$eNhl_W!Q$R9V>5Li#iG92ZHOeZGBurwj(?e4;D84U5ni!i1jZbW_E5tN z@R2h9H1ZH?Vk^M6C4yd8Ly9!`u{z}TEM&a7_w+5&3mK}K4{+izY(nRw*Xkfy{nL}o zMl9J7gEGI5ps)k~4{4e>7`pxtB$JDcwdfzmER3MzDCNcIG-g!qs;|D)OQG!_Am>b{ zc>KN&JJP9om#NXgq^-R>CrPe0>FP9=tqWIC71N2@9Z6xsF*7#C<*&pM8GDm~;jMWn z{%!bEJmHnXdTo$6;=66T)e*^C9Q4wmc&(uEvhiDD0T23yZK(c1p{oY#h6FP%Qv4ty zmR&8}^pO=?74k)V1Z8yK7)EC%^$aJ07zd;@UsEb>!sP%<;UU@-*LFGNo{F8G)7aFO zKy=>zDDK;O%z0x;Y%9tcZaXskBi-(WGu&(XXfi_>iP6mNW}@*#4gA1XgrOHw=b}dw zQo#B?8da_x0`G40^eIY@4DM6&>z7$&kc{&>W|J@9wyx82b+$<{g=y4J;J;c6TDmcK zQ|u~GBJKjSnW&~tJ!w-bJt-I2=8lttRaUX;#sO~?lfQr91O9b^z!GzC`Dm>~FYyx% ze}oBV^2*UWb~o$>Md>!EvZw}1jXeij9jGjzJD-!iL&W19s-Qh~1P3_*m)!kepNgwx z(;+)Yb}`jOkbO!0%YqX$t2@^Pix*ylB}4F;5FEQz4>*9y566aAeSI2*BSH~gr`Rji z@iPSJXJT5I^Qq++jM^w03rzU^IES8h38pdLtAdKpKJpn4&|Kp%>Z zaW&1w%YO#CqYD~aA!)E(Q}5)!h}Z%RRW z4FWDJqCfLA53g$gXpGpR)Jw6-4&BBS8K;{LH5)M=%5UAa{8%{zn!x%8d=yL#ScsO31Nmj zztSsZR6!NBCuJ-Uli37XSd^;aDW|HnqDQ<#oygCp$=wjvxd-3qyM-0-qr<8E2xFi3 z{4s)>R%{8@tT)9mh0TnLxUXi-5Qh}Jp2jnbIgr&G#m8DAsx&N|-HW|mzXxWE^wZMX^3I)pWJz_A(Zt;fd?cw4D7?4yel{z4Yp}>%!3nM2S21rFFz8_mj0vpx8tL+Y}dPVC&swil@`4s`C9S5r7|#7r_z5*s-}yRrHA`01`@+<2#94I zMla~=BTjt5?a%3z?^2FzcB8AY`(Cj76j8(fdsrEkk`zF<;$=JRK9L* zMED&Hl3u!IME}?ee}$b38(J}a7oKs-G?@fjgU{L{iHYlFCT`*n{3pT}1hMt2?WRa* zTDE= zGJh=yl^|@)ptuqntF0m|Zp<*|tc}96nKRF%1*;Uoz9H`9^Xl|${{H=T_wZ@uvFSAJ zO_%7<{IX3F;aP=Zxqgky^m7DWy78JIiYpy$CP-%glqyi_ghvb{RZWmo(Ljr;CU(XO z4tDmAOvZK&z&}d@|GSn7dUam0Z+@($_MZl<=4`D{+02vW90v?#>L`>qyCDo7+15yj zpdX)rYTJw}%H2lVIlJ81DD<;TFuVe4oXg-bB_N@)-RLZxsV8I_;OrC(2$ciaD+0hX zdUBoJy%QYe+^yEW)Z;~!^+0Fl({Hy5N$kGrpSvKJG$lV(&|7K}XWh-(@nhdSJpyo% zg33DxFh*FGAVl~vz64=UXi@!j@+dFB&Y`?z95JX4P1|J;I-KmCcHi?f@~;Q0TP)-A2F!|9FmPx;(S$ zt{>RU|0Z*|br-Ql;zd|DOpfu9-8OqiTNOvKtYj^`6n3mC*2`Ce)TDNgbNY|o)ioqV z>(3bcj7V|y5pa~aa%u=0oY^Di{btl%|2p<&ZS$I!Kx2;=G^IoRb?h11+y8gyfnM34 zTYB7UD@+#50npGh{M?b2LMxUi8&NNf5`oZm%s<*bk7OE0Dix|*rBu=?hT=heMW(d( zS_f5S;@hHAQ>`7req@ililK?vOoaby+nK_w0pQD-Aw*Ju1xbG?iCiWRQQ?)ar~RvIqHr^Kt8 zHF52X*0r9nK#FD>^f$9moyI}8=1V+H_PZxrBgR-&ye!k}we8hO%{L13|@cZ4<&O5^M zRU;3FmNvorZ`-qjTSvQ!=R4EemzzeXVl~fC57$MJQx+`q6-b=6>Z8MsPh#IBi;G;? z4)>jR)-2B=Y1|K7w`U3h%L7))1y>vhBs1oShl4%=ub1na;yO1j_5Sfs(f)`oXTT1sLp$R9EXEmB2D;1O-P$M1~CY+@5cHJn*k@MSTNhDd2qg4KCD3hM!Y7(ak(~i9ld+7|;}eB{(gkED`xEt6Rr0)M^lKaT$8n*jxxzZ3kuGWl16QIMVgrB3;G;P3UBzd%7HJik?I{to_o zao8_VKsWqP@c&XO_B+q-6+?e9T_XNZC;m}C^gGM%tDt|e07023(4+jmCi*+U?|JjT z2%PYK68x1#{~h{!+Uqaq8POlm-;-d!GyI-v`HP{Ga{Z|6TF(cejb4{ZJ-6{Pw}`nNFWcl6&U(0^e804!<% z;2+cJ-{F6s1N{n*q5TE^=Zxrg^xyr(ujo96U(m+?<2@8)pg>>${HT9M0i1x;k%IZh G+5ZD-#<*$# literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template2.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9fb8bdbdc766d6ce4fe9a8da0951281e26556bf6 GIT binary patch literal 10885 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 literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template3.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template3.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..cf463a3a46dfbd341c4e164f99c22f272f450b40 GIT binary patch literal 10705 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 MXXp5n`nO;I4+xM+rT_o{ literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template4.xlsx b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template4.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1afec460db49162cd2edb374ea2b867b0104ab5e GIT binary patch literal 10841 zcmeHNWmg=DwryO3YeR5otb+uH;0}!i3-0d0JxH+N?gZCBa0yP35ZoPt1$Xz?bLYM{ zGnsk6;NJSsvby%^s#<#=SAxL7;{gx>$N&I<5>SM;=V1W@0H7lP0Js2TSZy(TJ7-fn zXMI%<2U90qW_Mc~id=YD`fLC!^!)!G|G`h-Q^J5<7b}*;RpKpXY=fH7c5W#nY`9<& zlkyHyd_vsxReora<*HR-)Us1n>(+6KFtULyUvaCxjLM>U%Z|r_vg93 z{el*Agg6IT;8C{Ob73wbef>zYbbv;_T?nUjSQp$Dy>=xmn7pGPvsVgCztqc%DazbU za(!Un&{yCFA0@lXG3$N~AE`mim^HI=GA#oi_{n{d!1YsF7*<45K%7dO<1S{wB6jIy zs&(ZQjC~w>uNTVF@ROhJiZ-$QBdu+qcQ=jK=f1B=33lG@q)S4S^q6ha zgo7vplxAULIMwvP;M#>d!H$uhZ)a>Rm9d8p)yLT632ofR-U2J4NxZP)^S};Bv_Lo+ zbpus^XHiPr+qZD-b(lBOTV}*Q9SPaUMT&;xu@%i|GRRbx*C;Y8hIf>O=a~Yo+o$Ei znctHjH1;FC_NJac_@4%QSD@T_dV&Ke{Y9X-dTpk#P=T&M_4f=aP<=;J8z&a#-(LSK z%>Q6D{^`~~#mPh9USb6w1w2s>w!T_mq#Czjk=T|ud4PGD`e4AF8k7I!;Yw1(Ij5e? zGvl*cmPhJ^S#_R!9+sCYPLc#lK!({wvw9cgp|KGOJKj+;a$ma3pUS3muk3<7l5sO< z_CoN(r?uSo%%CSfpo^4xPk=y^jwO#qq9ai2To$twq?5Qw8oWZRG{n&}ERFoi2`*Rg zy5**Yi(CG)<$$Mse*rZ(rciu1M)NvmPIUK-Ggm#QfTpw3_xn!h_d6=blAxe8xXhn7`#cD>E%_2oxbGHICB3&;Ti7s*H0)UVbFW@j2^bH*8R5nmUar3V zN69!z@Hk1q0RWpw001WR40jtAH+x5GBYS)6-{M)mnzr2n58ea0!6n79er$2FEv4-1 z0c={~aYXsFdmyq!SX8J|esI1Kl7x^sLQU{o_kPh`3Z&n4*OT=)KVWF4AnI_P{8i;qv{t3a<;;iMfgRb>qM!st3;=Rw z&U^~j1;6feE#<9vy{exngPItc+*DqXwuDT9wXFQt){ZOO&y;f6FqCS22H!U05}j!W zDssBeC_XqEb@=GksN0XFodO$!i;F5;Aak{VDuayFWHLNn`BXiA)RJ!jVzbsHudET; z;U8y1XlH#<UCB=O;`Ynr<_1R8?D$(%QpnsH4~uqbGz&cvH43lOPc~7N1m~tEk?oWM8ut{72A8F z5hPP}6H#v~5#wKDQ6JezZCP&u?soiKwAp>ySDTjLGu%he)dGNN5JSI!^cdeHI{{Zc z2t~tL*yHj`2@w@xCS+VKB8!Bd!}=qYv)vNAT+=louq^ zFtQ`%vLbEjWM>HREXt~w^UnD3&6YB}AKyutITmL;iI^+Ti+jYBP^KB)8&6ZvC)zQ1 z*(SiPhtGi$4ubi`u>BdQB-aU*KqP{p$_3Y}5Hirs9aTo71YSn-82yLE6Z+6a)~j_H zGP(&?%yv;$Mj40 zHpfMNeRV@wLd$L#7j(=#(^pa3YNrR$?QIVabR-9t9EU1srP2r7lM+F9IClDA6qa(v z>^*LUN^-eDWqwE^FfSr2db{z-Q*meE8}fJ6-19Trxa>#9 zRxqudMk002?yVGdJ7?p8<9xH`>t{b55+r}^qCFR2EBa{0l`ypMRCj`CC$12^sIy4g z^zzBU#mi-5dEjgzMOR}Q!UAU>>~S>I=X%A7u?B~G(Frd8fCKM|YjgAp!G7p$2oBlkpqwl~3IP{q ze|N9!;2B1m6K<1W+_x85csxI^=|Myxd4*Ks=S}0m99F}uE^Jm#qom*EHJ1l!H~enj ztL&Et)HXCwq%oLnJ~fFgTq1r9@E{*MBSMkkNB01XZBJ5yuRA3!kxyIh&E!3XtvmB> z(KboSAY`SS0^Y&S6d~WF$lo!8$%Ck$$yETCHmHS$ zw@!Jgx&EW(n$0MTg1?W-)YEd6Ob0tWUDJdfvWHpk$gJ_mtnc-vz(~X!b#(@opM?U% z>4;#QHu>%8kr#roIERllD(R&f3ET3>!?c{_IPG5GCv1uM$aZ|Ul+_(} zqSy5i?rBMd{q#x?;*plnvexVgpHv>~wDOS3S+ka3H+l8M$bQ+)IA_g>SjoP54+MG5 z&pYxNA7TC}I-qUFt)*8}as@EoANL0_T}DH#JqZvQ*e= zMPsu0J<85$7kG2;UhjYhG?dvby163L$bC(l=gl<{zvE6|!Q6GM4S>yo7?){+<-(dl0T>k1@~q2%H~=5OSIcA zhg0P#ejzSeuWweWpcRUW^}dj}b|NhOQI;u6f|{vIo0MJi^kA-_eNpGN#ZTGkdgOhW z(_r^ZlB$9abHFpT!A^~ABj#RF{B(mC%2f=#>Wr>4Mg)zEx9FYTtt%9Zw?2yZ>Qy7% z_UHr_l}dpU0q(fNMX?St4do=f!miw+-<(oB-gvdauTDGlWTPSJBZTewE4-Yfmkb~~ ziAH<)p7_WM_t@Fpwk(8oc%Nrj7BDH4X;l0=0$yN^9DOBhS}0{@<_@VV^PS1=4WsnK z1Viu^{_BLLr7oQJuaaoeeBk5M=#)g(MBT~6UL4sQqI8i$aN-4F86c#dP;?@W?cjq@_Oqkl4+NB8L3t z!~Hm!8d#$QXH7pP;!HRSVz;}b{T94&Qnk1u?}*D{DqS2P&OyM~xw_urnBfJ31c%#zu_(MOeeO+V?RXX+Qs8z6mcMgdIK1p0!nh(h z1{?n-L2iMwdjIlP(WP$(2jMU${j{-sU`kNbvNcByz0QJ0XMT(Jc zR>}$=21U9dNNc{BV0)nn;s=dI=ePpI#|O$Vw>uV)u)l2I5merE%JT( zx~Qhv*Tuv+1!d(-CRp>37PwNL!zrzwRBEbmqqkGj>g-w^B%&wk>P!~QZ(WoG#L6@x z#&5CmI8Llw-oPfp&_~ISO6ruqWv-SoMP;%heLt=mp+t~GX^mts&`DrfE&pHZ#hnpbZL2>kiNTZ8g%fJB$W)mf4AJi5lm9_x(ig6^Sc9xUj?^d$ZX5|;SIYzDiQNzSh7WAj7Du2}SatV= zG-5KOFU0;tsCb(nMyK}`6mtJ11R{<|h_{Z7e8lrvk;Q-=pWD^uc@l_a^;FzPU}^h> z&WyHPZ}0b}Gp^x5jShBa!LLrRM=HjKiUP9%!0-~JW(DJej?aJqG~r`h?~fi3rT0of zcU$6fPT>F|3`{&$PZG_wJC@!-!szH5RR7goLK(=e$m5F$~%FUC^#)pm~ z%JnYbIAecUiHY1~*wzcBTJ#t|YkqsL?FTM|Q$jwPH)RD9H;M2*5=dU;D`;CoM2Az> zAq--7;?m!1)Ro$Go?-Qil8wEvLvXumg@9q=P|dN;_^Hbc%9vR4Q_Nt#JD}Pep@neJ z6o1x4+{68;i`bAFcPsL(yTVMK1T`@7a+jF0ulg(eeBUSh4nAz5?RYVKir|6(!t!i0 z-_EYvDTEevte2c2^`bNE?Ew`Op_c?hX;fqh;nQA`Q{Z&aJ2(R?f|&!?S4*D#BLD|r zK%sgPqMS5w!n(H%|}ICNxt_dQk0lZi%5 z#AymuaPXx`cfv~9)=InQBVMcLB7k4{jKqN z--NxM;A2;GvfBM;A@9?Hk&k!&F(N8qdfqo14d#k^-WNMZD*~4t-0)_6hk(6_8ED-%^tUL57zhe}4IwUXKp4lDKz8y9d7bqoTqS<*j=emjs87vdK!#n- z8?!jR4yIwv^k<)8>~`xI2@a#HXWJWrYHwcwkM5=qe+<60bxRPN%78C0zEcn-dD%cT zj29&%i8p?9O-4y}aHz67>k=Lu01;H(7L>4+Yo*rE#7g$ERV6!A< zJM67xALm1eg5sYlbuDNZMf7<+wyPxCl)UJZ)v>k)NRZO4kLuIf7iIq<<1U_ENpV^7 zAySxI^$cHJvDVWsKH-rngba8~XD2N|yq~GKZ}jLoEe{h@&1CTJ{1*eUdMwhx9NPxr+lk+f>Co=xGJXE`L7cB3g%=?(_a@a z9DTnuAz$#5A57-wj>6ybXnbcFw%na(lP;`2X!rVv@w^$8^nMF`-lF4s=Hj9CAxBox z%XY$}$m#hJrK!!ENTMc?{1KBO+y~F&Q8WZIwiifM{Qd*ELv9XL9l?kw6@s~pbMg)t zK3+=^u~jcL`fxUNx>_vA(*2W2)@>zKQOlcCxzar`suIPF>UyZ}+)McElWYdhiPBSD zt4HI^9Hx7cDq5C}T)v|5+yFVAS*MVfM}Aiis|FYqE()@l%t{NVoi8N* ze4AH_5kklnYnJ<6W?e_RF3i-($3hRk!YDO$Ao4zZC>W!jJI@b&@|kp1EiC0}gA9zT zvo0i945zfT2CO((6$FEq7!!?$-7MqmhiF6N%GyY7Q+g3r{2uOE{OB#B!c1-55V)b9 zz@^-?(A43Q*UTdz+^R&;oR-LZm3BpzXfQKzIuz-Phbi~>Onv;!6MNZ2E8x?J>evEI z2E_}Bc{oavL(~Z8wt84*)vg@WO%!x5*x^>W13wFYTrGF>-VkcDGyNE!hn1fhn}*}W zh$3}j#x(Rcbd1>VZNxO$#@tYAJgY7=l0F%jP?b_u43^KrRV!-f&vcmT2Eru>7pGe( zC%^>0B71k}6Dty^aYg?$+1o>V=Hcen+O&YtKmM^Q(L(iuuPIpLqJf=`8>oUkTn#g@ z<5+JrWYIF4(R{Y8UOqpv;ehe}oP74F7&-MpFNb?laK(TA!;iB-d@>PL{c$yPDrrJ# zFHXOZ) zrXgt9kr;GmeN`fP*Rv{ITz*{CIPh9BjT_I2bwo*15K5MqAZtAnVGz_BwuJq-S(4%# zV8>OJsENRQYN89fdEN9ITu9Zvi<~Rss^(AjRJUQivi|*S_uM>Y12z};>V8y+|A`qK zW}5>l+Sif?J%scqCPrG5nI~w$|M%#Ks)6x?8rp&jgHD;ye~*q%&K@?VPQMj&pVWu! zCWNuts82A;TALrkNaMb_PI1(|hsRM4?{+c%9ItlOr)B>EZmlYf~IY-a7!bu9h7?*&VURCy)@-t5Sd_iUWL>Ri1BFwFbX83!KmQ!m3ExGHYUxMJc_6t zU)e`avQ{e_CxNLb`yRJ>?&el>I0?FeL5U#`-jJ#wTnud;!xTr=mH(VlfLZjmB){Z! z1=l!@JABkU-XL>oYQm0+)&HF%_4m;wl58y~NC8X4|a&a7ySCQoULOxHI zO;cL9x^U}DJ+CcHnO0g04Pt!2&gFEOLP(H_W&;QnaK*JauWzAAQ9PP!Nm5I!IO5D_ z7cSitEC1zWagLT*rO3mrk+w(_s8%JpME!_m9xPw)i-peISl-E@$EYiOcPQO(@0!c; zvzDVH$gHgHfmp5cNSO!@l+}ws4F;h!*c{lMKSQP~%J{fI@J?m9!%miln+o@*N2~#m zAZcp}2X-f!RIj1xTC5hs3-CN(;3`ByzNxQ$&lT{vbU69QMWxo$P|tf?{ba8}dWvEW zX0Sl|*lW@tVO=oyiq%?s6es8akLyLl9m%=njFkLM@y6l|3M7ehjt!9n9d%WbXdgww zRgUJm@8V*Fh1~GRd4Be0@Y}B&OdvptgcCjE$aHNLZZlIuD}Hf#K$6MVp`J(9&X5a; zR%K+$VVzzT*i zysTluaGPC(<(-hu=9ADjc|Je=WP~!NNR|nfkQPGRePclGl^%xL4Vk^njRNOB#1XCW z->x!jHf|JnXmQQi$Rrd9ZaR%fhDUN=5MPrC9J`$8tQ;*f?0Ivr%P9?SM3s?>i7~{$ zF9^XKidm*QU_Mcf>qE8jcQvWLhp1db8)h5OAuZm&)nsAlXlkP3>}Y9c{=29W6zwNi zfxSo4-XY)I%o=Kq#VlRv%Q#9pXoBGvbEmbbITb>6yX|x|-V|0@CHBhBg<`=)%Ho%e zvbh}FT1h<+0K5F>=KK?leWO>nqgRBu2(y}9)`>8R*JEM74N@2wD4&#KXoCGhzy;5p zIqHd z^Trb0_~*1CL5}A3JO+$pk2MzUQ~Cg0o=8R!x{jPQ2FyTylqp;Im8UlyPC1kBIW1Yn zv4hzAbXt=nly{`k7(FXj8rU&iCRF0h=&4pXKE!p?7L^GEQFOi(@#g zs1m@F=oMNda#g0h0A73(H3t<0Ui=L;*0KO?#*~=|%18@6_B8It+g2XZ-pq-FGFij> zym-Av@o>fAo7E)s<7Hu2Tkqx7yeReKM_>NMJy}~|=LozwAC0be;FxtOW^tI}iiy-Gn-sJAjY)HN=swM%Jkr_hNnvgmS z_S$sV*YE{S&6!|YSa)uo66w>W?4XrnJfXd*fLfnv(Ye76cT4U_bq$+Ku-8@mTzhU% zPjio@orC$?(&KUGRiga5t&anMO|b0){68_}M+~2=Jd`OqP_K*)ZNr+_8!I{5J26dzk;NcofxKqUs3*`P}HbWj~B@E zCAuCMDL)}DKi+qI!?0XIu79icRn)Bk$L~}Xl@`t6P3pb+fUCCf2fDgLUr>YHfnqCI zc)9N8jBmE^+HEQCb^{L6ToX|&;6_gOnu^jUrv^dJdgB9OVS<+udUF6Xg@M{{6eRBK4@7jE8dY0m?zFKXcH~!QsCY zgmUlSM@HO;Echi}?^VDNtBF0kS}deryj!SI$u4KgXE4Ju(2|X&FURHG$By#!W-M8WtKwn@Gv7ewCxFGwkwt4Ma!M(;w=f zT#=g~#c+a87HfOWjIO!IQ+(V|*fhhb&ri-G3!7y0`9=8{>Q2(>&gH( z2@z-N(KnqxL<$5S>E~c!$ybvY4+nhYdPlUCoA#6r>r`QL#yX(WHcJ zl@X!F2;7XR+^ws(w9=MDJKbD8yxFZe$sJtKG4|4~QbZx6u!1E^ zo-Xtn3J7d$tA*6;U~l?{-Ew@l&n~$K9u-)YWOvYCv3CkRm3u=|)xQ%t?c?PJ6O_o9 zP$ENjqyFSfU7VclZU4*V|79`&kRLk&`N|6XZ3RKBzjY{@%a(mUs{BBhpT4+RR%W7n zloljYBzC_tG|J2GKDOR>cY567J$LFt(h@jY_=XLMN4NtwuB<+}nS@`!?jFy*m#f+q z5hQ^T)#YdU)+cevniIv=%aA`Lh8D@ox(MNY@KMifN=MH)D0AG{c&Wrq$whZWqTrez z)sj+#KG>T}J@WB967FM5Pv;~49HVbF`$FIPhI9D-GvD(sW0$rV%Zys;_!5(JoM4}b z<9TyRh<5^&_;mharP>2V%Z@5-NBq!s;YxybAKz>bYb&tqnVcm_5am~zD0&ElWSn?C z1=L?!N>rLf9oGpF2sDp;hZyCp(e9QcjRHzF6ePp3Jc_mBMyV1oD|&N)GRW? zf7zS(75vwc?;lVAKpOR5;Qwa;{HvW`=YW4$@4A@K-Cp_GACB!V7KBLT}~Q z?(DAyel1!5Fz|->Uk3gtV1I@FnxFpxEr521q5tjlYqtKYg}>%)f8YUt5$L@AZ&}>0 z@V|!KKf_h1{sjMb0R9#IS3mb>GzsWWXygC$dP)!kC_R4LM|%e7ggVCny5GM24?_)) A%m4rY literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index 83c7f598..dbb05e8c 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -1111,6 +1111,42 @@ class CoinCellAssemblyWorkstation(WorkstationBase): raise RuntimeError(error_msg) logger.info(" ✓ COIL_GB_L_IGNORE_CMD 检查通过 (值为False,使用左手套箱)") + + # 检查握手寄存器残留(正常初始状态均应为False) + # 若上次运行意外断网,这些Unilab侧COIL可能被遗留为True,导致PLC逻辑卡死 + handshake_checks = [ + ("COIL_UNILAB_SEND_MSG_SUCC_CMD", "Unilab→PLC 配方发送完毕", "上次配方握手未正常复位,PLC可能处于等待配方的卡死状态"), + ("COIL_UNILAB_REC_MSG_SUCC_CMD", "Unilab→PLC 数据接收完毕", "上次数据接收握手未正常复位"), + ("UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM", "Unilab→PLC 瓶数发送完毕", "上次瓶数握手未正常复位"), + ("UNILAB_SEND_FINISHED_CMD", "Unilab→PLC 一组完成确认", "上次完成握手未正常复位"), + ("COIL_REQUEST_REC_MSG_STATUS", "PLC→Unilab 请求接收配方", "PLC正处于等待配方状态,设备流程已卡死,需重启PLC或手动复位握手"), + ("COIL_REQUEST_SEND_MSG_STATUS", "PLC→Unilab 请求发送测试数据", "PLC正处于等待发送数据状态,设备流程已卡死"), + ] + for coil_name, coil_desc, stuck_reason in handshake_checks: + try: + hs_node = self.client.use_node(coil_name) + hs_value, hs_err = hs_node.read(1) + if hs_err: + logger.warning(f" ⚠ 无法读取 {coil_name},跳过此项检查") + continue + hs_actual = hs_value[0] if isinstance(hs_value, (list, tuple)) else hs_value + logger.info(f" {coil_name} 当前值: {hs_actual}") + if hs_actual: + error_msg = ( + "❌ 前置握手寄存器检查失败!\n" + f" {coil_name} = True (期望值: False)\n" + f" 含义: {coil_desc}\n" + f" 原因: {stuck_reason}\n" + " 建议: 检查上次运行是否意外中断,手动将该寄存器置为False后重试" + ) + logger.error(error_msg) + raise RuntimeError(error_msg) + logger.info(f" ✓ {coil_name} 检查通过 (值为False)") + except RuntimeError: + raise + except Exception as hs_e: + logger.warning(f" ⚠ 检查 {coil_name} 时发生异常: {hs_e},跳过此项") + logger.info("✓ 所有前置条件检查通过!") except ValueError as e: diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_20260112.xlsx b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_20260112.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..40a421b56a3d882b7fbf315de6bbb1729bc3896b GIT binary patch literal 18292 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 Ang9R* literal 0 HcmV?d00001 diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv new file mode 100644 index 00000000..7468dbcb --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260317.csv @@ -0,0 +1,10 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260317_162514,0.6470000147819519,28.75,502.0,3318,80,7,NoRead88,YS104219,, +20260317_163955,0.6800000071525574,28.780000686645508,883.0,3285,80,7,NoRead88,YS104395,, +20260317_171603,1.1490000486373901,0.0,2167.0,3302,80,7,NoRead88,YS104287,, +20260317_172257,1.3760000467300415,28.10999870300293,414.0,3269,80,7,NoRead88,YS104286,, +20260317_173332,3.171999931335449,28.84000015258789,634.0,3318,80,7,NoRead88,17160106,, +20260317_173614,3.0429999828338623,28.75,161.0,3285,80,7,NoRead88,YS104389,, +20260317_173856,3.140000104904175,28.579998016357422,160.0,3205,80,7,NoRead88,YS104357,, +20260317_174428,3.171999931335449,28.06999969482422,318.0,3285,80,7,NoRead88,YS104212,, +20260317_180440,3.171999931335449,28.44999885559082,200.0,3269,80,7,NoRead88,YS104228,, diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv new file mode 100644 index 00000000..86b1133c --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260319.csv @@ -0,0 +1,2 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260319_114636,0.2590000033378601,27.529996871948242,258.0,3302,60,7,NoRead88,YS104373,, diff --git a/unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv b/unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv new file mode 100644 index 00000000..60b16c27 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/date_20260323.csv @@ -0,0 +1,7 @@ +Time,open_circuit_voltage,pole_weight,assembly_time,assembly_pressure,electrolyte_volume,coin_num,electrolyte_code,coin_cell_code,formulation_order_code,formulation_ratio +20260323_144656,0.01600000075995922,12.25999927520752,220.0,3334,20,7,NoRead88,14441891,, +20260323_144929,0.0,11.940000534057617,152.0,3075,20,7,NoRead88,14465181,, +20260323_145329,0.0,12.229999542236328,160.0,3269,20,7,NoRead88,14492431,, +20260323_145726,0.0,12.34999942779541,316.0,3367,20,7,NoRead88,14544961,, +20260323_150000,0.0,12.100000381469727,152.0,3269,20,7,NoRead88,14572221,, +20260323_150514,0.0,12.49000072479248,314.0,3237,20,7,NoRead88,14595521,, diff --git a/unilabos/devices/workstation/implementation_plan.md b/unilabos/devices/workstation/implementation_plan.md new file mode 100644 index 00000000..86f2ee32 --- /dev/null +++ b/unilabos/devices/workstation/implementation_plan.md @@ -0,0 +1,88 @@ +# 物料系统标准化重构方案 + +根据开发者的反馈,本方案旨在遵循“标准化而非绕过”的原则,对资源类(Deck、Carrier、Magazine)进行重构。核心目标是将物理结构的初始化与物料/极片的初始填充逻辑解耦,彻底解决反序列化过程中的初始化冲突。 + +## 拟议变更 + +### [参考] PRCXI9300 标准化模式 +#### [参考文件] [prcxi.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/liquid_handling/prcxi/prcxi.py) +* **PRCXI9300Deck**: 演示了如何在 `serialize` 中导出 `sites` 元数据,以及如何在 `assign_child_resource` 中实现稳健的槽位匹配(支持按名称、坐标或索引匹配)。 +* **PRCXI9300Container**: 演示了标准的 `load_state` 和 `serialize_state` 模式,确保业务状态(如 `Material` UUID)能正确往返序列化。 + +### [组件] 台面 (Decks) +#### [修改] [decks.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/bioyond/decks.py) +* 将 `BIOYOND_YB_Deck` 重命名为 **`BioyondElectrolyteDeck`**,对应工厂函数 `YB_Deck()` 重命名为 **`bioyond_electrolyte_deck()`**。 +* `BIOYOND_PolymerReactionStation_Deck` 和 `BIOYOND_PolymerPreparationStation_Deck` **保持不变**。 +* 以上三个 Deck 的 `__init__` 中均移除 `setup` 参数和 `setup()` 调用,删除临时的 `deserialize` 重写。 + +#### [修改 + 重命名] [YB_YH_materials.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py) → `yihua_coin_cell_materials.py` +* 将 `CoincellDeck` 重命名为 **`YihuaCoinCellDeck`**,对应工厂函数 `YH_Deck()` 重命名为 **`yihua_coin_cell_deck()`**。 +* 从 `YihuaCoinCellDeck.__init__` 中移除 `setup` 参数和 `setup()` 调用,删除临时的 `deserialize` 重写。 + +### [组件] 容器类与弹夹 (Itemized Carriers & Magazines) +#### [修改] [magazine.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/battery/magazine.py) +* 重构 `magazine_factory`:将创建 `MagazineHolder` 几何结构(空槽位)的过程与填充 `ElectrodeSheet` 物料的过程分离。 +* 确保 `MagazineHolder` 和 `Magazine` 的 `__init__` 过程中不主动创建任何内容物。 + +#### [修改] [warehouse.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/warehouse.py) +* 确保 `WareHouse` 类和 `warehouse_factory` 遵循相同模式:先初始化几何结构,内容物另行处理。 + +#### [修改] [itemized_carrier.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/itemized_carrier.py) +* 移除之前添加的 `idx is None` 兜底补丁。 +* 修复命名规范,确保 `assign_child_resource` 在反序列化时能准确匹配资源。 + +### [组件] 状态兼容性 (State Compatibility) +#### [修改] [resource_tracker.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/resources/resource_tracker.py) +* 在 `to_plr_resources` 方法中调用 `load_all_state` 之前,预处理 `all_states` 字典。 +* 对于 `Container` 类型的资源,如果其状态中缺少 `liquid_history` 或 `pending_liquids` 等 PLR 新版本要求的键,则填充默认值(如空列表/字典),防止反序列化中断。 + +### [组件] 料盘 (Material Plates) +#### [修改] [YB_YH_materials.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py) +* 重构 `MaterialPlate`:不在 `__init__` 中直接调用 `create_ordered_items_2d`。 +* 重构 `YIHUA_Electrolyte_12VialCarrier`:将其修改为标准的基类定义或在工厂方法中彻底剥离内部 12 个 `YB_pei_ye_xiao_Bottle` 的强制初始化,以防反序列化冲突。 + +### [组件] 跨站转运与分液瓶板 (Vial Plate Transfer) +#### [修改] [bioyond_cell_workstation.py] & [YB_YH_materials.py] +* **分析**:目前的 `bioyond_cell_workstation.py` 在执行转移时,是用 `sites=["electrolyte_buffer"]` 试图把整块 `YB_Vial_5mL_Carrier` 板转移给目标。但由于实际工艺中,配液站将分液瓶板传往扣电工站后,是由扣电工站的机械臂**逐瓶抓取**并放入内部的 `bottle_rack_6x2`(电解液缓存位),用完后再放入 `bottle_rack_6x2_2`(废液位),因此配液站的这一次“跨工位资源树转移”在逻辑上存在偏差:目标槽位不应该是装单瓶的载体 `bottle_rack`。 +* **修复方案**: + 1. **目标端 (Yihua 侧)**: + * 在 `YB_YH_materials.py` 中为从配液站传过来的“分液瓶板”本身设置一个接驳专用的 `PlateSlot`(或者单纯直接移到 Deck 指定坐标)。这个位置负责真正在资源树层级上合法接收配液站传过来的完整 Board。 + * 重构 `YIHUA_Electrolyte_12VialCarrier`:为了防止初始化反序列化冲突,取消内部在 `__init__` 中自动填充满 12 个 `YB_pei_ye_xiao_Bottle` 实例的逻辑。`bottle_rack_6x2` 和 `bottle_rack_6x2_2` 初始化时均应为空。 + 2. **转运端 (Bioyond 侧)**: + * 修改 `bioyond_cell_workstation.py` 的资源树数字转运代码,将其转移目标对应到 Yihua 侧新设立的“分液瓶板接驳区域”资源,或者干脆只更新资源树坐标位置(使其脱离 Bioyond Deck 加入 Yihua Deck),而不再强行挂载到一个无法容纳 Carrier 的 `bottle_rack_6x2` 内部。 + +### [组件] 依华扣电组装工站物料余量监控 (Material Monitoring) +#### [修改] 寄存器直读与前端集成 +* **物理对象保留但虚化追踪**:原有的实体台面对象(如 `MaterialPlate`、`MagazineHolder` 各类型及其对应的洞位坐标)**仍然保留并使用**。保留它们是为了给机器臂提供基础的物理空间取放标定,以及作为前端页面的可视和可交互区块。 +* **内部物料免追踪**:既然余量完全由寄存器接管,**我们将不再在这些弹夹或洞位内部显式生成、塞入和追踪每一个具体的极片或外壳对象 (如 `ElectrodeSheet` 等)**。这恰好与我们的重构主旨(不主动在 `__init__` 建子物料以避开反序列化冲突)完美结合,进一步极大地减轻了后台资源树对象的复杂度。 +* **监控方式变更**:放弃现有的物料余量方式,直接读取依华扣电组装工站开放的寄存器地址以获取准确余量。 +* **前端界面集成**:在前端界面点击负极壳、弹垫片等弹夹的 data view 时,直接读取并显示寄存器中的各自余量。 +* **新增寄存器映射** (参考 `coin_cell_assembly_b.csv`): + * `10mm正极片剩余物料数量(R)`:`read hold_register 520` (REAL) + * `12mm正极片剩余物料数量(R)`:`read hold_register 522` (REAL) + * `16mm正极片剩余物料数量(R)`:`read hold_register 524` (REAL) + * `铝箔剩余物料数量(R)`:`read hold_register 526` (REAL) + * `正极壳剩余物料数量(R)`:`read hold_register 528` (REAL) + * `平垫剩余物料数量(R)`:`read hold_register 530` (REAL) + * `负极壳剩余物料数量(R)`:`read hold_register 532` (REAL) + * `弹垫剩余物料数量(R)`:`read hold_register 534` (REAL) + * `成品电池剩余可容纳数量(R)`:`read hold_register 536` (REAL) + * `成品电池NG槽剩余可容纳数量(R)`:`read hold_register 538` (REAL) + +### [配置] JSON 配置文件 (Configuration Files) +#### [修改] 资源类型名称更新 +* 更新以下配置文件,将其中的 `BIOYOND_YB_Deck` 替换为新的类名 **`BioyondElectrolyteDeck`**,以及将 `coin_cell_deck` 替换为 **`YihuaCoinCellDeck`**: + * `yibin_electrolyte_config.json` + * `yibin_coin_cell_only_config.json` + * `yibin_electrolyte_only_config.json` + +## 验证计划 + +### 自动化测试 +* 对重构后的类运行 `pylabrobot` 序列化/反序列化测试,确保状态能够完美恢复。 +* 检查各工作站节点启动时是否仍存在 `ValueError: Resource '...' already assigned to deck` 报错。 +* 检查 `resource_tracker` 中是否仍存在重复 UUID 报错。 + +### 手动验证 +* 重启各工作站节点,验证资源树是否能根据数据库数据正确还还原。 +* 验证“自动”与“手动”传输窗资源在台面上的分配是否正确。 diff --git a/unilabos/devices/workstation/implementation_plan_v2.md b/unilabos/devices/workstation/implementation_plan_v2.md new file mode 100644 index 00000000..7e2233ef --- /dev/null +++ b/unilabos/devices/workstation/implementation_plan_v2.md @@ -0,0 +1,388 @@ +# 物料系统标准化重构方案 v2(增强版) + +> **基于原始方案 (`implementation_plan.md`) 的补充与细化**。 +> 本文档在原方案基础上:①增加当前代码现状核查结果;②明确各任务的执行顺序与文件级改动;③新增注意事项与回归测试命令。 + +--- + +## 0. 核心原则(保持不变) + +"**物理几何结构初始化(Deck / Carrier / Magazine 的 `__init__`)与物料内容物填充(`setup()` / `klasses` 参数)必须彻底解耦**",以消除 PLR 反序列化时的 `Resource already assigned to deck` 错误。 + +--- + +## 1. 当前代码现状核查(2026-03-12) + +| 文件 | 计划要求 | 当前状态 | 是否完成 | +|---|---|---|---| +| `resources/bioyond/decks.py` | 重命名类;移除 `setup` 参数和 `deserialize` 补丁 | 仍是 `BIOYOND_YB_Deck`;`setup` 参数和 `deserialize` 均存在 | ❌ | +| `coin_cell_assembly/YB_YH_materials.py` | 重命名类;文件迁移;移除补丁 | 仍是 `CoincellDeck`;`setup` 参数和 `deserialize` 均存在 | ❌ | +| `resources/battery/magazine.py` | `magazine_factory` 不主动填充物料 | `MagazineHolder_6_Cathode` / `_6_Anode` / `_4_Cathode` 仍传 `klasses`,初始化时填满极片 | ❌ | +| `resources/battery/bottle_carriers.py` | `YIHUA_Electrolyte_12VialCarrier` 初始化时不填充瓶子 | 第 54-55 行仍循环填充 12 个 `YB_pei_ye_xiao_Bottle` | ❌ | +| `resources/itemized_carrier.py` | 移除 `idx is None` 兜底补丁 | 第 182-190 行仍保留该兜底逻辑 | ❌(待前置任务完成后移除) | +| `resources/resource_tracker.py` | `load_all_state` 前预填 `Container` 缺失键 | 第 616 行直接调用,无预处理 | ❌ | +| `bioyond_cell_workstation.py` | 修正跨站转运目标为合法接驳槽 | 第 1563 行仍 `sites=["electrolyte_buffer"]`,目标 UUID 为硬编码虚拟资源 | ❌ | +| `yibin_*.json` 配置文件 | 更新类名 | 仍使用 `BIOYOND_YB_Deck` / `CoincellDeck` | ❌ | +| `registry/resources/bioyond/deck.yaml` | 更新类名(原计划未提及) | 仍使用 `BIOYOND_YB_Deck` / `CoincellDeck` | ❌(**新增**) | + +--- + +## 2. 执行顺序(含依赖关系) + +``` +阶段 A(底层资源类) + A1. magazine.py — 移除 klasses 填充 + A2. bottle_carriers.py — 移除瓶子填充 + +阶段 B(Deck 层) + B1. decks.py — 移除 setup 参数和 deserialize 补丁;重命名 + B2. YB_YH_materials.py → 重命名;移除 CoincellDeck 的 setup 参数和 deserialize 补丁 + +阶段 C(状态兼容) + C1. resource_tracker.py — 预填 Container 缺失键 + C2. itemized_carrier.py — 移除 idx is None 兜底补丁(B 阶段完成后) + +阶段 D(跨站转运修复) + D1. YB_YH_materials.py 新增 vial_plate_dock(接驳专用槽) + D2. bioyond_cell_workstation.py 修正 transfer 目标 + +阶段 E(配置与注册表) + E1. yibin_*.json 更新类名 + E2. registry/resources/bioyond/deck.yaml 更新类名 + E3. coin_cell_assembly.py 更新导入路径(若文件重命名) +``` + +--- + +## 3. 分阶段详细说明 + +--- + +### 阶段 A — 底层资源类 + +#### A1. `unilabos/resources/battery/magazine.py` + +**问题**:`MagazineHolder_6_Cathode`、`MagazineHolder_6_Anode`、`MagazineHolder_4_Cathode` 在调用 `magazine_factory` 时传入 `klasses`,导致每次 `__init__` 就填满极片,反序列化时重复添加。 + +**修改**: + +- 将三个函数中的 `klasses=[...]` 改为 `klasses=None`(与 `MagazineHolder_6_Battery` 保持一致)。 +- **理由**:物料余量已由寄存器管理(见阶段 F),不需要在资源树中追踪每一个极片。 + +```python +# 修改前(MagazineHolder_6_Cathode 举例) +klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan], + +# 修改后 +klasses=None, +``` + +> **注意**:`magazine_factory` 中 `klasses` 参数及循环体代码保留(仍可按需在非序列化场景使用),只是各具体工厂函数不再传入。 + +--- + +#### A2. `unilabos/resources/battery/bottle_carriers.py` + +**问题**:`YIHUA_Electrolyte_12VialCarrier` 第 54-55 行在工厂函数末尾循环填充 12 个瓶子。 + +**修改**:删除以下两行: + +```python +# 删除 +for i in range(12): + carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}") +``` + +**理由**:`bottle_rack_6x2` 和 `bottle_rack_6x2_2` 均应初始化为空,瓶子由 Bioyond 侧实际转运后再填入。 + +--- + +### 阶段 B — Deck 层重构 + +#### B1. `unilabos/resources/bioyond/decks.py` + +**改动列表**: + +1. **重命名** `BIOYOND_YB_Deck` → `BioyondElectrolyteDeck` +2. **重命名** `YB_Deck()` 工厂函数 → `bioyond_electrolyte_deck()` +3. **移除** `__init__` 中的 `setup: bool = False` 参数及 `if setup: self.setup()` 调用 +4. **删除** `deserialize` 方法重写(该临时补丁在 `setup` 参数移除后自然失效,继续保留反而掩盖问题) +5. `BIOYOND_PolymerReactionStation_Deck` 和 `BIOYOND_PolymerPreparationStation_Deck` 同步执行第 3、4 步 + +**重构后初始化模式**: + +```python +class BioyondElectrolyteDeck(Deck): + def __init__(self, name: str = "YB_Deck", ...): + super().__init__(name=name, ...) + # ❌ 不调用 self.setup() + # PLR 反序列化时只会调用 __init__,然后从 children JSON 重建子资源 + + def setup(self) -> None: + # 完整的子资源初始化逻辑保留在这里,只由工厂函数调用 + ... + +def bioyond_electrolyte_deck(name: str) -> BioyondElectrolyteDeck: + deck = BioyondElectrolyteDeck(name=name) + deck.setup() # ✅ 工厂函数负责填充 + return deck +``` + +**同步修改**: +- `bioyond_cell_workstation.py` 第 20 行: + ```python + # 修改前 + from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck + # 修改后 + from unilabos.resources.bioyond.decks import BioyondElectrolyteDeck + ``` +- 同文件第 2440 行:`BIOYOND_YB_Deck(setup=True)` → `bioyond_electrolyte_deck(name="YB_Deck")` + +--- + +#### B2. `unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py` + +**改动列表**: + +1. **重命名** `CoincellDeck` → `YihuaCoinCellDeck` +2. **重命名** `YH_Deck()` → `yihua_coin_cell_deck()`(可保留 `YH_Deck` 作为兼容别名,日后废弃) +3. **移除** `CoincellDeck.__init__` 中 `setup: bool = False` 参数及调用 +4. **删除** `CoincellDeck.deserialize` 重写方法 +5. `MaterialPlate.__init__` 中移除 `fill` 参数,始终不主动调用 `create_ordered_items_2d`(当前 `fill=False` 路径已正确,只需删除 `fill=True` 分支) + +```python +# 修改前(MaterialPlate.__init__ 片段) +if fill: + super().__init__(..., ordered_items=holes, ...) +else: + super().__init__(..., ordered_items=ordered_items, ...) + +# 修改后(始终走 "不填充" 路径) +super().__init__(..., ordered_items=ordered_items, ...) +# holes 的创建代码整体移入独立工厂方法 +``` + +**同步修改**: +- `coin_cell_assembly.py` 第 20 行导入: + ```python + # 修改前 + from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck + # 修改后 + from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import YihuaCoinCellDeck + ``` +- 同文件第 2245 行:`CoincellDeck(setup=True, name="coin_cell_deck")` → `yihua_coin_cell_deck(name="coin_cell_deck")` +- 文件重命名(可选):`YB_YH_materials.py` → `yihua_coin_cell_materials.py`(若重命名,所有 import 路径需全局替换) + +--- + +### 阶段 C — 状态兼容 + +#### C1. `unilabos/resources/resource_tracker.py` + +**问题**:第 616 行直接调用 `plr_resource.load_all_state(all_states)`,若 `Container` 类资源的 `data` 字段缺少 `liquid_history` 或 `pending_liquids`,PLR 新版本会抛出 `KeyError`。 + +**修改**:在第 616 行前插入预处理: + +```python +# 在 load_all_state 调用前预填缺失键 +from pylabrobot.resources.container import Container as PLRContainer +for res_name, state in all_states.items(): + if state and isinstance(state, dict): + # Container 类型要求这两个键存在 + state.setdefault("liquid_history", []) + state.setdefault("pending_liquids", {}) + +plr_resource.load_all_state(all_states) +``` + +--- + +#### C2. `unilabos/resources/itemized_carrier.py` + +**前提**:B1、B2 阶段完成,Deck 类名与资源命名规范已对齐后再执行此步。 + +**修改**:删除第 182-190 行的兜底补丁: + +```python +# 删除以下整个 if 块 +if idx is None: + fallback_location = location if location is not None else Coordinate.zero() + super().assign_child_resource(resource, location=fallback_location, reassign=reassign) + return +``` + +**替代**:改为抛出带诊断信息的异常,便于后续问题排查: + +```python +if idx is None: + raise ValueError( + f"[ItemizedCarrier] 无法为资源 '{resource.name}' 找到匹配的槽位。" + f"已知槽位:{list(self.child_locations.keys())}," + f"传入坐标:{location}" + ) +``` + +--- + +### 阶段 D — 跨站转运修复 + +#### D1. `YB_YH_materials.py` — 新增分液瓶板接驳槽 + +在 `YihuaCoinCellDeck.setup()` 中,新增一个专用于接收 Bioyond 侧传来的完整分液瓶板的 `ResourceStack`(或 `PlateSlot`): + +```python +# 在 setup() 末尾追加 +from pylabrobot.resources.resource_stack import ResourceStack + +vial_plate_dock = ResourceStack( + name="electrolyte_buffer", # 保持与 bioyond_cell_workstation.py 的 sites 键一致 + direction="z", + resources=[], +) +self.assign_child_resource(vial_plate_dock, Coordinate(x=1050.0, y=700.0, z=0)) +``` + +> **说明**:槽位命名 `electrolyte_buffer` 与 `bioyond_cell_workstation.py` 现有的 `sites=["electrolyte_buffer"]` 对应,减少改动量。如改名,D2 需同步。 + +--- + +#### D2. `bioyond_cell_workstation.py` — 修正 transfer 目标 + +**问题**:第 1545-1552 行创建了一个 `size=1,1,1` 的虚拟 `ResourcePLR` 并硬编码 UUID,这个对象在 YihuaCoinCellDeck 的资源树中不存在,导致转移后资源树状态混乱。 + +**修改**: + +```python +# 修改前:创建虚拟目标资源 +target_resource_obj = ResourcePLR(name=target_location, size_x=1.0, ...) +target_resource_obj.unilabos_uuid = "550e8400-e29b-41d4-a716-446655440001" # 硬编码 + +# 修改后:通过 ROS2/设备注册表查询真实资源 +# (需要从 target_device 的资源树中取出 electrolyte_buffer 的真实对象) +target_resource_obj = self._get_resource_from_device( + device_id=target_device, + resource_name=target_location +) +if target_resource_obj is None: + raise RuntimeError( + f"目标设备 {target_device} 中未找到资源 '{target_location}'," + f"请确认 YihuaCoinCellDeck.setup() 中已添加 electrolyte_buffer 槽位" + ) +``` + +> **说明**:`_get_resource_from_device` 需根据现有 ROS2 资源同步机制实现,或复用已有的 `get_plr_resource_by_name` 类似方法。 + +--- + +### 阶段 E — 配置与注册表 + +#### E1. `yibin_electrolyte_config.json` / `yibin_coin_cell_only_config.json` / `yibin_electrolyte_only_config.json` + +全局替换以下字符串: + +| 旧值 | 新值 | +|---|---| +| `BIOYOND_YB_Deck` | `BioyondElectrolyteDeck` | +| `unilabos.resources.bioyond.decks:BIOYOND_YB_Deck` | `unilabos.resources.bioyond.decks:BioyondElectrolyteDeck` | +| `CoincellDeck` | `YihuaCoinCellDeck` | +| `unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck` | 若文件已重命名:`unilabos.devices.workstation.coin_cell_assembly.yihua_coin_cell_materials:YihuaCoinCellDeck` | + +--- + +#### E2. `unilabos/registry/resources/bioyond/deck.yaml`(**原计划未覆盖,新增**) + +当前第 25 行和第 37 行仍使用旧类名,需同步更新: + +```yaml +# 修改前 +BIOYOND_YB_Deck: + ... +CoincellDeck: + ... + +# 修改后 +BioyondElectrolyteDeck: + ... +YihuaCoinCellDeck: + ... +``` + +--- + +### 阶段 F — 物料余量监控集成(原计划第5节细化) + +**目标**:弃用资源树内极片对象计数,改为直读依华扣电工站寄存器。 + +#### F1. `coin_cell_assembly/coin_cell_assembly.py` — 新增寄存器读取方法 + +参考 `coin_cell_assembly_b.csv` 中的地址,封装读取工具方法: + +```python +MATERIAL_REGISTER_MAP = { + "10mm正极片": (520, "REAL"), + "12mm正极片": (522, "REAL"), + "16mm正极片": (524, "REAL"), + "铝箔": (526, "REAL"), + "正极壳": (528, "REAL"), + "平垫": (530, "REAL"), + "负极壳": (532, "REAL"), + "弹垫": (534, "REAL"), + "成品容量": (536, "REAL"), + "成品NG容量": (538, "REAL"), +} + +def get_material_remaining(self, material_name: str) -> float: + """通过寄存器直读指定物料的剩余数量""" + if material_name not in MATERIAL_REGISTER_MAP: + raise KeyError(f"未知物料名称: {material_name}") + address, dtype = MATERIAL_REGISTER_MAP[material_name] + return self.read_hold_register(address, dtype) # 复用现有 Modbus 读取方法 +``` + +#### F2. 前端 data view 集成 + +- 前端点击 `MagazineHolder` 类资源的 data view 时,调用后端 `get_material_remaining` 接口(而非读取 `children` 长度)。 +- 具体接口路径和前端调用代码需与前端开发同步,本文档不作具体实现约定。 + +--- + +## 4. 验证计划(细化) + +### 4.1 单元测试(自动化) + +```bash +# 序列化/反序列化往返测试 +python -m pytest unilabos/test/ -k "serial" -v + +# 特别检查以下错误消失: +# - ValueError: Resource '...' already assigned to deck +# - KeyError: 'liquid_history' +# - 重复 UUID 报错 +``` + +### 4.2 集成测试(手动) + +按以下顺序逐步验证,确保每步正常后再进行下一步: + +1. **单独启动 `BatteryStation` 节点**,检查 `CoincellDeck`(现 `YihuaCoinCellDeck`)能否从数据库状态正确还原,无 `already assigned` 报错。 +2. **单独启动 `BioyondElectrolyte` 节点**,检查 `BioyondElectrolyteDeck` 反序列化正常。 +3. **同时启动两个节点**,模拟执行一次分液→扣电的完整跨站转运,确认: + - `electrolyte_buffer` 槽位正确接收分液瓶板。 + - `bottle_rack_6x2` 初始为空,不出现虚拟瓶子。 +4. **重启两个节点**(模拟断电恢复),确认资源树从数据库还原后,`electrolyte_buffer` 中仍持有正确的分液瓶板对象。 +5. **寄存器余量读取**:手动触发 `get_material_remaining("负极壳")`,确认返回值与设备显示一致。 + +--- + +## 5. 与原计划的差异对照 + +| 维度 | 原计划 | 本文档新增/修订 | +|---|---|---| +| 执行顺序 | 未排序 | 明确 A→B→C→D→E→F 的依赖顺序 | +| `itemized_carrier.py` | 移除兜底补丁 | 补充:替换为带诊断信息的异常,便于排查 | +| `bottle_carriers.py` | 提及 `YIHUA_Electrolyte_12VialCarrier` 需修改 | 明确:删除第 54-55 行的瓶子填充循环 | +| `MaterialPlate` | 提及移除 `fill` 参数 | 说明保留 `fill=False` 路径;整体删除 `fill=True` 分支 | +| `deck.yaml` | 未提及 | **新增**:该注册文件也需要同步更新类名 | +| `resource_tracker.py` | 简略描述 | 提供具体的 `setdefault` 预处理代码示例 | +| 跨站转运 | 描述了问题和方向 | 细化:新增 `electrolyte_buffer` 槽位的具体名称和坐标;修正 `transfer` 目标查找方式 | +| 验证计划 | 简述目标 | 提供具体测试命令和逐步手动验证流程 | diff --git a/unilabos/registry/resources/battery/bottle_carriers.yaml b/unilabos/registry/resources/battery/bottle_carriers.yaml new file mode 100644 index 00000000..d004ee8b --- /dev/null +++ b/unilabos/registry/resources/battery/bottle_carriers.yaml @@ -0,0 +1,12 @@ +YIHUA_Electrolyte_12VialCarrier: + category: + - battery_bottle_carriers + class: + module: unilabos.resources.battery.bottle_carriers:YIHUA_Electrolyte_12VialCarrier + type: pylabrobot + description: YIHUA 12-vial electrolyte carrier for coin cell assembly workstation + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/YB_bottle.yaml b/unilabos/registry/resources/bioyond/YB_bottle.yaml index f8e17261..b339e0d8 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle.yaml @@ -1,90 +1,146 @@ -YB_20ml_fenyeping: +YB_Vial_20mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping + module: unilabos.resources.bioyond.YB_bottles:YB_Vial_20mL type: pylabrobot - description: YB_20ml_fenyeping + description: YB_Vial_20mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_5ml_fenyeping: +YB_Vial_5mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping + module: unilabos.resources.bioyond.YB_bottles:YB_Vial_5mL type: pylabrobot - description: YB_5ml_fenyeping + description: YB_Vial_5mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_jia_yang_tou_da: +YB_DosingHead_L: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da + module: unilabos.resources.bioyond.YB_bottles:YB_DosingHead_L type: pylabrobot - description: YB_jia_yang_tou_da + description: YB_DosingHead_L handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_pei_ye_da_Bottle: +YB_PrepBottle_60mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle + module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_60mL type: pylabrobot - description: YB_pei_ye_da_Bottle + description: YB_PrepBottle_60mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_pei_ye_xiao_Bottle: +YB_PrepBottle_15mL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle + module: unilabos.resources.bioyond.YB_bottles:YB_PrepBottle_15mL type: pylabrobot - description: YB_pei_ye_xiao_Bottle + description: YB_PrepBottle_15mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_qiang_tou: +YB_Tip_5000uL: category: - - yb3 - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou + module: unilabos.resources.bioyond.YB_bottles:YB_Tip_5000uL type: pylabrobot - description: YB_qiang_tou + description: YB_Tip_5000uL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_ye_Bottle: +YB_Tip_1000uL: + category: + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_Tip_1000uL + type: pylabrobot + description: YB_Tip_1000uL + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_Tip_50uL: + category: + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_Tip_50uL + type: pylabrobot + description: YB_Tip_50uL + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_NormalLiq_250mL_Bottle: category: - - yb3 - YB_bottle_carriers - YB_bottle class: - module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle + module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_250mL_Bottle type: pylabrobot - description: YB_ye_Bottle + description: YB_NormalLiq_250mL_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_NormalLiq_100mL_Bottle: + category: + - YB_bottle_carriers + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_NormalLiq_100mL_Bottle + type: pylabrobot + description: YB_NormalLiq_100mL_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_HighVis_250mL_Bottle: + category: + - YB_bottle_carriers + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_HighVis_250mL_Bottle + type: pylabrobot + description: YB_HighVis_250mL_Bottle + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_HighVis_100mL_Bottle: + category: + - YB_bottle_carriers + - YB_bottle + class: + module: unilabos.resources.bioyond.YB_bottles:YB_HighVis_100mL_Bottle + type: pylabrobot + description: YB_HighVis_100mL_Bottle handles: [] icon: '' init_param_schema: {} diff --git a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml index 4698a266..c352d0f2 100644 --- a/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/YB_bottle_carriers.yaml @@ -1,37 +1,22 @@ -YB_100ml_yeti: +YB_Vial_20mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_20mL_Carrier type: pylabrobot - description: YB_100ml_yeti + description: YB_Vial_20mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_20ml_fenyepingban: +YB_Vial_5mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Vial_5mL_Carrier type: pylabrobot - description: YB_20ml_fenyepingban - handles: [] - icon: '' - init_param_schema: {} - registry_type: resource - version: 1.0.0 -YB_5ml_fenyepingban: - category: - - yb3 - - YB_bottle_carriers - class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban - type: pylabrobot - description: YB_5ml_fenyepingban + description: YB_Vial_5mL_Carrier handles: [] icon: '' init_param_schema: {} @@ -39,7 +24,6 @@ YB_5ml_fenyepingban: version: 1.0.0 YB_6StockCarrier: category: - - yb3 - YB_bottle_carriers class: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier @@ -52,7 +36,6 @@ YB_6StockCarrier: version: 1.0.0 YB_6VialCarrier: category: - - yb3 - YB_bottle_carriers class: module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier @@ -63,120 +46,135 @@ YB_6VialCarrier: init_param_schema: {} registry_type: resource version: 1.0.0 -YB_gao_nian_ye_Bottle: +YB_DosingHead_L_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_DosingHead_L_Carrier type: pylabrobot - description: YB_gao_nian_ye_Bottle + description: YB_DosingHead_L_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_gaonianye: +YB_PrepBottle_60mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_60mL_Carrier type: pylabrobot - description: YB_gaonianye + description: YB_PrepBottle_60mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_jia_yang_tou_da_Carrier: +YB_PrepBottle_15mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_PrepBottle_15mL_Carrier type: pylabrobot - description: YB_jia_yang_tou_da_Carrier + description: YB_PrepBottle_15mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_peiyepingdaban: +YB_TipRack_Mixed: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_Mixed type: pylabrobot - description: YB_peiyepingdaban + description: YB_TipRack_Mixed handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_peiyepingxiaoban: +YB_TipRack_5000uL: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingxiaoban + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_5000uL type: pylabrobot - description: YB_peiyepingxiaoban + description: YB_TipRack_5000uL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_qiang_tou_he: +YB_TipRack_50uL: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipRack_50uL type: pylabrobot - description: YB_qiang_tou_he + description: YB_TipRack_50uL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_shi_pei_qi_kuai: +YB_Adapter_60mL: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_Adapter_60mL type: pylabrobot - description: YB_shi_pei_qi_kuai + description: YB_Adapter_60mL handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_ye: +YB_NormalLiq_250mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottle_carriers:YB_ye + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_250mL_Carrier type: pylabrobot - description: YB_ye_Bottle_Carrier + description: YB_NormalLiq_250mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 -YB_ye_100ml_Bottle: +YB_NormalLiq_100mL_Carrier: category: - - yb3 - YB_bottle_carriers class: - module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_NormalLiq_100mL_Carrier type: pylabrobot - description: YB_ye_100ml_Bottle + description: YB_NormalLiq_100mL_Carrier handles: [] icon: '' init_param_schema: {} registry_type: resource version: 1.0.0 +YB_HighVis_250mL_Carrier: + category: + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_HighVis_250mL_Carrier + type: pylabrobot + description: YB_HighVis_250mL_Carrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 +YB_HighVis_100mL_Carrier: + category: + - YB_bottle_carriers + class: + module: unilabos.resources.bioyond.YB_bottle_carriers:YB_HighVis_100mL_Carrier + type: pylabrobot + description: YB_HighVis_100mL_Carrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 \ No newline at end of file diff --git a/unilabos/resources/bioyond/YB_bottle_carriers.py b/unilabos/resources/bioyond/YB_bottle_carriers.py index 29a53242..3add4f79 100644 --- a/unilabos/resources/bioyond/YB_bottle_carriers.py +++ b/unilabos/resources/bioyond/YB_bottle_carriers.py @@ -2,15 +2,18 @@ from pylabrobot.resources import create_homogeneous_resources, Coordinate, Resou from unilabos.resources.itemized_carrier import Bottle, BottleCarrier from unilabos.resources.bioyond.YB_bottles import ( - YB_jia_yang_tou_da, - YB_ye_Bottle, - YB_ye_100ml_Bottle, - YB_gao_nian_ye_Bottle, - YB_5ml_fenyeping, - YB_20ml_fenyeping, - YB_pei_ye_xiao_Bottle, - YB_pei_ye_da_Bottle, - YB_qiang_tou, + YB_DosingHead_L, + YB_NormalLiq_250mL_Bottle, + YB_NormalLiq_100mL_Bottle, + YB_HighVis_250mL_Bottle, + YB_HighVis_100mL_Bottle, + YB_Vial_5mL, + YB_Vial_20mL, + YB_PrepBottle_15mL, + YB_PrepBottle_60mL, + YB_Tip_5000uL, + YB_Tip_1000uL, + YB_Tip_50uL, ) # 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial @@ -206,7 +209,7 @@ def YB_6VialCarrier(name: str) -> BottleCarrier: return carrier # 1瓶载架 - 单个中央位置 -def YB_ye(name: str) -> BottleCarrier: +def YB_NormalLiq_250mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -233,17 +236,17 @@ def YB_ye(name: str) -> BottleCarrier: resource_size_y=beaker_diameter, name_prefix=name, ), - model="YB_ye", + model="YB_NormalLiq_250mL_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_ye_Bottle(f"{name}_flask_1") + carrier[0] = YB_NormalLiq_250mL_Bottle(f"{name}_flask_1") return carrier # 高粘液瓶载架 - 单个中央位置 -def YB_gaonianye(name: str) -> BottleCarrier: +def YB_HighVis_250mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -270,17 +273,17 @@ def YB_gaonianye(name: str) -> BottleCarrier: resource_size_y=beaker_diameter, name_prefix=name, ), - model="YB_gaonianye", + model="YB_HighVis_250mL_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1") + carrier[0] = YB_HighVis_250mL_Bottle(f"{name}_flask_1") return carrier -# 100ml液体瓶载架 - 单个中央位置 -def YB_100ml_yeti(name: str) -> BottleCarrier: +# 100mL普通液瓶载架 - 单个中央位置 +def YB_NormalLiq_100mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -307,16 +310,52 @@ def YB_100ml_yeti(name: str) -> BottleCarrier: resource_size_y=beaker_diameter, name_prefix=name, ), - model="YB_100ml_yeti", + model="YB_NormalLiq_100mL_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1") + carrier[0] = YB_NormalLiq_100mL_Bottle(f"{name}_flask_1") return carrier -# 5ml分液瓶板 - 4x2布局,8个位置 -def YB_5ml_fenyepingban(name: str) -> BottleCarrier: +# 100mL高粘液瓶载架 - 单个中央位置 +def YB_HighVis_100mL_Carrier(name: str) -> BottleCarrier: + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 60.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="YB_HighVis_100mL_Carrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_HighVis_100mL_Bottle(f"{name}_flask_1") + return carrier + +# 5mL分液瓶板 - 4x2布局,8个位置 +def YB_Vial_5mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) @@ -355,18 +394,18 @@ def YB_5ml_fenyepingban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_5ml_fenyepingban", + model="YB_Vial_5mL_Carrier", ) carrier.num_items_x = 4 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] for i in range(8): - carrier[i] = YB_5ml_fenyeping(f"{name}_vial_{ordering[i]}") + carrier[i] = YB_Vial_5mL(f"{name}_vial_{ordering[i]}") return carrier -# 20ml分液瓶板 - 4x2布局,8个位置 -def YB_20ml_fenyepingban(name: str) -> BottleCarrier: +# 20mL分液瓶板 - 4x2布局,8个位置 +def YB_Vial_20mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) @@ -405,18 +444,18 @@ def YB_20ml_fenyepingban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_20ml_fenyepingban", + model="YB_Vial_20mL_Carrier", ) carrier.num_items_x = 4 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] for i in range(8): - carrier[i] = YB_20ml_fenyeping(f"{name}_vial_{ordering[i]}") + carrier[i] = YB_Vial_20mL(f"{name}_vial_{ordering[i]}") return carrier # 配液瓶(小)板 - 4x2布局,8个位置 -def YB_peiyepingxiaoban(name: str) -> BottleCarrier: +def YB_PrepBottle_15mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) @@ -455,19 +494,19 @@ def YB_peiyepingxiaoban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_peiyepingxiaoban", + model="YB_PrepBottle_15mL_Carrier", ) carrier.num_items_x = 4 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] for i in range(8): - carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}") + carrier[i] = YB_PrepBottle_15mL(f"{name}_bottle_{ordering[i]}") return carrier # 配液瓶(大)板 - 2x2布局,4个位置 -def YB_peiyepingdaban(name: str) -> BottleCarrier: +def YB_PrepBottle_60mL_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -505,18 +544,18 @@ def YB_peiyepingdaban(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_peiyepingdaban", + model="YB_PrepBottle_60mL_Carrier", ) carrier.num_items_x = 2 carrier.num_items_y = 2 carrier.num_items_z = 1 ordering = ["A1", "A2", "B1", "B2"] for i in range(4): - carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}") + carrier[i] = YB_PrepBottle_60mL(f"{name}_bottle_{ordering[i]}") return carrier # 加样头(大)板 - 1x1布局,1个位置 -def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier: +def YB_DosingHead_L_Carrier(name: str) -> BottleCarrier: # 载架尺寸 (mm) carrier_size_x = 127.8 @@ -554,16 +593,16 @@ def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_jia_yang_tou_da_Carrier", + model="YB_DosingHead_L_Carrier", ) carrier.num_items_x = 1 carrier.num_items_y = 1 carrier.num_items_z = 1 - carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1") + carrier[0] = YB_DosingHead_L(f"{name}_head_1") return carrier -def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: +def YB_Adapter_60mL(name: str) -> BottleCarrier: """适配器块 - 单个中央位置""" # 载架尺寸 (mm) @@ -591,7 +630,7 @@ def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: resource_size_y=adapter_diameter, name_prefix=name, ), - model="YB_shi_pei_qi_kuai", + model="YB_Adapter_60mL", ) carrier.num_items_x = 1 carrier.num_items_y = 1 @@ -600,7 +639,7 @@ def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier: return carrier -def YB_qiang_tou_he(name: str) -> BottleCarrier: +def YB_TipRack_50uL(name: str) -> BottleCarrier: """枪头盒 - 8x12布局,96个位置""" # 载架尺寸 (mm) @@ -609,9 +648,9 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier: carrier_size_z = 55.0 # 枪头尺寸 - tip_diameter = 10.0 - tip_spacing_x = 9.0 # X方向间距 - tip_spacing_y = 9.0 # Y方向间距 + tip_diameter = 7.0 + tip_spacing_x = 7.5 # X方向间距 + tip_spacing_y = 7.5 # Y方向间距 # 计算起始位置 (居中排列) start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2 @@ -639,7 +678,7 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier: size_y=carrier_size_y, size_z=carrier_size_z, sites=sites, - model="YB_qiang_tou_he", + model="YB_TipRack_50uL", ) carrier.num_items_x = 12 carrier.num_items_y = 8 @@ -648,6 +687,182 @@ def YB_qiang_tou_he(name: str) -> BottleCarrier: for i in range(96): row = chr(65 + i // 12) # A-H col = (i % 12) + 1 # 1-12 - carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}") + carrier[i] = YB_Tip_50uL(f"{name}_tip_{row}{col}") + return carrier + + +def YB_TipRack_5000uL(name: str) -> BottleCarrier: + """枪头盒 - 4x6布局,24个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 95.0 + + # 枪头尺寸 + tip_diameter = 16.0 + tip_spacing_x = 16.5 # X方向间距 + tip_spacing_y = 16.5 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (6 - 1) * tip_spacing_x - tip_diameter) / 2 + start_y = (carrier_size_y - (4 - 1) * tip_spacing_y - tip_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=6, + num_items_y=4, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=tip_spacing_x, + item_dy=tip_spacing_y, + size_x=tip_diameter, + size_y=tip_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_TipRack_5000uL", + ) + carrier.num_items_x = 6 + carrier.num_items_y = 4 + carrier.num_items_z = 1 + # 创建24个枪头 + for i in range(24): + row = chr(65 + i // 6) # A-D + col = (i % 6) + 1 # 1-6 + carrier[i] = YB_Tip_5000uL(f"{name}_tip_{row}{col}") + return carrier + + + +def YB_TipRack_Mixed(name: str) -> BottleCarrier: + """混合枪头盒 - 复杂布局 + 上层: 2x8空位(原50uL枪头位置,现空余) + 中层: 4x4布局,放5000uL枪头 + 下层: 2x8布局,放1000uL枪头 + """ + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 95.0 + + # 各类枪头的尺寸参数 + tip_5000_diameter = 16.0 + tip_5000_spacing_x = 16.5 + tip_5000_spacing_y = 16.5 + + tip_1000_diameter = 7.0 + tip_1000_spacing_x = 7.5 + tip_1000_spacing_y = 7.5 + + # 空位尺寸(上层2x8,原50uL位置) + empty_diameter = 7.0 + empty_spacing_x = 7.5 + empty_spacing_y = 7.5 + + # 计算各层的起始位置 + # 上层空位 (2x8) + empty_top_start_x = (carrier_size_x - (8 - 1) * empty_spacing_x - empty_diameter) / 2 + empty_top_start_y = 5.0 + + # 中层5000uL (4x4) + tip_5000_start_x = (carrier_size_x - (4 - 1) * tip_5000_spacing_x - tip_5000_diameter) / 2 + tip_5000_start_y = empty_top_start_y + 2 * empty_spacing_y + 5.0 + + # 下层1000uL (2x8) + tip_1000_start_x = (carrier_size_x - (8 - 1) * tip_1000_spacing_x - tip_1000_diameter) / 2 + tip_1000_start_y = tip_5000_start_y + 4 * tip_5000_spacing_y + 5.0 + + sites = {} + + # 创建上层空位 (2x8) - 不创建实际的枪头对象 + empty_top_sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=8, + num_items_y=2, + dx=empty_top_start_x, + dy=empty_top_start_y, + dz=5.0, + item_dx=empty_spacing_x, + item_dy=empty_spacing_y, + size_x=empty_diameter, + size_y=empty_diameter, + size_z=carrier_size_z, + ) + # 添加空位,索引 0-15 + for k, v in empty_top_sites.items(): + v.name = f"{name}_empty_top_{v.name}" + sites[k] = v + + # 创建中层5000uL枪头位 (4x4),索引 16-31 + tip_5000_sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + num_items_y=4, + dx=tip_5000_start_x, + dy=tip_5000_start_y, + dz=15.0, + item_dx=tip_5000_spacing_x, + item_dy=tip_5000_spacing_y, + size_x=tip_5000_diameter, + size_y=tip_5000_diameter, + size_z=carrier_size_z, + ) + for i, (k, v) in enumerate(tip_5000_sites.items()): + v.name = f"{name}_5000_{v.name}" + sites[16 + i] = v + + # 创建下层1000uL枪头位 (2x8),索引 32-47 + tip_1000_sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=8, + num_items_y=2, + dx=tip_1000_start_x, + dy=tip_1000_start_y, + dz=25.0, + item_dx=tip_1000_spacing_x, + item_dy=tip_1000_spacing_y, + size_x=tip_1000_diameter, + size_y=tip_1000_diameter, + size_z=carrier_size_z, + ) + for i, (k, v) in enumerate(tip_1000_sites.items()): + v.name = f"{name}_1000_{v.name}" + sites[32 + i] = v + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=sites, + model="YB_TipRack_Mixed", + ) + carrier.num_items_x = 8 # 最大宽度 + carrier.num_items_y = 8 # 总行数 (2+4+2) + carrier.num_items_z = 1 + + # 为5000uL枪头创建实例 (16个),对应索引 16-31 + for i in range(16): + row = chr(65 + i // 4) # A-D + col = (i % 4) + 1 # 1-4 + carrier[16 + i] = YB_Tip_5000uL(f"{name}_tip5000_{row}{col}") + + # 为1000uL枪头创建实例 (16个),对应索引 32-47 + for i in range(16): + row = chr(65 + i // 8) # A-B + col = (i % 8) + 1 # 1-8 + carrier[32 + i] = YB_Tip_1000uL(f"{name}_tip1000_{row}{col}") + return carrier diff --git a/unilabos/resources/bioyond/YB_bottles.py b/unilabos/resources/bioyond/YB_bottles.py index acbbf35b..54f3e2a9 100644 --- a/unilabos/resources/bioyond/YB_bottles.py +++ b/unilabos/resources/bioyond/YB_bottles.py @@ -1,7 +1,7 @@ from unilabos.resources.itemized_carrier import Bottle, BottleCarrier # 工厂函数 """加样头(大)""" -def YB_jia_yang_tou_da( +def YB_DosingHead_L( name: str, diameter: float = 20.0, height: float = 100.0, @@ -15,11 +15,11 @@ def YB_jia_yang_tou_da( height=height, max_volume=max_volume, barcode=barcode, - model="YB_jia_yang_tou_da", + model="YB_DosingHead_L", ) -"""液1x1""" -def YB_ye_Bottle( +"""250mL普通液""" +def YB_NormalLiq_250mL_Bottle( name: str, diameter: float = 40.0, height: float = 70.0, @@ -33,83 +33,101 @@ def YB_ye_Bottle( height=height, max_volume=max_volume, barcode=barcode, - model="YB_ye_Bottle", + model="YB_NormalLiq_250mL_Bottle", ) -"""100ml液体""" -def YB_ye_100ml_Bottle( +"""100mL普通液""" +def YB_NormalLiq_100mL_Bottle( name: str, diameter: float = 50.0, height: float = 90.0, max_volume: float = 100000.0, # 100mL barcode: str = None, ) -> Bottle: - """创建100ml液体瓶""" + """创建100mL普通液瓶""" return Bottle( name=name, diameter=diameter, height=height, max_volume=max_volume, barcode=barcode, - model="YB_100ml_yeti", + model="YB_NormalLiq_100mL_Bottle", ) -"""高粘液""" -def YB_gao_nian_ye_Bottle( +"""100mL高粘液""" +def YB_HighVis_100mL_Bottle( + name: str, + diameter: float = 50.0, + height: float = 90.0, + max_volume: float = 100000.0, # 100mL + barcode: str = None, +) -> Bottle: + """创建100mL高粘液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_HighVis_100mL_Bottle", + ) + +"""250mL高粘液""" +def YB_HighVis_250mL_Bottle( name: str, diameter: float = 40.0, height: float = 70.0, max_volume: float = 50000.0, # 50mL barcode: str = None, ) -> Bottle: - """创建高粘液瓶""" + """创建250mL高粘液瓶""" return Bottle( name=name, diameter=diameter, height=height, max_volume=max_volume, barcode=barcode, - model="High_Viscosity_Liquid", + model="YB_HighVis_250mL_Bottle", ) -"""5ml分液瓶""" -def YB_5ml_fenyeping( +"""5mL分液瓶""" +def YB_Vial_5mL( name: str, diameter: float = 20.0, height: float = 50.0, max_volume: float = 5000.0, # 5mL barcode: str = None, ) -> Bottle: - """创建5ml分液瓶""" + """创建5mL分液瓶""" return Bottle( name=name, diameter=diameter, height=height, max_volume=max_volume, barcode=barcode, - model="YB_5ml_fenyeping", + model="YB_Vial_5mL", ) -"""20ml分液瓶""" -def YB_20ml_fenyeping( +"""20mL分液瓶""" +def YB_Vial_20mL( name: str, diameter: float = 30.0, height: float = 65.0, max_volume: float = 20000.0, # 20mL barcode: str = None, ) -> Bottle: - """创建20ml分液瓶""" + """创建20mL分液瓶""" return Bottle( name=name, diameter=diameter, height=height, max_volume=max_volume, barcode=barcode, - model="YB_20ml_fenyeping", + model="YB_Vial_20mL", ) """配液瓶(小)""" -def YB_pei_ye_xiao_Bottle( +def YB_PrepBottle_15mL( name: str, diameter: float = 35.0, height: float = 60.0, @@ -123,11 +141,11 @@ def YB_pei_ye_xiao_Bottle( height=height, max_volume=max_volume, barcode=barcode, - model="YB_pei_ye_xiao_Bottle", + model="YB_PrepBottle_15mL", ) """配液瓶(大)""" -def YB_pei_ye_da_Bottle( +def YB_PrepBottle_60mL( name: str, diameter: float = 55.0, height: float = 100.0, @@ -141,11 +159,29 @@ def YB_pei_ye_da_Bottle( height=height, max_volume=max_volume, barcode=barcode, - model="YB_pei_ye_da_Bottle", + model="YB_PrepBottle_60mL", ) -"""枪头""" -def YB_qiang_tou( +"""5000uL枪头""" +def YB_Tip_5000uL( + name: str, + diameter: float = 10.0, + height: float = 50.0, + max_volume: float = 5000.0, # 5mL + barcode: str = None, +) -> Bottle: + """创建枪头""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_Tip_5000uL", + ) + +"""1000uL枪头""" +def YB_Tip_1000uL( name: str, diameter: float = 10.0, height: float = 50.0, @@ -159,5 +195,23 @@ def YB_qiang_tou( height=height, max_volume=max_volume, barcode=barcode, - model="YB_qiang_tou", + model="YB_Tip_1000uL", ) + +"""50uL枪头""" +def YB_Tip_50uL( + name: str, + diameter: float = 10.0, + height: float = 50.0, + max_volume: float = 50.0, # 50uL + barcode: str = None, +) -> Bottle: + """创建枪头""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="YB_Tip_50uL", + ) \ No newline at end of file diff --git a/unilabos/utils/log-origin.py b/unilabos/utils/log-origin.py new file mode 100644 index 00000000..cee3269b --- /dev/null +++ b/unilabos/utils/log-origin.py @@ -0,0 +1,385 @@ +import logging +import os +import platform +from datetime import datetime +import ctypes +import atexit +import inspect +from typing import Tuple, cast + +# 添加TRACE级别到logging模块 +TRACE_LEVEL = 5 +logging.addLevelName(TRACE_LEVEL, "TRACE") + + +class CustomRecord: + custom_stack_info: Tuple[str, int, str, str] + + +# Windows颜色支持 +if platform.system() == "Windows": + # 尝试启用Windows终端的ANSI支持 + kernel32 = ctypes.windll.kernel32 + # 获取STD_OUTPUT_HANDLE + STD_OUTPUT_HANDLE = -11 + # 启用ENABLE_VIRTUAL_TERMINAL_PROCESSING + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + # 获取当前控制台模式 + handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) + mode = ctypes.c_ulong() + kernel32.GetConsoleMode(handle, ctypes.byref(mode)) + # 启用ANSI处理 + kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + + # 程序退出时恢复控制台设置 + @atexit.register + def reset_console(): + kernel32.SetConsoleMode(handle, mode.value) + + +# 定义不同日志级别的颜色 +class ColoredFormatter(logging.Formatter): + """自定义日志格式化器,支持颜色输出""" + + # ANSI 颜色代码 + COLORS = { + "RESET": "\033[0m", # 重置 + "BOLD": "\033[1m", # 加粗 + "GRAY": "\033[37m", # 灰色 + "WHITE": "\033[97m", # 白色 + "BLACK": "\033[30m", # 黑色 + "TRACE_LEVEL": "\033[1;90m", # 加粗深灰色 + "DEBUG_LEVEL": "\033[1;36m", # 加粗青色 + "INFO_LEVEL": "\033[1;32m", # 加粗绿色 + "WARNING_LEVEL": "\033[1;33m", # 加粗黄色 + "ERROR_LEVEL": "\033[1;31m", # 加粗红色 + "CRITICAL_LEVEL": "\033[1;35m", # 加粗紫色 + "TRACE_TEXT": "\033[90m", # 深灰色 + "DEBUG_TEXT": "\033[37m", # 灰色 + "INFO_TEXT": "\033[97m", # 白色 + "WARNING_TEXT": "\033[33m", # 黄色 + "ERROR_TEXT": "\033[31m", # 红色 + "CRITICAL_TEXT": "\033[35m", # 紫色 + "DATE": "\033[37m", # 日期始终使用灰色 + } + + def __init__(self, use_colors=True): + super().__init__() + # 强制启用颜色 + self.use_colors = use_colors + + def format(self, record): + # 检查是否有自定义堆栈信息 + if hasattr(record, "custom_stack_info") and record.custom_stack_info: # type: ignore + r = cast(CustomRecord, record) + frame_info = r.custom_stack_info + record.filename = frame_info[0] + record.lineno = frame_info[1] + record.funcName = frame_info[2] + if len(frame_info) > 3: + record.name = frame_info[3] + if not self.use_colors: + return self._format_basic(record) + + level_color = self.COLORS.get(f"{record.levelname}_LEVEL", self.COLORS["WHITE"]) + text_color = self.COLORS.get(f"{record.levelname}_TEXT", self.COLORS["WHITE"]) + date_color = self.COLORS["DATE"] + reset = self.COLORS["RESET"] + + # 日期格式 + datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]" + + # 模块和函数信息 + filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名) + if "/" in filename: + filename = filename.split("/")[-1] + module_path = f"{record.name}.{filename}" + func_line = f"{record.funcName}:{record.lineno}" + right_info = f" [{func_line}] [{module_path}]" + + # 主要消息 + main_msg = record.getMessage() + + # 构建基本消息格式 + formatted_message = ( + f"{date_color}{datetime_str}{reset} " + f"{level_color}[{record.levelname}]{reset} " + f"{text_color}{main_msg}" + f"{date_color}{right_info}{reset}" + ) + + # 处理异常信息 + if record.exc_info: + exc_text = self.formatException(record.exc_info) + if formatted_message[-1:] != "\n": + formatted_message = formatted_message + "\n" + formatted_message = formatted_message + text_color + exc_text + reset + elif record.stack_info: + if formatted_message[-1:] != "\n": + formatted_message = formatted_message + "\n" + formatted_message = formatted_message + text_color + self.formatStack(record.stack_info) + reset + + return formatted_message + + def _format_basic(self, record): + """基本格式化,不包含颜色""" + datetime_str = datetime.fromtimestamp(record.created).strftime("%y-%m-%d [%H:%M:%S,%f")[:-3] + "]" + filename = record.filename.replace(".py", "").split("\\")[-1] # 提取文件名(不含路径和扩展名) + if "/" in filename: + filename = filename.split("/")[-1] + module_path = f"{record.name}.{filename}" + func_line = f"{record.funcName}:{record.lineno}" + right_info = f" [{func_line}] [{module_path}]" + + formatted_message = f"{datetime_str} [{record.levelname}] {record.getMessage()}{right_info}" + + if record.exc_info: + exc_text = self.formatException(record.exc_info) + if formatted_message[-1:] != "\n": + formatted_message = formatted_message + "\n" + formatted_message = formatted_message + exc_text + elif record.stack_info: + if formatted_message[-1:] != "\n": + formatted_message = formatted_message + "\n" + formatted_message = formatted_message + self.formatStack(record.stack_info) + + return formatted_message + + def formatException(self, exc_info): + """重写异常格式化,确保异常信息保持正确的格式和颜色""" + # 获取标准的异常格式化文本 + formatted_exc = super().formatException(exc_info) + return formatted_exc + + +# 配置日志处理器 +def configure_logger(loglevel=None, working_dir=None): + """配置日志记录器 + + Args: + loglevel: 日志级别,可以是字符串('TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL') + 或logging模块的常量(如logging.DEBUG)或TRACE_LEVEL + """ + # 获取根日志记录器 + root_logger = logging.getLogger() + root_logger.setLevel(TRACE_LEVEL) + # 设置日志级别 + numeric_level = logging.DEBUG + if loglevel is not None: + if isinstance(loglevel, str): + # 将字符串转换为logging级别 + if loglevel.upper() == "TRACE": + numeric_level = TRACE_LEVEL + else: + numeric_level = getattr(logging, loglevel.upper(), None) + if not isinstance(numeric_level, int): + print(f"警告: 无效的日志级别 '{loglevel}',使用默认级别 DEBUG") + else: + numeric_level = loglevel + + # 移除已存在的处理器 + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 创建控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(numeric_level) # 使用与根记录器相同的级别 + + # 使用自定义的颜色格式化器 + color_formatter = ColoredFormatter() + console_handler.setFormatter(color_formatter) + + # 添加处理器到根日志记录器 + root_logger.addHandler(console_handler) + + # 如果指定了工作目录,添加文件处理器 + if working_dir is not None: + logs_dir = os.path.join(working_dir, "logs") + os.makedirs(logs_dir, exist_ok=True) + + # 生成日志文件名:日期 时间.log + log_filename = datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".log" + log_filepath = os.path.join(logs_dir, log_filename) + + # 创建文件处理器 + file_handler = logging.FileHandler(log_filepath, encoding="utf-8") + file_handler.setLevel(TRACE_LEVEL) + + # 使用不带颜色的格式化器 + file_formatter = ColoredFormatter(use_colors=False) + file_handler.setFormatter(file_formatter) + + root_logger.addHandler(file_handler) + + logging.getLogger("asyncio").setLevel(logging.INFO) + logging.getLogger("urllib3").setLevel(logging.INFO) + + + +# 配置日志系统 +configure_logger() + +# 获取日志记录器 +logger = logging.getLogger(__name__) + + +# 获取调用栈信息的工具函数 +def _get_caller_info(stack_level=0) -> Tuple[str, int, str, str]: + """ + 获取调用者的信息 + + Args: + stack_level: 堆栈回溯的级别,0表示当前函数,1表示调用者,依此类推 + + Returns: + (filename, line_number, function_name, module_name) 元组 + """ + # 堆栈级别需要加3: + # +1 因为这个函数本身占一层 + # +1 因为日志函数(debug, info等)占一层 + # +1 因为下面调用 inspect.stack() 也占一层 + frame = inspect.currentframe() + try: + # 跳过适当的堆栈帧 + for _ in range(stack_level + 3): + if frame and frame.f_back: + frame = frame.f_back + else: + break + + if frame: + filename = frame.f_code.co_filename if frame.f_code else "unknown" + line_number = frame.f_lineno if hasattr(frame, "f_lineno") else 0 + function_name = frame.f_code.co_name if frame.f_code else "unknown" + + # 获取模块名称 + module_name = "unknown" + if frame.f_globals and "__name__" in frame.f_globals: + module_name = frame.f_globals["__name__"].rsplit(".", 1)[0] + + return (filename, line_number, function_name, module_name) + return ("unknown", 0, "unknown", "unknown") + finally: + del frame # 避免循环引用 + + +# 便捷日志记录函数 +def debug(msg, *args, stack_level=0, **kwargs): + """ + 记录DEBUG级别日志 + + Args: + msg: 日志消息 + stack_level: 堆栈回溯级别,用于定位日志的实际调用位置 + *args, **kwargs: 传递给logger.debug的其他参数 + """ + # 获取调用者信息 + if stack_level > 0: + caller_info = _get_caller_info(stack_level) + extra = kwargs.get("extra", {}) + extra["custom_stack_info"] = caller_info + kwargs["extra"] = extra + logger.debug(msg, *args, **kwargs) + + +def info(msg, *args, stack_level=0, **kwargs): + """ + 记录INFO级别日志 + + Args: + msg: 日志消息 + stack_level: 堆栈回溯级别,用于定位日志的实际调用位置 + *args, **kwargs: 传递给logger.info的其他参数 + """ + if stack_level > 0: + caller_info = _get_caller_info(stack_level) + extra = kwargs.get("extra", {}) + extra["custom_stack_info"] = caller_info + kwargs["extra"] = extra + logger.info(msg, *args, **kwargs) + + +def warning(msg, *args, stack_level=0, **kwargs): + """ + 记录WARNING级别日志 + + Args: + msg: 日志消息 + stack_level: 堆栈回溯级别,用于定位日志的实际调用位置 + *args, **kwargs: 传递给logger.warning的其他参数 + """ + if stack_level > 0: + caller_info = _get_caller_info(stack_level) + extra = kwargs.get("extra", {}) + extra["custom_stack_info"] = caller_info + kwargs["extra"] = extra + logger.warning(msg, *args, **kwargs) + + +def error(msg, *args, stack_level=0, **kwargs): + """ + 记录ERROR级别日志 + + Args: + msg: 日志消息 + stack_level: 堆栈回溯级别,用于定位日志的实际调用位置 + *args, **kwargs: 传递给logger.error的其他参数 + """ + if stack_level > 0: + caller_info = _get_caller_info(stack_level) + extra = kwargs.get("extra", {}) + extra["custom_stack_info"] = caller_info + kwargs["extra"] = extra + logger.error(msg, *args, **kwargs) + + +def critical(msg, *args, stack_level=0, **kwargs): + """ + 记录CRITICAL级别日志 + + Args: + msg: 日志消息 + stack_level: 堆栈回溯级别,用于定位日志的实际调用位置 + *args, **kwargs: 传递给logger.critical的其他参数 + """ + if stack_level > 0: + caller_info = _get_caller_info(stack_level) + extra = kwargs.get("extra", {}) + extra["custom_stack_info"] = caller_info + kwargs["extra"] = extra + logger.critical(msg, *args, **kwargs) + + +def trace(msg, *args, stack_level=0, **kwargs): + """ + 记录TRACE级别日志(比DEBUG级别更低) + + Args: + msg: 日志消息 + stack_level: 堆栈回溯级别,用于定位日志的实际调用位置 + *args, **kwargs: 传递给logger.log的其他参数 + """ + if stack_level > 0: + caller_info = _get_caller_info(stack_level) + extra = kwargs.get("extra", {}) + extra["custom_stack_info"] = caller_info + kwargs["extra"] = extra + logger.log(TRACE_LEVEL, msg, *args, **kwargs) + + +logger.trace = trace + +# 测试日志输出(如果直接运行此文件) +if __name__ == "__main__": + print("测试不同日志级别的颜色输出:") + trace("这是一条跟踪日志 (TRACE级别显示为深灰色,其他文本也为深灰色)") + debug("这是一条调试日志 (DEBUG级别显示为蓝色,其他文本为灰色)") + info("这是一条信息日志 (INFO级别显示为绿色,其他文本为白色)") + warning("这是一条警告日志 (WARNING级别显示为黄色,其他文本也为黄色)") + error("这是一条错误日志 (ERROR级别显示为红色,其他文本也为红色)") + critical("这是一条严重错误日志 (CRITICAL级别显示为紫色,其他文本也为紫色)") + # 测试异常输出 + try: + 1 / 0 + except Exception as e: + error(f"发生错误: {e}", exc_info=True) diff --git a/unilabos/utils/log.py b/unilabos/utils/log.py index cee3269b..f10bd518 100644 --- a/unilabos/utils/log.py +++ b/unilabos/utils/log.py @@ -191,6 +191,21 @@ def configure_logger(loglevel=None, working_dir=None): # 添加处理器到根日志记录器 root_logger.addHandler(console_handler) + + # 降低第三方库的日志级别,避免过多输出 + # pymodbus 库的日志太详细,设置为 WARNING + logging.getLogger('pymodbus').setLevel(logging.WARNING) + logging.getLogger('pymodbus.logging').setLevel(logging.WARNING) + logging.getLogger('pymodbus.logging.base').setLevel(logging.WARNING) + logging.getLogger('pymodbus.logging.decoders').setLevel(logging.WARNING) + + # websockets 库的日志输出较多,设置为 WARNING + logging.getLogger('websockets').setLevel(logging.WARNING) + logging.getLogger('websockets.client').setLevel(logging.WARNING) + logging.getLogger('websockets.server').setLevel(logging.WARNING) + + # ROS 节点的状态更新日志过于频繁,设置为 INFO + logging.getLogger('unilabos.ros.nodes.presets.host_node').setLevel(logging.INFO) # 如果指定了工作目录,添加文件处理器 if working_dir is not None: