From e517c7e5e400f47a23775c5853c38bb9142055b9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 8 Apr 2026 05:28:15 +0200 Subject: [PATCH 01/11] doc: add missing image to device discovery section Signed-off-by: Joachim Wiberg --- doc/img/windows-ipv6-ping-reply.png | Bin 0 -> 117993 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/img/windows-ipv6-ping-reply.png diff --git a/doc/img/windows-ipv6-ping-reply.png b/doc/img/windows-ipv6-ping-reply.png new file mode 100644 index 0000000000000000000000000000000000000000..2cb07ac374b453a08e4805328f15beec017889b9 GIT binary patch literal 117993 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

NJ)+P3;H

w9|xRa{X z;*`6`8)*Z@GJ}}Z&lM;epX~CkreO5?NlK+cPFz<2?_`&8^|Kz5jtlpP{knT#=K$ER zxsRu-V+Ivye_|okH-Nn)f`j=CB40`J1S)ZD&cRuzY^c2vLL6rVswH}#!lH5?AMV!P z_Bzww#qxDX`q-qHRa(${3kF9^@Wcw~%NV&D_+k~sjcqgM4N7V5#;SXgQTZNfy?AjI zdE6wART_pZy~+EX3wJqZi+0kV5sY+S?H>hglk1kUn)xzDIum??VtX20NuZJz1%{pj z|nr3xlQEG5#HI^2fA|JR8em}@DLAK@N^p*gnCwL=Y`vU zBjK)ZoWq$Ba?&H$WbIrkubG=qeN}$VlgG52NvF>jFo4;KkF~ zb;9qN$d65u4&#nfHO*ox!nd|n31qphRw>DK`F5S}jc>4?6PDI> z5);>ib@7@QLDfc|zPk3jduUs+j00o~$IAR&P~w+*8zkNUhvmD^GMHB{uUlfGH$7H_ zdSiGGiV{9Nx%uJiZ~e(U4mV$tyDfW{&z!&Y7ADeeOQgKsvlfCKpTs7QrJUn0_Hy4K zY=?Eqh9zf-qlB!OQb#~SuRDXwrnfX3qFQ;LQ*KFfqu%M5Y+7yqnN$3IjH4_|=sA75 zV>q(cd;wR0R+o^QpkVC8(9>t*nOFq9fabE(L$H%7dW}hdV0~vD^W>VPXLc`a&vlgA zR<(T6S#Gqx>Dp92(28`|S|rPRb#l<0pYk+Tn-KruA7u!8*fg4b4m85wzTFmpUR`rkCz@hlt0*kggnPSOBS}~LGP0uYI4qzdL zQJou9;)&-*kiDPYKhHCSH-3yf!=EBk@DHu)PIz)1{$lp}$NJ#ReyV8d&8E+(+{Ix;Kg?>q zQnq?3m=N!qU_0k*tZ*2Cxp2v=HhbY#S=ItiI^NM^nQQl_2#~*(;?Lv8Wd)0sE1PNE zMpVs4S)K_P-wNJiCMPt2^N@Lf!AI}e!y1CR^9%7s-H#T`n!!l+l_uL{2l!r8ggfc? zk@bZ4%{4X%$D{p)OG;o`$I1~cpWT(1n;oMeR#(pA8|m}&w$8tv&QPd;@6j?kzn1x* zgl`x%$bLNaX%texvV~Sz4OQ75`z_sTtd?vWQ7QsEu#n_#Z7OhN7EqY&Ru)$4Qsi9d z`a`byC){gtu9ZZ9uGc;(>7918c>eG3-s(Q&0~w+V=hJf)jNSbmB7ah9H={T+eaX5) zr|!%O0)9qSROQ3X;Ed=pF1hA1n0z?qfEeMK4EXdO#Z&w1Au~-~C{B_gJR}3G{n$z# z%0iyFf_uOdR%AHK$FXzN$JAY5ja&QuI}f3@DfG#(OQHoeS;4{gk(0MIaPwe-UCW^p zR%&n+ISJyujC|0>gMdcPcPz8mmftK*B}P)$cUemsL$P|++c1CD_IWtDJkID17#ql8 zylmiFcKvQd7efOVUK;(BU#x5uLz z^2sD1i0VP>fFJ&>IhT7m9%WOE5z#_l%NHbc{0;NQmhYrnNT@x$zD&5B>MM#Vc_TP0 zLPus?v57tFY|jq|c?puqJ>(tm{^EUGyBRSR>;2<>Ki5&D?e$p%J~^s(7S~)%=jFhU zd^qurm&1g5fyEInpwK?zPm6)^^ufeaP>B}3!=Xk9Vm+&3ccsEXPFv@zn)Ciorz$yo zuci=9VGFxFJBNfb``wKvJ+!1uF#JK32PjU18BZKprL~dEuc$Dk7G5B7AT#?|jm|wD zxpC!#%YB`Vujo-wklzB0EynNzES{}LTl*|0+|w9buUr-n)|<0$p8K$q6GsL{ZQ9_D^|y3erelJcg8~yUG`8UCL8C_KkNujKkN3lBl*m9I^+( zp;w4ySD(1~Jv^Nt;qWYd~jnwyFD8Xtz&!7Zk{Dn)FUF zjMr_q$`t+04kOI?(noqy-SD@7sye_CO7Gb!dKNrEY*bP4k+$q!Ttt(EhKz$zmHBb} z#bHx`^I_DP`uuL${le7ePFNF73{^@F~$wXhcQv-(O$!@p-snUj7TcB=w(3wPHIJ`}#U?~GW9 zjpy#}rqmRoU`HOVcIn&$1_bjQVyT+euQZdn5JdeMRFC~)tw01z@Dxr^NlKsO0P2)H z6IJ2H$xqL%Ikjd@j2S$uRi`3)&|vZdBh{S)BCyf&L%Fi9F$*(k6Y4UUdT|-*y%mAe97?$^fXxa zCQR~Km&|w8v?o)mPbKVC8NtzNA@EZ%lklmBTwgspi8LtYcwDS=kC*)?Mh&yEp*LAR zejhYZPjPH6D`f{yQWC@X<)NN%jiof9L)-SSpK}B+ByJp5^!F0cr@tg(#X)t(0%)#W zP%ApnhVRyH)~Q_()>_kDRg-{hM9XGLF@8R~O;#n}^#hzu==X?@xn~ZuAqLc?bg)Pe z!m@!)j-qDU&*nN^^wk<3A9Ynw9)U;jYsNVJ19S|qsAtn;DLOY?mbNOV* z7VV*9_sGn{Snl_+{q55^Sv}}nps{2zu6KRM!G6d|PbVjM-CdPD>iMGNg?@3CiIQ6dEC3Y= z7kC2pQMR=E)BSJquz4>0Qy3xyn-M+`pfsB|W5*|rytW`J%pOP|zCpv_+tgFTJTD)e z8q{h|6Poo#;=XwU*=Mh*To_oGWuGE%qoQ4Ng~+G|WxlD6das|B8$BtNv@)IY%UHi{ z<0Uh`8^ZaM^F+GM$AJ`a5e+xeH@I3fu*PY^g(X=Y445G$Kb9}ta8Hewh;S@wI_#@L zc5v~j5|xGyj@^>KQD`;UoV0##Ld@OA7ZjT--L@L)-kI)?LVL18F$(QDOjrtdBOr!& zo;0o@`V;bszaikm8FsQdi6nY!ok{i2__pf^Giz{wd{e-HE(cD<0x}C$4qMYtK{zGlS*=`lirEmfdV+NpTz&7&=lu?bC zQeXOT_A@g&6|(y-8obR7eyDrrB@Hy8Yh9fpk;%a3h=#l}4KmOv*gdPSOXSn+vJrC} zcCw!r=pRnw{1f3nu^v`M#Ysk9y~V_p?K0nHH@2hmr1>2)hR4@lrtU#V&P>AYhSqla z&tEyxWKutXA+Df%`1P_ftjbpdd+1pwN5W3kzW5C0YaTn5WFcn#kX;H(^=GYl0(vhU zx8dnJ>{}DKVTS?4eQC5J47O47>3B#LE2N`Xob+9{5>j~)(c&qiDscC~{iF^YPLj!S z!ftpsh0T+?wvm$9?)bXw>@x31qCcY-73dT2AuU8vbYpjJjivy|S=pV|hbSLdt=3bNYi22ZXN2l^GOmot&VxYH6%x8pgpkbTw0DMEi*Dt_{H zZUTO>be6Dlx`R!u(U-blh4c#sX7cGaF%%UA^3DWe=~7UbnE1@p(N$Y+(zkSe`%S@_ z7spq@2zhQ4`3$HSNh~WH915bqe z*nJyB&5CO~(hnAPOtW5hezAYDU0tBI&v+;u*a^_USb@iLwoYDr#Nv2}rX~tQEq$FE z8D{Xg^}6`ivPib;jN*Iy;GZz9kl<37R&YMdGH|#AcqEpnvBX>*O_8ccuXWat!Oe}f zrSFwr8C2Ak57(|y*9}xTOz#g6fp9d33=;#Jc$Pm6%;y~x*mQsK#{7Bm{z8dfoVkR>Z+c7OLl z>>EKUhicdMEouM{KF?o#74x9gr&#IyD>=cd0xYzT8P&2z*c6TW=ZY-nC|pOwhIyHiwG9&jqUse@ZqeXUDmJU_Rjqx6%OC!E9#^a3v+|>AEDc}D z_lUoxvh!xVSRyH`qSu1I-FAiwe0f?FLHwz;)?9j!%JKdNJuxF@nB<)B)2i5uUMmNRvjX?*=yzGB^~xbDceF%YgFC!Z&2wYWE#i#N$^lCnefnVU)v2*i&`nS~5Hf zI;~JQv@_Nbfy}VpT1IdyEiB;9?z-nl+d&o&kZL7*6i*KuhP)|B26KD*b(IM}V(HIk zSx+9uNU>H+>G81jf~nmmMi;Q-YJ;V!^Wm!{E{q#2_awam&765zfLNOL_(k5#^Thr6 zZN%q)fPaViux4(JsMU{u8&UPKZ2<1qi{<>8_tk{_uFcubO(n^k1IT#`G8^HxF|!4^ z^)c*eFs_jI<8fybW)kXtUA{YFJCf1hrNZ_0KSZScF9fpcyadAow)0!Vg(=&G?cd zCHu))#H2P`-QBZnU@vs~W@EvopnZ2-)8fG@p6a5#s?~e&+uTP6=^vEPQ2c_q{DnG4{ad*oX`raaokl{LDxdOlcHRN(l!q(sKCBA#T6>9Oq35!c zmIG!}Xd@zqHsBritI_r@mmaZ1L$yl925-Tev4PW_^~+ zQb2i_T-?s-94EBk>L0N^9_?DIO}rH7!+3q$wVda}VNvbJ{tPoA{81mPrJ$P7i5B9Q z-yNsU#&<+=_uirw1zaCK-c-+=T6bLxJI_(ZQ*7|?5F!@Of z;4U9|62_U$hi|{nWSO0G(`|$1Jvow~st`ULfnN)R^sdgium`bKlF;7d>e--|WIF1|OXJCWon|os zD*#K3gJrh)QjA_Cw)HhL+Yj82>S<;D-QjS_Cm;!qvZPkut`I)OVnsOEV6ofBi;$T} z_`!h*B2XKu)^1!h`6PP;i>FSEdm$Q(+~Ej(Q<=)QxMfKT4=9wAZ_zt=u-u$1_PRp} z90z=s2lVd5ehi+q;hGDA-)j79cItc^v8wu1O?E6xY*PVlxOw508;)PLj@f#jR(<2{ z$+q6hv}I{_jagOq+}>dApF(_xE4}v*)U(scZUtA!@tMc?+Sna6Ma1!L!NIS>o+twP;w{qfVk2a@V5AV_q6H zera+ST#Tef`qg4+on+D|P)9Ry*QjuY^~4S3cCIHE-!cqThAu`jD2VWA8}bHb@l-vL zggSmjLwh|-nL?j1MRt{67@nQ9Ty(J?Aoc>k93{4>hz}zM+T)S4WWnl&Ax6WyW5)ZU zy+6&7hUH_IjRc`V)F&s@e%1*RHtqgyXHv#kQO@$=|>(c+6 zPw4gF=9`&3h;)eNMEh31I;R!XX{BZJurLuyf4lK+W}DmS`W4- zw6d^2c0Ow+!wn`TI}f)eInN((CZD-Yt->xY20I*3Pc6J>LsA0H(yg6kUFrs2I>V}) zGi)N_JqI`+f}TZFuF>@hE+nr`sA$k|CfMZj9UFzjhyILum8yIQhz#fQ=9Nxz(wVos z)YY_a0_15dgX!xoRgW9l`t869Atju#D!u5F=uU-66{ach!2n+NqZcV^9_RSbp8NGP z3Xr_Qv5LBZRk-y#3Si~5A)hO_+9!9uh&f4`z7;g@bYIHugdRp@ur|;%LosojrYCB_ zms}x>!4=YYGZj5G-%Ceq=~6OXDw5{p91s0qplk10EE)H0rR5ifVmFNv4s1BKqbH9o zknn|tLrN44IXB!sz{mLew}2e`XvW0R&?owL$Ws%jGI*PmLOkro{3qCb=|oj5wd^P> z&jr2v>mM=YVXnC~dqxoejPVf5JNn^L6IZLO0lEVdJbpE3Qw^P`-*qWM-0^evwZ_b| z?Vq=hv2m&L`7F*=KGwvWP;ARO!Ui&SRt(mls=%JUA zysdl#ccp^krh;WyK^4@Odf3!1 z?<3XntBOB3<1gP0nhu&$pZ^6KKV345C8U{hJ3}83Q1c```co$BUURo^4*LnDT8Fdw zt2*kmNV$oUc(mdtkM?uf;k5Z0KQ6M|j|NiJGD6rn)YfRv$1-YrP@T85!osilJ!^!I zSAx|>*nR8Lgk=8IR9(L8*gft`xsju2x^YkVBEexUCD+|s>nu`Dr`=OKtyK-_$3mV~vzV50e#%UJP?8O6G_7bI2H_I%SYpOXmu62W^ND zG6aoP5laPySj}tTO3700yWS*4>bo^JBKEez7x<)yX4fI@RW=`HCE2crX(37&?%wS%s$ec@%H=H40Z4kI3K zf&m1BcYg6F)gBMSWeGJ?8{0p@6e8?Z6UF{hMho4QUVk~tZ^v4?S27c9 zUY|vwO^X7m`JI9;&X>${+qPRjgfZuyrt7D{cP$@rvmPu171$wXGtM>~t%v(>`0Z@{?nd#V1%aKcY-Ih~G9f3d$N z2eW1IUpZKtaLRP0ujuXeg}lAhoLEBUnxpHS=NVi>Kaavj+(gS>Tgwp-b`;+*8KAfM zRL2g$>TM)mK6+rMe?raSVGO zF7NDNDH;MP+gRZ;qNA?$2KDD3qam~-@+PNfF>Ibo3OD2IE?re zkLXU@O-yspy`DyaqokJKFMQCYuz970g<%=xtPoQkAbQmwMPSluh7<(=%5h&v`=+SG zR3=xGHT(lBGOPfg{eIpLx#N`?Uju)j5{-O<@ZEWXkX?SD&2VoMkh{7k>++0u;)l~X zV8ZoA2NdJH43cVmzg@V`%Sw73x5hW7eNw{~Vp^P#G+SdfKlnD2lma6zqNbp$G6g8C zY42v?(Wx_Sp184hFq0ka$H)EvR-+#MzJ0w2d+ioWhMXc189J8MO;|Yc9$zL}m8j1F zRPr+AKT*kbR)0{*wwZrY$-YCZK(V@@r6b(%0+^?`6Tde*Uj$=o=1HlN*ucd7*wnmZ5M8!f`_iJN3Rm#ag27bi&`*v^8< zK1UFGn&VKOh(OZd2+!?DZQlQQtcfi?YF{LDu>Etm*C-P3hQJ+42r#%8RX3hSTxzC$ z%k41sc%TBNniB4vJ#NuXLLZ;sqF8J)cyp0g-6(GI><2luaALy%JUJF;C5XWNSdyl? zv`|;GSK4XA>(2ARvIoBW_+iFmn%tSuu2<-TUHVQHoOEIGN$tUtv)7}&%)GXCT_d)$ zauIo`TU7eGRYP|xytBufmOzEe71w8_T~sS&119aGPT>ZkQs*lNKrC9CijJ@-Z)){O zTVouk!DVC%4A+r03^Kl7*RB-AHpex}`T?u_KwrfH!Ib)M+azLAH_# z_VX7)P2++%zuGYT zOG0#=g4~v0y2#*tyT|m2kyIgliUIZRa%~VlHrt>VO5MDPvpo45OooOjL|LgXkTBOd zV=#r1$v?S#)#x<~?Y$a`8+z)b# zSq%Y2(PLfd&=5#tr#rd;%H2gaaA8KO2{(+455!J(hHV)Y-1Eqh?Nqx#w~i82Qoifb z2tkZf_RdmQg-#e6>FSx-ioB~qkS(o7Xp(4LPVrougZ+5GWnl+wfw~$ycVpxFiR6}e zh}kG&3H>#jjWIUrs&9fKtxrx!R?rg^qdpw1tu^EoT-tkcPnkc7?nWncZliVP&rmZB z9ppDe2k~bM5`7NrNtxJz=2vop2FvH$UY*<{FdciMM#-eUYo z-&p~rup=+wdL#rH>{3;Ld{|aYxq_Z!t(L4qf9y56^5sC?G=y#3`HgiqQIdT#;i|$H z%@Rz8a{(xPI3gKDEgKC-e3YDebSWzTLeY{QbwD#cmYl?R3ryl*DuMMrud#qrZ4uN2DWpe-9yjk2jfT z^zk9`2!v!{vy^Ug3&8{C9{~cHo9TZDPWgeBOo5hui39(&cm8LSNQ{1C$n64ro6(LZ}AC%ZHSH_m|)8p4sSso!!6v z!&H887F(m&s1ZOU(Hm;lcRpbt|M~fauPK}LZMoOkYl&D{g*R+O?+1iF{e2X1!1vln zEq~`0F8PlQ_|I?EcmcZE`b*x<2fsW1uXg>v{3BcjV41~J?{HxMSFU5bvs)DV%rW%Q zZR+g*{D(9F=-F_AyJqD7IZFPw06IhiG@yqYgw%htsQnPm1DWZ$*RAZJjTKky)#+#Ckqg?o!Z^-L#@r39~VUPA7_wiF?(^Fs$Qa=3qUTArU@*D3fW>xuy3$Q^*Xc!(vB-41dIj*`_ zduU;g_)b33t!z7e3f4}UKUZg^l}TnQawjiKuUDG241%q1FyuXk^`CQ=_c`qZA8NC5 zZYXhRPVl%2rkRV%1N}A-(Gm$LaNnRWv4*lEp3TSh*MA%Ic_2=1N*E0ooG!o=>Po}hxkmD${n{hk*&eusTd_lh-Ew{q{Sx;wwe&$T#W)%*JL@%We_ zoh0n%s|xt!L|0FmF&0FfLOfMWYCniCef<2(cG;#cH{@?Lnrou}o@m;2#zpUbd+b5j z+!B~{7|{y?_*G{Fns_b#7BL%XZ!8?ipCU2s?Uu42rI; zl`WzD1e4S~@-a4MRu3>>hdtxm_FGJ#v4J|abp8^0GG4cyl>n8oKWS+viJl2=M zOr1q&UkID7Dvt!${#dE!pDAc2io?znE(GSyZ0pp}`{l6Zt66Wj)fGrt7J6z#33u^- zE9YuHmWcIIRZE^})gz1#4D)(7qnPRMz}}Ky6!Dg{`#0{~mI=4t?E3{g|1@K8ETn^3 ze{+Skb4NFAFP^O2>_XPl?x;E!^4R~W9!3<>@+Is>FYSI09jv6s;*yi{!11*Z_2nx6 ztU&jgvcS`;^|emZcjwwSP-@o1n1gPQSydaTwlWHk+|5Dz8hGezccpjZ^rwLsOK(+E zkFL>mS4?e#K>1agG_+#!g|AeacjAa4q*!;$ea`D=hln+I9>95A%=;IUd=`$0dtoFQ zlJ_+&5r6`UoM6#$KqZQkEEinu*yk85{3rSRrhGnjw9V14Km z`f#NiRXb-|KQ9~60O~QAz~0_{9cN~T9q3e!s7{ZR*GK>5N|xRNU=q%|*x{bh`L>#p zS|@JDq19O5tH`6^)n8|;YK^W4QAzh6W!x@_-ou-OEzWEu@ZY2edEFd22vcAQ0NbuL z9RQpAG(%`%yLM&&;3`h~xp^fyBhT!XDCcGNy+}Aw*p)Ey59nD`Mxw&e|3TQw7Yp%n zoiE!oRlnm~Z`{_2?`liEGW4*HbF$_w?4C%a z7W?9jbahbw7w^2*KsQnJdy#t}fFb&+Ad-Xrpbh@$tj>nTVMzymDx^VT=nd&hL2jCR z79M6++~j$|&Vt3!1pVURiVDT)GpQ_-aE-7;K*T+7T(-}6S9Py~&e2^!Z~+~gZ{v3c zcwv`d?Yh#I-Pvb%zAIa2QKWocs%`nRfAJWq^6whPblgcHko>0NSWrbwA#OeSE^axn zN2A`;_|LN^cZ9hbeJ}m2`^-l%6&OT@xNUkzJ$!34oLsad%lf~KTFY`(z3NFVD?&HT z5%(6@eynDDveVguTBU5$CjdJ%)1p|X6cHq<6MZX7;atT_@Lp-kD2+dn_QBf;@%gRa z8snF0b3ZivCXkZHlI^(PRq`4+pv5x}|DiE;7h590|75H54TPvB4*wgn_qtf~UW*U}&Sag6ekKhw73RLV?#CW+ zgkVv#pS=liL%V2`QA4b;j9~xT^H zww6NHb!UjI<3v>JT>@x!d!Pb+mL>FoMvhDOPiM&wH+P;1#$o2CNP|w@t-I~@+I$pu zk(c8w6t=A|mF_xXe4TyaPNBECPsv_!ZP_I5bolNU*en;+au1jp;|>B~v&m13qb<3G2ky&LJGv^#lFRr%x83 z^3Fb|`e4U&EM6eoXyLM6AomOd|8=3dEoRENL#$5_Kn=oFfKN zw&Ply0yaJHMD;nQVik9_xgd;UekSawZaZho;2!r>8`Wm4jy*J{QvZ-s zXfBs?pZT|~*y~?y#s7h3Zn{rks(Kanw1*>lqSR;f3ig^S@%+J&ThGnle0`ISqqb|R z=k-HR#+eAQ&U9Cck#r7fVAm5vyf2)Kv2)+qDIY!hC=#*uitp|j6^bMd|CddqPLz9S zxn$GBtDhJjxFFdux+R9|y&UHED=YOPInfR?(49OXp76I zIsD$i31vAw!F|z0PK!?%LcieRy%}-X#T<**GkcJf7Mp2I%~zDz-WfFGRl28GIqBix zTz1|Ofa1s&@LWH+r%ng8Vnu@DS)^q3Y_RQd{lfWVfg4~!*ihvkicJq|q$?GwH0ag1 zclIW_O1&g)K(ZB;SFVWCa@F;uVykNVXjTyMT)s@l*Yk<6Ug;*aWp1CRM2UgmJe~BB z*~EJ%LUFGTlt-B1jFptl0Z@3lK0IvYHGj}Fd3%Pv2+imtb4(n`7oJZS_I$xj3rr($ zRk~HpY}|)R%ud7llazMrD2E1(1S7{{NhwU%-!08A0c@H%@3>u}Ob0PEV6{+4D#GU# zoF3#^V{kZPGJO)tPSDPYHK+hK^?1c*Xecq5f^3I!Ke-2Cg zOm7gFl`M<>-IN2FOR`^k)x+k8sRB%ng|BacixXDM)S0zYur=G6^{va2X@gFseZ3)j zcZv68w(rVx1#9i$ntH5mG?j?aCmNp%SH{<%3sT9%#4~VrE`iABerx@t294q_Zg$53 z)t?z->;psRgzIH6_8ilBA1Xlw8R@CP7brF)i=s6L<6Qj0SOp2LI^Ywy=3B4x;pU|I z2f|)uA3|BUQ`d{@)3NiX?96xHdP}^tab?xi!XX<9(RXtWjl3`!v8_*76MEd+-+CwR z7a(>IhbPUTQ6Y+o*a$1?-Dmjc4e+!xHizMpsesQr5GlkHp?v7jbhoi?)MaJ#0p^a~H~USKejy*RMl%fNz> zEQERRiwqjvGZUvk3J|!f{ay8(6q4#jD|kAvyA&4Dwqf8(t>f5Cv;442zqwgm9{(;0 zDuJbc_ikxfZR~z1ev4#nl*WLy{!_uhs?Hzm6Ik7#GYh~elAPcFZn5AAp50qZDJ;ca zbN8^$k(x^N{c#fK&a!xPxp4tvB~~Rzp(e7VN$Oe%A=V1bE=d;mTOm4M&)jx=bBB@D zwspn$Oe7WC%35NfGmTL5cY9))c%^;`!tmt-$K61e?m)ay-PF<_nlFwib4hxiJTJ4d zO)#sYTg88s|u1=?l zvp&gGdwikvx!2vG7ZwpR-DN@?x-=tKTYJNUBe`XqN2yMGr4YI11&`&a;{$FTDOY+D z$?3&iQ*cihoQji#9I?eDWVGnz_NMTx@wrcy4&lz`Fx>-pv#s4egd?-FV$H8i=|Ff( z!ebZ+{hpN1bSIxq5xp6PEfn`V)HWK2)Q5J{GL3i^JDwsbgYvD%r}I6%^h>X)vXTz0 z_xfRGpTykp#xh!$T+?W@bEVjy|i5o^ldKcsK^et?qn7P}wIh11$W@Gwxb zL!&a(68XpZ%rHie{0C9}kQ%j5=toMJ*F%t3AG%Oku|bYf4s(49{r z83y)Ue#J~%qK+^{uZmEJ)_I*2H}hAO?6gd(CKy%*`dNejJ$(xmrLLT>>==m~^ScD%0pes_7^=ialg znanwp%;dwlj<$~f`mMDless*5gOsV8{iF?-w@EX`&KNARnr>DnJp4%X)?$9>d{D(I zo~BQ5K7g0Th!EBdn6JY^no3@4V=Z%k2=1ua&D@X;6Cg8p*&xcu8x7MFp_8n$`5mfHBuo-S47*45yX!gk$&=K`c|i-Y z_4U)b8b&5-tFy`lp-$BU3aWV@AwIWa?5-p{L!$L@`5kCr_U&?fGZmc1UYaSHd#I-Q zI6Gl!r!~xb=(K-#B6`gJD+=>eo9NZnZ`H&qcZ+tf>16hxTXKl zbxaQyuO=HVA=^rCGpsQEG=e>xrO}Af1-f->kr3%EwfEH;~$d~%pIf~<~we`sSJVL%E*0o)6Nu02k$d%|$z-Er~K=g*O zDcIygl3T*+%PzVcfk`J(yW=RVZ%6f8F0BaWr4ktFK=PuVqgAR<&j!YtD#;>60Ke_- zIWyuZJD4o&Y8Qq)CK#^v*dFLFzoS?a{i};AmBiZp-AP~g_lKTUZOb;$-e4;JhKwEq z2*|kmJJgLQ+QAOQ<0sqlC(KYq5=c%3?;%5_-n$M?j#Jc*dos^Ho@X<}24FEw$?$EO zRdlNwaB zea{VDa5D3t4o#Bb!#6C?+A}^nQU7}4%MR~iBjlmxp<_W1ob(RF&Nwalnd;bYREzIj z6tmaX`yK7GqLIODKMQ++O_Ge)Eu*W&mL&R!N376%C@N14#ojqu_0}nWjD3$2-}jxK zze=DiDje45!iwk9sVb}u_E({6Cy4p zq0`G7pONV+3C*F}tbE~FDdO9d=zm`4r}<^B;Z!087hS2*sa2L7`R3x)r!pb~O1K*C zh`F0o{TR`Ztqb+~wtA1*#m@x9PX?tD;3GH>ot+GVq{rz3`qS45V&FbzOU z<}b`z%3-*|>NtMmd(RQemyb>&+be!&*e=Me(iEnQr#7})KBmK4ZkFcAEddgrH>qiq zgB9<~l1X|~4XVm$J~DU{ttc8ZgdqnO7~49$`)KR?yT5H@0CH${QC;fRcBk#bThzggbe($f_oSkBhCi!0|Dmnd|IpSB_o=Y7wHc`NMGD!zY7cpm%dCf= zpdlpKNa-2klliv$iDbW+rXc}VhdsiJaB}Zo|DXeAq#<(i#NyUYg{M5$;3W}7c#tR+ zUn5b9$a5fr?f1%|^XsLB5BoJ*=&Ge2Ghln{*@s#Jb)SG2 zl!H7pc8b@*zQ#^I++s1p5jtyQD2GEWTvKzGk5ElF&f)o3Z$=(IfEg1-fiJSkujbM1 zH1iknzI!hMhGlr~4Tx1*nDcQKhsDxcKi>ibkmv2@7N+Fzq&{PGZG<6DFAtNtPrD61 zUJU$n^xdJ`DLEQG5w6u8jr5mw{0T7NnQ81xU%o%g56K7~eY`y$;&6=P!@c%uPP z{rqk!Nx#0%Oqu;W^IZJ{#K&V8U5npKv-*5VDa2_~{EpvKP80kJ%6 zvYED5*zd6(m&7=-A@^YTW3by-eQv0p&R70u*1$XU)5CK$BfWNA=`?74>5d2@53pquQ^u-c8Tfb(;H`nd$wK zWrp4uaklTo4R|vw8dGZiae*z(k%}ULy0;e2>&8~w>BX3tNiW!c%bol= zUOHt=hXIm8C494O+}BMW%ZJ}ZMy;K>Ox&i0cd5sg&Q&5_#O~pGqs&I@Bn!+%bSKtm z*?IHbbbqB?3f#!?m@>O6%0JM!YI#UND9=41SD5S0*7mTEHnFKpNB*a#7Bw5%djdN7 z_{E%=zpcOC`rF-;Q~c*3sSkDm7e)q^w738U8-o5+4F(?NNAo0DM%LdJocQ71q=)@& z=cW4v2C2g(YbAQo_ILge)=%+0<_!7DQ9zV)0Bs-ZXklLf>K%|@GR@c*-G3TuZoD^s zdQL8_U7E4(7G^KJgpdaUP3S40sYaz-me@A*&(08j4RfHXC za!S!77Q*fnzkbXaQnlebq-~mq;F>^NIs0+e+ho=lzul_8op^=t!M{IVwZ%BDuwbHA zzB%{|Vz45ojImoifbflsBPH0S!~Lf0%OEuc(K#eR44d|!x8!E+Lxsq&y~;xrL#zJH z0$^Pjn40McpiT7S8VCw~U|zYRu9mdX^XL-4rBEwTyxZa78|B9&wU6((pyQoY@27CI zH5P4ee9@HJ*DP&bZ;5Yv#s_~jxT$=Ga2eof+Zvkcb{sZ(DxlYz$xSYjX6M^Ogucj` z*)gV?JP!>bfGG_e51c0JvJTEDT)(MsQ6&}Elk9)S^d%7h#ST-`))pwK74fv!WxpqTqkb5*dbf%TFGaF<{ns}D2+aY(Zo>M^Yy@6lZq zI7wn}`j?CO>oNKl+k<@h1e`xRtum9FX~U7nxXCY*l8!B=-q!_fQD>fA?+)$=oBYA9 zF~agrBeP#m{04Mww{x%=Ha9=Fno~rW<5%0g>qqR4LS_mMxyy*Pm_r~x-1RjcVn-@`)JAP?I7Rpf*&>re<0(ziaHQ!UN0=G4@e0Q zB+={41@~)yF#i}=`$&5n4Y?@Ta!6TsAvEx{GW7_zQPUpAEr}nWx^E!)yi7?*#;94U z{uzW$(MC-8*SmWU?K0z{`1F}P?gQ#@`))gGmFch-DT}eDU2zDd0NPevBQ-%|V~pvL zm*e-4y>P!CApT;`hWVJVT}}f}tStRhm5}l?S}zJXPi=p7+e^xNCfK%OyDh=DA5c&C zv4@8TURreead|r`;|8v6JX1(u8Ro|8e5zvY)V`P3$YhO3=C5|~rYyJNfj;|v3~Q3p zS@a=qdV>d6@7jUNrINcw__{-&);LMt-d=*M9d^yN0$G4iEfKx>tpd#GJMl=e{BC|S z$zz*lOkRUHC7h(j+ONXJaanc8AetnrAgO&x=0U5p2Y=spis&wI1*f8J z=?g>`_6IF{y+S#B$=ehJZW=SVsOWJYi|?3p2+e;jJ&c_Q-8J0E3lOF(J<8(FOe=OJ z>A%+7svy`;<$fPv-I}c*ue=)Va1cuq)?DOQpJ4zUA$Weguo>4j+fKGux#8wBuWt+x zH?Wz%$U3-hyd*mSU@#E|#CvsxZvc;d1npE)v)H?uR<&WLpyYJnMz@_a$y7#Vr=pfG z0n=Uh0vN?gKl9Zk>E-?85J@tcQ4i0wrHY(JvZr&=A^1$k$w6dF(rW14HEruWQ6Mmy}V~DzBNL` z>x6nJ4D^{Oh}7@>GEF;WhEc85EJv%r#wGdbM22B@|8RWsQZ?_KX>VRyRM4Q+$OG}X zJ5EWW`R)IU4al|9{m>gWG0iP^i^K>hN& zUo@l8_q7c@Rt08G$Rzic&kf_k!ON`a>Iy6TDc}R&UyRLCp4jtE_{9~aGb9MTXb76m;jrJ@IUNEP|L#}~`KFdnP(+VTkOzWK zZ;4Tba_w_?tF~WEsW)J;YkWO^$>S^t8~Nb<>Xb`4DPE(uWjbijmT`l=X4B9qG2_u# zUUfz>i{;O`&J2A6J4_8i^l0n5JrXnRU%4!@q8e*waT{j^t$UsC!Nv)FW&tckeuFY~ z^>vl1hE8^3BLUQ1S$PnHrVmrjyL3vcr;*52mZavl%583(myDK&V>`|s%w zHV(h6?=}Awb|1@K<9>e^`}t&b(>K5L+Qpp8O*+#sXKPaQSl$t{H^dWj5qo9M$;3{k ze_{Y@DenO$A-C}LmR73zHzQqqA`rj7&iZJe=*te$2W`0|;O5f%+GOma$NrzDE43YD z+MUX?*NN&PSNX5`dC1CVZ5cHrX6DS*J?J!d_d{a@^uF=q{+V*o?uMojBN-vuY? zbfXOnx;luC@(QInAqH|7td9DpEb(l;?!kgp^wEDNSq<+5Hpif8**CA(ehaG7y8WB- zwPTHWWWo#dw#4PtXPL&An{5rZJ7p0L`9}IT;An{VwhZ>`>tBMvUsCPgK*|5{9Et6m zW~kkA(!h+IO~Cd=6?Un~|IJr^QkB67T>2cjwe28VgBclzr_nKH4;vr+g@2lI;c!Br zVFDNbnHblN?EJt`Yn3IVC0*V?t0_SIZ}9j2lQDnKnUlMdHkAzfgVH|@eXG5=#V$QB zW*PRdw^!!?9`98)5?gUPUCTwW{iV$Rgp9`` zWsJ+b_Osf{ITk5tM|2F=Q3h!DGSqq#yfwRYNZ=H6kdSa*`R+0 z-oiOP0ovfnFRcC{_Y1&|lsNd-nB;$YxBvFFJCT3Pj~t}t|A*A`3#@H3bZ>3{L+JYA zZno~hg=GZI|Kwc%`w{xDZ;_sID-5a5earnHBIj3s#_sB6FY*63(ErB@`p;k0Rd4kG zIVf*6|3l#3A3Js}{xs&ne=mjq*Cz)%cIuE5-CF!-waX2qv14b{5%EIqpUqtIKT{|4 z&Zqr01^%_N7N~KOH+~pS=sHE%E^7MvE`&-@K zE#h~E-Y95g_qABK748e(QApsHR{)eqq{I93lC8aXv?$!Ei7 z=eok6MREnhpS1D!BCiO$XZR7AJ%!#K6HxRPlUuWOQzg?AFBL1yd>5)wA%yPFa#>Mi zZWwfri0_H{KH7tRhzWi|m(iEM;Bmo{I2CZ_d;7FU%@o45?vEI8*bAv;BU{f*pomrk zV>ZZj*Fb04^^L1>zKJTdaLQ^qCT8bryyB}?yOBEC`=iiqz?|8w)dil|zJs`%xYOK? z^`nw|dca=2!=sHcWhwaXFuJnfoY12~dO-!3FL^bH1UyUk6g<})pEdlvsp+_#kC z4~{s_#==t#X2Au%LjzPS=EkdIZ=o8&#%h>{79IBjwfSlr*;1in`|uJ1{2M)yZz2sM z6MW|?#~*k$-ijVD>a9yQD5Dy&j{GdT8_mbj-ma>_M|LJB%);5sH=1>mX?jCAtl8dA z&N}5eL4WXObh0!jO9z};C>|`Bao|zW))jw%$QM|;1m#1&_5cS*pRxvRF)lzG`9I1`yc3x&kjnv$IcP55}S59TgPrFAZ zm!mv!jTuwh@ixJ0Kz;T^x55)sm*Sw|)5}V#~w3 zl}~pYemDDKGyRH#fABO;VpIrECI&aYGG<$me! zc$Y!ciBnOuU>4yBIUAvBlG3Swm%woLC^e#qvxhoQZa%9r?nMK(Ys8%Pm}!QM`$8(DvGMiUnwkV`p?|OTMs?LEnNP!e zlsIOkbMKWdSVujGlE81FudWP|^vo!#!<8-qw4Kj+3soLooNa(GoKbu?jC*dfU`BWY z*k8vvqQ~LG&%%DEm{GN!$D5ia?U<6&wLy)-=#BNaZj_Q+VL~$5e19|wQqq}|4(~8$ z&gxvU$}Te=>fe!9|B8;K)WJP&-T7J)kxi=d?N`yTCWI$_BDyz z%t1bmN;BBqqmQT|EO%2A{(SfQm~O;LwNU43`Nu=O2vIPPh9T^#LxfzBpJ9NvT7F>m}1w9y)5zC|DDBi?N-d5NTXiJnr zE%r^M_2#ztR-SA)MBmX`52~zab#OfBNcF!uMf}u2oLH7V!Q(qw@;2VCJuwp5R~s>8 z{X#jyHDT#8O4*)}8DKR!tX>ts{boSGH2lG_Vf!er!sul2D$upl?x@s__&9BHgP_-b zFZBDD(aE*7Z!m)1$nH_=Gy&UFy0-){))48yDEgVf;Jj0Cxr zH(D)Q))gPn+^bHafPW<^&`7;paA)mtqJ9BW=P?}$V;ZWwM6PmHK>E4_D{9vFH!i07 zs$U)D{~D?1r6e>xG02nEy#BH-U~`>qyxB{^$uk*9Y&EMl@lc_*b3oYs{fweh|BuhG z=qM+`I_wrtI$Igc!5q;8+@yO`3bw@rV`Pee4>x8R^2CEJK9?GpGp)8gX_gwe8nj6%2mFl6VF(0_7fJ6A=PBC+eeT+8SN+iHXyXGW!KG|0BM+ev9&Zo}%k~JzZ^v={6 z=@4J+j^!ANu^Z^jiaQ-k)i0y>?50o)Y?3{O*nI<=zJaYBevr=iatO8Amcc;2ILWe| zehM*vIMR?qs`Fj=f`p*k=i$->3|KZ+{n|8PSlV|Dw9`*Orqg8pI2V~6Ft)K#_(J}* z&FT7JVM*()Wl=gRP4LhdG3ct>F04Gffngt`vs|pcz#pA1eYytlYnT2-P!9XirX(!_ z`9`aH)_9Dk-|8^#WE=I6Dt8n(6nRZOgKe+}?LK$7EZ^aJLWao9;Qn4G-*MMne{@pH za+rK0^$iryvuyG_bq!Xc)vOBWq54!LYQ$*cCG33M>CLzs%c(*A8n~?8 zDy5#JUe*c95xqX0Nb&+^Qsb7IVVF?!{8`~Lw2*8P9!(p*|BUW9En`%y*0&!0MC^=f zp4&02W+b+C8TAI<+fI8y4Bg0XF4RA0z0REOn{=H>^2Ez3m=1&}g#BiX5Cjo5!=F6m z&5~~@H+=EI{l>7=a=^DC|7n;6#P=y3n^_Ax@D&!n-tTP>pF z$3Z}|W8|cm64>~V8a$T_Id>W9+x6eP#F^2yPuMw2H^Qy5X7P5*x=BpBBvd1Qq53o$cQl`%PxnvO>0ClP^sh@Ll(7L9l{$4cNuohsC==)6)a9d* zdi@Pn#zGPjx`zSbYT~raSVyx)R4;q?LzA(jZ0YpPtYe2bvuF8uHX=~!FMO8@M-RLN z)hJlri@S->a<9@5&sz1G=%DP_%u>o*;x>eKwJH`5J-AG}sSmhcRC{xJEr5w5m98e@ z5LjcsFLi4W_|A{e>V1Wu?sq-P${m-3sU()`;bsRvCY~f3$0T3$YJ;`ZNzm!;`6s%g zBC1Ab)qHB@6~zlQwhHy~!-@Lb*2M3K`kKfwz;_pVq=ca4`%?pnDTe0wW%js7rdknU zV13d)0Kw$i?tuC4b!hFp`0rVn_z;$k*U@VI@?EZ4YaRf>c#@pO-d@QoIx4_PI>!shTL?ci>2t+S2vh>v9s?Gw(Z`f z9V~u5pGoIqewk-n`cfN(c@zNIyJd-u&dFTcMp$8|gT)^J+V7{uN#7>omyplXQ*SA| znVJvQwdq9@QS2&Bx_r#h1-iIBz-v)kvNAVJcia^#!Ma38zoSQ%gAF|Bn1u=oS@B6- zO;Q`%(fA7n*W62dtp>1t{{r=YG`Mm4OKc5)E8^t@L_f}9o=BL1|k8MLZ|(HVSi zX|$c-e7z^?1hIcT2|PFN_!Iem%zodgryD z2HoZc{x%_hnmKi=7ZMNIt?$6Z?WoD5i{_Sf#cd19uxGpW)sG+e!&!HkAsQ@$7a5g$ zzoMRwJlYg7fXE!td`-?$WqNUE^*iHz&z`Mr@u0TFOD*6Q;-W?aM>t$9x8en!VNbI- zyBTFgw2aIT0=t5*#WM^6F=G~S*vwzis}*z?95dBtmQ0u}s@nOhIYd%kH^raJTx zPhEkT4+3IQb9y^Bx2Dlr58^^%aqTXx%XVS-c$V!Cz=ZqxL)p~;w-BdIDXV3Eb%pa@ zl>N~IL6M}!B1hKqUL1eUL^2BR&TdMDE!NpH^uBd(otKkn?GVi7wBzWHvZoLR zpxyeTr_r@HJdsZCN|$Wn`=};lPhCEv8}UR35EZc}_qOh0k3q0}uCl-UQMt9JrDHJW zEYBWT1_Q=E1;4x*Ys?;;xZu3B)rgWz;+R#_p^l~w5Vk))vRY@^i+!dtGRyPZ&bF`> zy?8nn;7NsHzHnB%PJ15Wyu?mF*z49X@zX1f>mAz3gSzv*fGG?)oCc`?9R6rkMkVIv3XtKpxO9{&w$A$?#Y z6~5EIYi%p4Taqz$-SnE#zZ6YVxU;VxohPM&#BZoStgk*=U!go;nk3GE3DZ!UfQ`pO z7LW~KHEsLw66UAx%Fae$Aq2Q&)?NxD62gN`%pQ^2Z}s8VFFf@U4tNaeHVdgXmbuzK zG7{eHi`Bbrag_aKkF|CPLjgyd!&OO#Xg)}3Yw(1e$NeaQz6#ODU-!Kh^Q(!(sNvI4 z1%ArvL}Q(bbhhxWQMLq<9-w%7`s#sk#2Y$a$?;()imkPvS56}9Lu@m)d7fp5C^o&; zFgqe;ukttzCA=W3r$6n-Oxkp#*Y0oFP)8?Upl0~)qTCtAGoPCh#sAK3S--gdP_LSJ z<8^>bCFQyDZNB&gNQP_VgL1fjiq*1q8YFBhHbaQ7Hdo$h5 z97Mp-5>KIORP~cU&Hdzm0o4vjK_nzMJ$IDPFD5gf*^el@?OT09ENx5-knMoUt&@Dg zI$ebzSp_c_?ju|M34DUhh&U>L0FiT!{zG_E)mP?~0qO~Wq<^GXKSEWZ?J5Ngl=_i@XOA5M09&pO08Z2vou^F zdGdU(mGGuNwurwkma?Kt(B41CltqGbrZrM|(CZD+O1_~I17F?O&{K&SS}ro_bPhr) z_(wO2Mq4r^OMmMJXHP}(d5SFGFCM?#TnQ78nb}}zd)n_lwxG(s`(yOX1?^d1#cR#_ zXy)Zt>GibpQ~Cg@@l7AKWKx@^b};yoBqyQG;n=bFvjvLBkCp$i6M2+nUF(KO|0P1O zX1EYpyAZvEKJ#UeO{0i2n+qz;D#^pJ(e6M=+^2el<&|{JMakjf?(J!B$8ih*wzp**vXibdF zXn<$4)@K60SuF~E>B?y~4`)mbYA$v83k+XhOsgOZy8m7w)3R$&rl!w6YYhmoWF)F0 ziJ4Vtl#5*mCdY&&on~ItZ1ai=WlYEE#h=;Qaklh7F0y$QGtpKk&EBXv^BFg_DMhmi zc71jETqcJzRNc`gl&-JaW#=B=8hoy7a`>Wvtd-l&3*UL`I%KeY*X}j+O#3adz+LR! z$(-0U`{*NNjm+OCU_$G2-!QR`Dvc{O;7neU%Q0h^SAf zpUC|7(LR@mt~AgFqwPEMavSiHy+*gjm2DQ;aDa(RnSf8*x6s7OEwbRvScYz}SSN!7 zmJXNBpZ5D;WEaZyo=$crqh0uI)^s{h7@7!ZytwSY8cWcynuqK^@+TYCoScpgX-3iO zkDc~W6mt4)-MJkteH4GX#Yyte(? z;=8QMIM6x$(%KLgNAhmEEZCUw(kHGFXsYgi?U`$>7xQ{0F?hy=F}xzUUO*14sB5v3 z+@N@81WS_tc&Q6~#pXrY{?|3q-Ag!OAjN=T_j$@@6rY|&afEfd!ffjoKxnQ{jcP52 zXR$1{#hWfO-I9%qJ!uzs0N~ZMCc{Xd;yPK0Trq@%$kXFTslK%aRx1LVwSAA1s&9aA zA9}8Rc>n6Qpe3d^fL|KPzUQ&J@{3u=V`PMg_~*@3?zGvmkqO>)GwIvAyM}zmmm3W* zzce2JRHs$V?b#NmgqrkHxu2`;4Mrb>e*S6t%(jSk^*Bt%S28XB4*KG1Kx5Lai{p`3 zz-rM7U+0bU+)N50@Y!EJFB6Uf*!a3PU`Nf%2n$yLWcsdOpZEOOQos1rajiY|ivG4N zrMZGHOp&pdBLoZ`JZNjuTMbq-VX#*ST1LXrEsiz@+h)}nX``DSiNKU(%i|IKYdH&8jfOaB zYWY+5UboC&oCb_selV>aOdsE!=T6EmTgM%CL!mcaK55Q>T-7ZX6qWzAF70xH$aP|~ zt;i2o8u1c-_Xm8H#zzQ5Je_!oPi5PV8Bw;WyhXyyc22WobIPyo|0fbR~K zPTI}f^v_^L$SEHS&zryMdkwPxZIH9Is58x3vYaBTlM zVr;JGsnkJ|e0HL5)l|{`N1^aR0GDg+f;Ii5S|p%!2K^4ak2*FZNIQyV1}n8ogi@hJ zUuy!~GJ-SkD)0h1|3#o%6$SCX55p~ZqVlq@MeX75#5GN`m@~`uVup*$k9F3iYMfpJ zr)&yKZtUb+$o{?`ZSta$o!m;i)>rTgeiQp=Q{u+zPVITb*UVQ(?WWx|;0?O(%|6{T zT4dv8%fXZsmaMvQQ*oIU1GvX-hD_`%)Nu;$JAdF2`a0*}$-cW^F}GvZZpL8@u$*Ap zH~sYVm~Q4u?j|9VmI#nvXHVF@og)qjQPLo$`WPBp7OED8thT^*ABnlQ;d_@qHBz3c zDSt%_m!*PL&ZKeSCfidu{{j&wpa(eR1L;wYh%;MMhC&P%M|>Vl!4-aPuBh_oa(hZ z&?%C(0B3mkU`w~J2b8ht4VGmF!RXa|N`@rga|niTGdS&`>;Ou zCT%i~G!4^Q5RBP7eSIiWXho_#9QQX-!H#sFE(NSMY`Rp0~~EWyxFsJM^=)Tw(nCT%h=2i-X?ezxEPQWkNS#_8U3p310(;#x z>;0eat#d?|Yo+x*!+&e0Fb9?@!-Z5#K4hJ{IbXiM+3OOTxQ&<0%Y5Ht6zJ?romAre>t)oF>r0&1u?9w{X;UV8q316R7qHE(orCVG zqLlq~xpd1EE@smg2XC&@p8DC{Sk!YB3Np?wn}#|GBe6NE`#OA*7Q)?Lex?>LGGJ~M zuV6kBsyotj$+p)7U9Y3FyiMdmD+y;*i8y4zqjBKi4TrJ3rQ@n@;GQAzdZI|JAKm5D z{n^#Rx9K*&Uo`bjGM*kgt@Ha{u|ZWFTrB|1!)IP#{-N>dOrqy*;PNrOXkik!v^V#M zp_%WAXA&ojH!i;AdR9(`C&h-rt^gEtiYTvY%@Ew`)xv3i(81WBO z6B-H!BpvkciRN3$@!YSr;|6j0M5bnqtpgp47gFqxK9S$zB?$bAr>E;c7+(epDevu+A)t$>^8^Euw#fnQfuZX_* zIh9eEmUyzKQfD=C*-e?IZI5HO9zC6iyA*PVm6c=_d>O(BZ#qad%eF_(%UT83!vzks zr|F6~v8%+? z%+4@ySKrFA1ojMsO&-D`%3wM00}jswznKPQ#isw1IM?gwMYDziu}$08;p{%Ev42 zhb)ZRzqIY0Nvi4jWj%|6)r07!TaGqmE6G%{whSMg`quXOwzi*Ih+Z|xpSBKN0?1a? zLfRMVlBm`ofo6U-K=8Jg!-Dd$_&b+&P zGp^BB=CSkNoubZT80mz`EL^~uy9C=(9W;ZhucoDJ{RN;o2>S)9EX$0qeVb1NOqK<2 zMo8o`sLZ{vP25asq=cFS5;IsV+C$({MFBknzthN%ayqA(?>}~q1GHHZfHH(wTxurC zU6O7Ivc+Dok2q5khxLrrnNytGhG2qUATA~|UqAk-X93`-xT8C-HHpepwqCkX1{f_T zmvbzus)On%DK_)Mcf&{0NDP7HWaSLngeXy!3loY>QTptZwtmFz#MXG&d$x+&28l)l zd4_5_`uw4)6@jkDFVQ=PWzo$fK}%YTwxO|?nN`MI{e(G2b0g51aN1%<*hjMS@V3tE zG%)hXu%_6#NO^(6LBaBhpKfO+_wS{JLHbOa_6fiZt3A~oykU$Z*)I~x_;=%VZuTlG zurX!$q>~nl?*g}%E4=+|GGs{@UP89Q%^r+jhDYoV1D7m)1lzbKMmc}=ELOR|WDMVRs#Di~1V{<&_F~~uy)^}E}+4M$B{nMC^RxDnf^+(Kt zWB$BUbC!kxY!|?_ALOgzYODi6&bHP{@QuDVVB$62565mxB>D~|`CeZSR()OYK~`nM zgN5sk>3wweLS_LzV~ihBF7~PMqra}!MbtFP-gVZF_=J-6EAjDyM3=FhVYr#6d|%lS z7iLHw_LRk0LD4BFmQt=ssqTpJ+_UcT!Zwt%$t=6w31B9?nsLYt;+sVrq3fhIUm!td zX=W~^GrY+*qWNuc{SB?xpL%&|*i}dj6jJ!nRcNLep`ckcK@qr-mNU6J*_(Kk8J)A} zT)_5jUqtkw2s&OzHCEl2zDu(1i8ukev9F&t{zECa;q$Pi!Wjcm_V#t&SB(~k;jyj; zS-HhbhdNQ%z=Q3*v*wgwop;jc6r2Fnb!I&OMn3X`|mWJ+aUf5_`CQ9+>erM>S4!^ z?(<_IK@q{*<;=`NRE5DR&-Vy5*(7i6yr&h%iyL8dq$tK_;!L8vV<$J257w};$i{?hY@Cb|TeHVK8;fj2b+|1 zDaD)71l_n+H+S)LWBI&4skO8x+Uyb}XRp0b(qr%|H+-wm2z^$pmINS#aLj2WA-x1U zxUDxuokqfrp*5U8HJ;7Z0t+s(66_xd2{PF(Gh(jbr^D{pM1WpZp?#d&9DhRCwAM=0 zJRf5i5?g5r1@&IB`J@U(ezjWD-Nm`T25@w%KAgfav$Y2>7f0w2Jr2kD40fZFvqRpv zh~W!aywFB)xV-i-wgU5H5$Pgt5H z&oPn^`Ts@Y#eVAQD(xZul`9p zPj=O?%BY|cLO?1euEA$JR$+3Hsco@>Z}_H)@fU?eZ<8N?fs>?=Z&GSX5oZWyQZ72* ztn7f?$ts*l=vb=YC8TPAsNQ-E_IJF9Zb;4#C(A%XB2DVLS4B?h+XNF9ykAzo@HYBw zo!KGGD=2JeY{MJJJTF`p0QQb!sMap-S^SLH~|RvaFOT$4b$>7aXo18CHlMu36JnOPu=kHNQl-_`@_B+PJEvu(|M|>AP0Wx;BJH(w>A*dp2x3l|UR=N2#K~mfzzMS@% zQBh<|tB%1<-;cXc)ZkcTDJGAerT_eJ;r4$)tHv+~{NCFerf_ zgO1x!`$?Mbm-ZSGv#Yex)Z?Y~>X|TPeocZ)G$!b@Z>QyMyH zIE|qJCA%{IK-`(9<5DZ6!|Ldvu(bxv?-*V}l)mOo3Xyw9zY-k*&%_?~+O!85J2O@x^T2${&oc8HH8|`5nexq*sMC z;9=n^f;V!5Y7lfpUfGw%+C$J*sD@kW-kT0A-+%Ef$|Ddp^M~X2bFPcUY+#-l_JLh5 ziWrSSfLh8hRDIPQYM4PJnx>tv@`js$K?q9L4D)j@0d60`q^y<c~l4b!Y0R z;LLb|7MW__ThyxaR;kwix!~S$iSc*7oL}2j;KuMWG|X4b|8LcpbpvJ2hv~T@3UuQ4`J%AlW^U;Cy`fRpF=`6z z^+q|HTgn#!pW>*Ve&c-me*?S?iT}X65yj8n{%3eMs&o1geAB7=UV!ph>gk;x;0&AE z&>cK>oU~S1ipKwq++B$v#KPDSUjlRfD`@?{{?bio_yJopN=d{w?&Aaj1-wTae;+51 zpH0U1^T=_@Fn{{p;J?9pENJ-G#}SJ;yRmkW{Qd2}d&1tVOoSu18dNMTn0%hB#>bG8 zVbOAEYL~YRvnn|mE4$ZQV zJO7my#}*&QlHll<-T#BVw+xD_TN^eLAP|BjIKcu02*E;dX%{P@4WBKoHO5dYJN^tSJy6fQPpd&wb#Da+Sh&Exc{E` z{#R!qsVJBkq9;yH^FMm&zhAb0cYJUEE??Rl_$#*izq`f%U;iTDa-Rj@^3cfhzh9sK z^<$KN8+9c)r3U|XF3G=Y`CrKI;627a_$X-g-=|pot4p9KdOw(3aJkU`{b2qVX9KwJ zb11mUo`#?%%6|^J|BZwHzw`e;>-?t}OSiJ1y{p<3TABu!0q9iXX`>&Sn1a~NoXPGG zK|#{eg)Qi0lX+Zj>cNP__7b8HYphW0{q7XXVlE79@`zN+RC=l{( zv?ku#;}duvlq z&A^}hAeA*4RY~xtSaJ_<`QWfRB<_e%sUgw3(k+q-+Vu_({n5SX=p%M&o>uhhs;8+G zTfbsonmB!mIOk#R7o5tgLuu2c{43+M+#y)eD0+XnRKWJ0-Y3%&mwcPkQ%KwW7ESj> zbiPdMZ{@zObpeYD-6;tTO%)KWdX^ix`g9O>u77k>tO4S><83heRFC|o_&oI}rq+RH z4D?yoYtXXF_SCDX^#lJBnngR84DS{|EDdKXf$o+E6@ISRk3(%&_NHzh#eVy*zJCg* ze?s5R$~d*c52TDN>&oe#)&i=IDvv%_VnG4P{vtIs($gw0ohwV$Rj;H8C2LVP5Efd! ze$Iun(0QCMb6?H2=wpvGKUH>kYl+H}$?~Vi+pqGPmsTma5QjQJ7^hSrM*FJDYTGB+G9x3e$*)QicLx?@RlQyQwe5l+#<CGV7+R(=`q5_K*)XJ6T+;y!lupcS~tO@X9wAEI|ZYr(z$qM{2dcK~S`QIuqU84D?v2 z65aguBeAdnAMI2iP}f;`A7c+8zZZsf`yis*4KEj`_qrq3gS|zQq#2C=m zJ+?2UuNid1JFit#x#ed^t7Qa`^-1k`Syex0YancKD=MPxg*mUw9aIK&dfDZXW}R!1 zP4Z6`pd~){F<*)dCvAPHtw=+Z)Mm@U=yOz9kRZOwpD#LgFm8F*=|}a0K;!B%>y`|^ zp!(Ky2QG4fS!&7wyM7l%&wgy9PH!yBI|ZxY-KY){jaF=k9uSV+f&ZPx^82(7jHvRJ zj=oo=J2j@7XfZ6)4u*Pte>y$w0_6IdP@m!jrbt3 zyeog}5=U|c4_^X=0M6#KO;FvDWqRq^f(8H3Z`&z@aS0=;9{wXPSPRQHZDBGiQ=P-e z38As@mOLlwqk41dC*f0m^OCKk4pWuCyMNwK){at-ZFP%!J|QT<#Htw1t~@`a=MQ2Q z{>kej1N5(;!Kg0QZ z-i@WYRnTDBh)wH6E1*WTq8GiYipG%l+Qzz8Y_bdOBGIpA(ltC=W@~SsK3#*>dujT# z%4>z_k{;j9IOS()sJ(1Gynxe{it~r~cf(ID4qcv0m(#4eBs?N_OQ~FJbXy+T!uqk? zMy;WH^D{mBVo7N?0PCpvIfZ_wbhwm@Toiq4=?EhrJ$C=FnAt#{pv6VF`KnF4(55Lf z+T_Qu_tBl_h~RbcB`T-$L*S~WoxHaiFm3I_FnOkzIiCe&uCybs{XDW(SmzGqNpC@U z(!J*=HW@^;aWwUdY8AIlTT5#@``a@^QJ(HG7%Gh_gOc5%PPRlNbx~&S^X)gEEAZ0W zrM%_{mZd;L?;OvzYX z*l`1gd`l9?qD^5-D;DdzGmf$43RF5p{sw9VGYmC~>*2z-TcfKM#FvgSiI+~<$k;7T zIQG(S9a8@`H)kIBT&Yy?+R>mhWz^Co=F2!u7YH!tNOxH1V)OEsvQzKGGK6gg*%j3L zs&#ry4(OM0W&33}UoN49iUa>jU{2IYT1#m^~+|$SO@o%R5TP#LLK6Rbx+< zL9g0k`SMO!Og_POIw_V<6yE?+Xj(6_&&+T8COZHGcqc2Ip`OQ5C~?B~0+_*NwX$Iy z)n9?RI9-`SlH-`9JGm1r4g4?3xy0A9pA!9bMC z_=@PI3pIW%iCn)5Rq?sc23)mQK$}&yeBBBtW0O`(L*5zBlyzAezw{sP;L#J!&z>X7 zF;twETfctHtWnkZn!Vsue&Ci@udS+|39+E2{$k?yh`l^zJbdOw_?{m8On0*6Q;c$7 zkT;P`x)hT)p`GZ88>x3he0v=q#+9XFiY{J_M?su^nvyV`ra=`KE-^`}A87L7@ zm=hd9p$9t=N?li-7KuT%+T$%s_7^$s_iVj`{D80TqDs9)rf_9`h%@mAMgF1ap-?=~ zSvA4fQOk|#b?@Pv*0%y<+|Bho6kJc#IBbDcaJ6^$+b)tSu=3FxmgqXNC0+VXs%jl zfkNCsA$*3hLH67>NuyU*E*u>wA)sHqPaPwci)Dx5GIjDSa%`@Y(mMviitEh^<)n3W ztC9<)hicnP7ri5KR9Wx0jT+C4$^j2PMhy)VtQ58e{Y=C~9QXh*82s&|C#q=eBP zIMC!?yypl>>=xct6!xP`|Mg^r@9(t(a{wzUDUbxr8})VP6$OLDYcKj6!O zb6PJ#GWu?AclP{btEG&cR;OZSNk4Q_NIS-7b1FL7mK8sc{EEk>W2Hw?Ji$?~DdG3S zWbr@eRL|csH%#)X&MCRP^&k&z(LBIvT=et!ygmjW8sB=?&vhXY4fGnl zc|R~$PS&CIiZfiH(&tB=`xCEDn5A`-+C~pm`s<=Kq&67jqrW3NO`b87kO!-MoZ2i~ zNjrJ%v9Y`3GnO9?|KY`VE>ar}V>Y1twrJWaz5PY7Mh2w5Y+I|cMI{@366~AGc)G)N z>`PgY25Cf9ScsZ_XMw*2=i(_iw>X>Cpn$K}%P4$Gs-E8DTK3(gbtf>THRZb^5%!M5 zC$JHQ#C%GrLnRNiRK(}1J11mRwmRJ<6L`s@k~pdIFHJb6yzhO(b_rsw$)I0Ge~+&C zd#U)Ygb*)IT!)n1b&fG#U)V(28Hy5A82sj|Bp>91o=c#~Ad$iYq_?u-2*LgkE_Arj z7G#D?>ZJkIB3#`w@;VgR3#Fa@SY+XNjI;4kHTK>r?m`%8{+?;JGdCDf99kjfsNL@=@)h2D4dxr=fQ*ti5qV zyC1D6%4PA2zbroJ0=fJ9!Ef!qm(;$cTc($xiIFP2-#{{C324$f3O*ZM#XIb2uUBb` z2R|3}udN+mxqb7YC;=*5>09cIv3tz;g}!7vyiXy;YMDFUx&782ErcpK(S+fADvlVn zMRh<)hjH@}DfQ0#*8gv{xHKopYhFK;h0vlU_3WyGZJFPLZ4-Xpnk{@Z1oRVFNbDgi zKKQW!j!}u5kKA~{3xym+b(UC^8!&Ch>O&$0*ff3ilk~kfFOKBihQN!|2K|B06P$zR zZ4c#rB671LoSQ~THJ*!+aL)3tQ?!4#_`{T6AE7rqbcGv@c~I)M{j*v!!uD~FPppT1 z)SWJ(9+}Wh8fE9lnk^>K!sy;qFxj`TynDCOPMC(nb+RAs6AGBN7>iKuz(EKoY>ZkR zm~iK~jGVOlEayYj?Dya4e!Pa)z`4%qZTSCT6(7`|Wv~%7pd9&r^js;)WqPOyB>`g| zcW`(3v^-ykGI@*gEex2)PUlC!i3MG@C6k+DrwlF+I2|FnuX>zYr%K7731wN&KW~Z9 zUC?3J+Y~*lLJ_N|So#;y-&*mu4{v&2{*YmwuQ1ZRo_h2KC_!7d7iNF=D_$k}eQ0~C zNsj;>d^{YL{pltkk_U`9qRRlF2<$>wEkk3=^^FXAM)!7}`#bGhOk& z%%ane_F`R0&e3%^_cl-|Anrr&8`E!JRx9;Mg{DvCI4w%QXjpc8HC2euzi$k;7=!9d zj`%#bKlR0j8F;tuqu8-4lvx~2|F5}_HeheWtRf+B$nRz;VJxLlWyO!v_YmAidL4Jfa;Q5DMJaKvKMGuwDVL}jc!_#sCSj1dN zLWx6aBX!O8={6q=92 zCQN+pbuL<v2b zum>r!S%4H@rB&>0XP_A6S{nfq0>sn>N?t$-pxFfx5!WMeV{FaK*D{2QQF{bFxF$@- z&XorCRmvlH_nV*#u4on?iY(ZPUv-%|slqYv^y?T~)0Dm1UuJ!54=5zuaFM{H^`yWL zPEkr#SvrcN2z_br;+^~YSZZsN#NNotaid_q9>GYmE7!~Ro*b8}?<%YSfGl7j*KgQf-R8?koyM)Gl6et31aK28<;jyPnTOOPIGlE< z5-cG?8}&VLAdtjwd;SB(e;ULOV}CjS?2LP?cJB1Nxp%tXDUGY4w*)&>S^FScU*6G- z42@=9-92yqq5~dzxY3f~{;Nyr?bfh|gt=8`_Wt$ricY$Cqt)m4PXJ5os!)!q^uaHxcQd% z_NI5B@*$&7T)M;SZ-48;LywNpoA_R2rRy$ktY{;4hIAqISg)yUHB)OEnBg3#GGBh~ zDiE!ZgB9nk*QIKoa;n7T8*Y!EaYK6Cs(}f1oNRNrx7@I6KZooATE1-nAPs2-t z`Z?h$LKwFzj@ZW74>dc$M>1`S7g-4JLZyL8B6)Gh?FvygSoM5Oc{M04UG~i22(hNh{5vYNF9UsNP(@8+XKOpahHB;}SZ~fD5qZ+pZkd?NwS&$IliH zi+ek0FLD5rIVeeZ(C85#N)jIHfRcpYs{Ud`Z6&%ekC5&U^Hf6ZT+h_irPWY&)Lv>;}V zLo#m3!+k?mXlfk1s7}&UvX5wK3(Q6J5Bj@s-ZO57PxUfHN;w@Zzdzad{%CbxRs8}o zK(f9G8;@uhShW&GZG!!54)pi*AK}V{{Hy^&ywnj{O&(hHvk~w*62m@{{$RiMh0x}< z&GpoNObtiHM||D9(xf=-v!(7PlMpK0YZIzgGhxwN-fO9+JzP_;lI0?Py?0_hB?#$# zmyv_@H(!=eGmw~pq7JAM?wTRNA8p5BVb3zfUF?zbKp!?_XIEM(xbt_ZU_+Ia&MweN zVDqft>Jv0#<~CH^_^^Lw+hV*`M2NJ=i|_RF9JY$#dzHq`^>dP!jw+;2ADL;K8bJdU zq?~6ZwWn!wNZ?^J2cp|reGgnrZ)nx8_x0)Dar#M=F9je8LjjGNKI29iNDI_>>MzBF zm@#DC8ZK!Go?aI>5*&m_>bd87UN_uMC==D4zxOF&tN9O$cOIMbK#7lWYshawrF^4Nmv=0Y#e(SoThQp9*gQ*SY z;V`0(t!XZMKV&1N?U0j-%O3TFt1HLIL$|w*@vazGUoci^5~e{*v|TJ&+5B|9I(;tl zY^z;(I8c!P(-Nif!itN!VA-doPrxpxce8j>G%2tvA?VXW>L~XIzIDIVof7@NT6jCk zwa*ozJhxitU-&m%yj!(ZSbf;cM=McyXB~e+!jRlWd1QCC24_uerOaPe@YC#|Bib4V z4g7Df#LwqRv}M=kPB(IW3*P7r0W_4ivlW1*vBM}wrteWMfmq_fafT9m`gUZw9@o+v z-oV>mPF9E|oi5OSMBOQU`y?yfcUT{s{4(n{&DeC@kH476K5sG{e2bilmh z+MQsl=HDm;O-at;K1?bj{fGLyO^x+2IT*6ar~5t20N1T4&9lLXnvSat{QQTDjK}pW zE4g2aA&z}lOJQNoRn$by)L&ex$@^j+PiJ%KsZqsoWJ3V7UPt*BRg7)5(tWp-&jLQC*6s6^pINMr16QmR7nozYD~av`xl7BB-Fe;2h~acy>v)yDf>BRb0)q{cH%#b~F9K zZdKae#O=%2vo%xuZ5@~erd+ax(?zu#gX;+wlGF^o=g*p)7JnU+HkK z@&Mteo%>y`QunW2U4@(5pSKJ7IPp!)^2HVw8nslag_)XRY&1%8kDAACBp^uMeGx8# zKoxdKOAAtS`JD;?={~l6vt}fuXG5O*dS9gW1hsFO;P4HIz1rIJtELA^`wz``hhb8U z+w$^AjoeGH3tsU9$T+8Wu_{Iy*-w!(72(^3O^qWjUyc#PeTLKTC2K*7K-&m9Ynz6o zXnO`L44rRF%hTBVsYdKBpe31)?vc`hnha%wUA&eZLI5&K@j->!BtBipc0{1F`h^#u z)j73f8nbT0|4YP2zCn-^*}mYC&k}W@J#xmU#k@n1a%7Q@>X)fEC(!a>#3H2He%Z@E zXbF9Q+#te5u_mh#{7d5RC41B#`EYnV^}(aG?&LqQc)kj`-(7ZaqYP}v^;K1k$;Xv( zXXBon=N4Wwc}YE@?V7=RME;59i|=Xw8!E~!_)fUvlvBm%!+AeZ{5c96`RcGB)e-H|QmDw#wUMdAm8 za>~s>>laQ{)%!8SPE=9wJVseP_XlJbbSvcmv*>f<=9{>;r`z8I8aJll;mW2;N68~o zxD1aHN?7}9k7p2;k9AP8#yV#-8`6EVa?dNrSi_6ANPDEb@R3nW2WP$WDA)(oo%jyI z`nS7#cPBKYMe%Njm(8N|>@4GQpry{ka0npD@w_D^b(&k-LceQnt}7^faKZco@YdGVOBeJMr7 z5aG9HTX;NzR#?zR1lfK(U-+p9pIq7%d-O8m0|+Lk?vvDfvKyw7%fIX?oWCzOAjm6q zjv%j$JZ}0fA2Awuu1+YSwS88e_(aBMPffpg7Dsp8Ce3vVEsa{dXjx>y zGm7SCMM02pk$rA|%Q=s9C87j9#>=mv=6$#8nc?ud(6-mj)W}*Ej_7MMOaMW+Qf+iY zp_{839yL*dLx96C}*>OG6u0?go`(W-;0(30= z)Be2Ig2sgo#c@k1uS!K99keyQ;Mkn{3Me_=Oqxxtuc_%cj~r$mzutKW1lmMmC6M9% z03Zc!kB=O(hkZ{BjE_Hz{#mW1+pZoTwp~=RwWmHc((&upe5b$N%7z$ojE;Aq^+$es z@2`p~$C-2%F9YD;DwgjsTg#mdTdwAvD8(z3u>101-hukGfzzJxIwygPS!tfU(+hE*Ko9;LC(5(WA<12q zL4?tNBdhUEYe*Acy zsJ*0x>>c)c4zfKEVCoi0N${y#LBDG^>1$PovpL?0r2987MkR?#)J4#I|7R6873PVO zZ-4)OQlwYZGjC8h{3yliiQS}e@Ot87r2aVLTgaQs8};;ffnXE!l!QYug?kG4ZEYxx z7tiADvy(8nA!LkmT+uKu1d2*Z+b>_Ooahvh0YdN2Mi{KfSxFK&Uh#!R@ix7|_`kTf zkL!7T8sO_4xw=QO1m)TXX=hB*`pjNbQFT0nuKp%3Ietb=R$+}~dhsaH+_Vr?I51ER zb;Dmj9X*g`T5rKR4DorDbfLDzDfv$pfQ*Lx;}d@#ZK~YY<)|v^9?BVARYSfy-&+2l zYP7otK&-qsV}0u=D!O5wfvSee&RWL&US-Kk<+h}~CSuQ*>l2wetU@%FWSjXLa=Mk% zwjb@b$c;TbNhZMzWB?v zJ?pi;sZTJAcJ?DKY{Nmuh0 z|7#Z69@OU1lh?TNKRQ$ugHdJRBnYr8$udQ>v`|D{(Pj=-o}UP8 zv$C}Hk?Camz-BIZBXm~(j+35%($Uzl>lP#0N?!@;>Rq{H^oMtx0okWwWK1DRs=mqCP+H$Oi`J#IzMI7~+||{yZGHpdzpFfF;ayGs z!r#G?D#x&4)W;Q5Pi9h*UtLTMVR`$o9)C0%cEp7#I6OEH6|s*EC!P~fgwaK5O*B2` zc2~<$Y|15mIYvb~r(zZm;uck*DGTtJ#dV4LV^CDvtMUeKX?NY!E@YFdW>tW!^4h6r zh~f5aVKFKf3y|s#Q1LwxRp6(L8Fs^?=0^nu&+3&ez4H8UGoovnOM9d*`JQ`C`Bl_^ zoTX=YNMB zqI`z|pSNQWAGY>U>g;Sq-1UD5j03tbnt-O-3r2MTHfL%Y%uTf4qr6w&yU+@${Irco>iw6#6gYQ6p-t>T;0}d#}JoeeC#fefpOj6 z>f;s6XDEadkB~3t$zNmtubKGo|NQUY)9+Ub!r@fuicO!qj1B&AX5^`*DBj*x&@PLY zdAcEos8j=|&<$L3NkK)_v;U-cgg+eq>2o;K-?VN?@Vz%zCN>f4-=U@phG+>WE%wcZ z9Y@FCvfNclwvURaxv|EY^%b-bEvw zoPKwI{?ED)Rg3Dr6<>maGyXHrw;t1dD{>azSpR3Oz@bM0!Z?JaA^-N?{Fj~Tm~=mc zlbk#9{>6X+8@@ zo}2%!r>I0~U$-JR%FpJ83uc!Pw$~|Q{%;p6F@x}hE{2!=Ufk{qv-nIVS8LOHY3HK;r8f*8vWy30gx5sT$^ zMU!Td?SJ`$QIne?^BcsF4YXBE+5d{xKG$FIA~*N$)O&Z?e^89?(%0QQhn1pL#CPlG zSzxKCtu=l2V685<|MzGnm{jQ}X8U$3=A@AIC%?nNU1o8&g_!F)HWpmpxv8an{AYb6 zr>XW0R7PZHnePk9+?9yJamXFzh7L~vIjeq^-*_@Gb7^j}`mnF3DAC9j5rL~e| zMH_#9B}i#inT#fMq$m#38)|k-w0|`hoB3n$ z*|uf*xv6e8R&HV0Z5UE|T%=E$g5Bj?%AC?pk zunn3^Y!mu8!du34uwsn<@Nab2LBIQ;Lujflv#R~REcT(>_%G=JXiMGEC`rhBtUw(U{Kb4Oi?cY=81SpP>=OG2^HA4oe{+ zL6`TlI4Sg3JWvl6x7|t5u_eqP7@q!ID znnMtTM5(eH(@Oe1A$#R60Rb%%iA00&)W|4#Yd$^{q4x37`DaV3j>%ilGgFNtHu-c& z-0o7Uu~;44sC>P^4vzI$A$M7Q8}#zU-j!DP>|hP#(SZM}%o0|~yZ9^W(8YyPR6thL z^T<1kPgD2BI7GXZv(@4e6GwA<7g0bN6y+4FIXht|R5xyxCE2RApP!!Kx6eaN_bal0L-TL-M*& zXr=*Ot{l8;+p#4VG{g4It?{>h3@oL)Lc1ilW5WVyaoZ%$7D_DgTdQ9-+?TvneJG5( zJQkEbfTb_%Y{GX@zu16}So?u&ysvp(sWIl!QEcO_%@Gv*Kd&HJ@YCLD)+lz zmJLpQN+&bF=&hbwj|Ie1t3!0e97Xp!oT95lig_-|-p~nw@fn76} z3@|qMp{Y4+4y&uf`WS55Dwo;6y3J@YBsg7|>iq2Fx{`6O&=ip-hwi7OzxRi2tK@h) z(-l{frHc2AZYt5O%$5$gb&EbtDvf;2GQZ%W#jS3ufVvk%L8B#y19e%m>yhjw_zsXR zV^MjfAi0C4Xgeq@=QaLJ^%TS18D1f5vJLeJ3FlpdK;m?lQq5?Q@aS=|+|Y3cVNtpR z3ats?#hFuqP3AY;@vZu|++>4QB7(L3lF z!rumE>E;XV@lW;IsM3H(LNvs??o(JD*@H?w%#^BILe%Ag=4AEbr zELw58g|6lc`Xp(FO0mP$$Av4jdGmjF$($J?jw~cz)!|_%VjO@D1>cCc`)XP2UVxR5 z1IsNx?#kbHA59PZ-pyY|$b;;@h7EOrOt6vwVa>dOXjxY(OnG%*U;d(nyytT1g$Ofp z^@tDW9jmgh_JGbmSRm*1Xoly5Z=>y)rZN&>T1cJ4ybfuVsm^n+5YHQzx9=!D zd~WxJ5T_%_+Z;L)FDN}M-Fp&iXSE!54s8(%m$nSaDVdm*QErI9DPy^L@Y7ni`S}Ma zU^Yf-aDuaJd6d|5-R)%i&TvIz6YbZW% zfR+XOoc=j&7P9N%Tb9+*CMkE zQ~+}%S0nl3@8g4>nXOfde}HUVjiebh1~7(=xDZwJt^UxILD$(M8T3&LFX&Mm0}>Q{ zQ?{C+`!<`mW|b7^Sue%&GqS1%L>13wCFSBHKN*Drk@><3?4U8RvDg=~)1r{y41Y9n znCSUX6moLT?+ZLU!L3o&wvH0o4y~K>&5pAKMB@c}&7#YWKc|`#7X1@nTU?^n4<^4=DX|O0 zYowWV01z$8t(ZJ51aqXQFCnwy7Uo}E)rw!9Vz4-Ifp3jOtT|caH|!?88f*O7?bH1n z))YE)UHZzO@MTYiC6}hfl~6ZC?b(?IOI&qf@{MouiCHxzk5!*6`w}R4onrLxD@?u6 zqHs3JlCKa|J?YN4GC0R8ZPd%(j2@xQ;Y3V3?g&;AG%DRH;ilYq?|BZqB-I>okGL?x zPHzC`nnY^7OaT%NIjEh#|H;sJm&i>NSZWf%E>f;8|K>+Z|Jn@d-3=sUn9mw<(5vKR z1LAk=GQeDO8_U55j5e4YJ1DkTvDs5&ciR3j*sC#4(UJPH9r^=;?fx5_FljM*VHh~r z%_8jDQ`7B*7&=q|-wc;~g>p*IJxnK9tJsFXE@f*|PUfbOBFfz@)LKVVS0_GRbvASJ zuoggh6^^P`dC9;y{0v+onsrq1(1dm~g247%3h2N`Njk$&?;91D-u7wU)OgS$uOXvy zJ-IA$++zEaSAJkys1Rh-D9(k0@0Rd@`;iv|<*$Or9H>%VC8+HwC#p2aLr(Y~*>bI5 zQ!ImpS2F12t~Ece68S?PBrq9XkY6^4#VJ=E6ytkUa*jYw5JU5V#DohYAHgMrmiszCLwHZ;Z9MYeZTbzdKoHvSdq9VsM%y>R`kiS( zJ{GW2u+q11`6@QY!MSiIZkLKpq3=Y4fNPsbWk;2_v(DX}Nf$BJX>5*T^?(nHx=)&8 z;_zTbSqvk(?I&N{V+NjEqi|*>OMKeC3RDJ;!wQHB>g=r*X!N;`z7pwa4Ta=OgH^ z(Z3aG9Zh;0%!}E&AA2(a&B%aR9f6h+5%3C03I$u~mR$lHM|R$L95f7l+B)KmMHd>R+Dc8I)UYY+)9e_XHEc-U#iU+O)q9cQk}emXi8&p?oi z$BbM|vVcH`?lQOvxIzAtt|fvULZti6v17mkTIXeCBy<>he=DG&$Yu3OBD|HC}lever z0xnD6dp0cgg6CcoQTi+fl)p`x6W7qEL}U`Sx1mc9q@*vT#9&|6>zO_VM`V1m5sBmV z79V<<-1nJd`AfbW~)*8KdTi_T0wTY{$x)}fti4=ScH@a?O_v!*l##@iab zweD_Rqnx)o;RPh7kvDf&mAhC#-@3$_p4rV$HOzH84lZjdblPX%tIf?D4^+O6`JQ@b z@kG1hsrF%8tMF50>L>xwh* z{|ZfU)X$y_o7U-jn#beJLyNIeW7?Iq;YVIyjK5+wrY5@DwLrHS=t z(7M=GjSIU_x?J0z&ffWylA|R#E?Lp(!_Cz!12DeOK0%jSZbG1ae@&|vEhe?BQH3sK zVZz9Hfku0;!ZyzC5^bja|8sWJz?kX!QPF z{Y1M50&wQ9IeYJ7(2bC|bqU5=Y#3MCqY+*T5Non(8Om%D@X19_p#f77kJwEZY9JC2itXNfu z2_Sm-mmcpc3W&tiR=Lf+ z{?&gB*LLQeHpkop55M(Nv(0K?nSPS=$?&7}uSC6bjU;XP&H;-|PdR~+;+$#pcPljK z_T?1iFn=rjA>LNn!UO%y{r5sz@9jZ%@Ve7LNTW|*&zOH5Qf;?zcR+uV#tZaGnAJcy z!_kOp`iG5={t5Rm>B|_p2OFaJi)(#nApAI*0w%npgy_$|g)u|no9pD!S-l^<*ecPuG5be{dKgukv6jdoZ!xN!2 zP1gs6u-Sp%gghhoiC}{MB%N-&?szqE!a-OrNnnf2&3%dNi(tJ?#dLn?mmENbEG#@Z z<&x>J1^=QJ7Mfm4(73zYk0`t4l6`%5SMc^j_vloY0U07Ln)O#g+(hCK>!B9iMSDP* z^W4}K%W!SV?+Zv02>5cls;Ox}U^y2!3FCu-PPs6_DW^WNg*&|i4v4FjxkCY$6K8MB z(Feq(41BUMq8O3(NYeeE=l$2O{uCGkRJL4^tecgd9vWeBZ9wmea>vb3h$9={j_F$OPP!l z{KEVYIVw!)jb?Kr&o7!60-1RIgL=cxR6+cl2(%?Be55qtzwjElR1Ds#Vkl{yvZu<5 zo4)M3O9k~v#DG?$2*7_LQE4Q^l&&KW<%n1<#ZymtPfcHu%c;o`<=Opg6xfeph@BS; ziZ38!TY*!1s`$by6a)4?qus1p=aMHo4m#n?~ft#>@*>a0LbeQ?#x4vQL(nHbp4;CC|MHS3szP9=sogQ5mx+=c( zKz%E@3!lfnwnr8Zyyr#*xNBYnBt0;nuN3{DTa@cJBtNDSPxJcr`M!c}MtYt^#DbUK zH2Bv}_8JHfJ>Ixs^ZsKwtu!@ z&J~(T&^a^4IYl+Svw6qF;wEUbhP(Z^93Y{rHSNGIJsDx-R@YSl(R+IxBQ!jSLQWR za)n&4d^63ObOz#RQs^sKhw-K!3g$o4@v_LXT1pglGaU(P3Yl z9(oMswmhJ%q@I8Yrf|l893rt-Yb0v*?8Rr|IPhAzampT5QL{%~Bu!JXGF&7HlVyXvU~ zO$W%N=R;=>k)b|0fF|JWP&f{u*nf{2?IF_OY$_)@N~^w6iZ; zb0Qma(Tcg%zU6W1bpV3>LXzv&szOi0??$}UCw8VRWd6up3r=9j$+Bz>p~XX5?3hJf zj2S+u7r(rd4_8;JCUTc=r&ojd{eAcHPbi)II}_lzVRv@t@a@%GOS0$X9)BomBA)y4 z`fMhwtLTB5)b!bkMr$L6Y;K~1(^c;lultVLTc7*!-K_lVAZyJjS6w#oSvnE>wc+6Z z&@B3;;nP31joz~v-^x&z(VuStp+gZfVh0@%!L!rv2Bn;kl4Wi#OIU|(g~D2 z*rtQEKhcR(a7$_iI`9y{L$}ih(xTu%tp_ki}{z_Oj zm{S!=zZ(gMi^NaTun6X+4m+2@9DLm_uNawC=Y18o0|_;DAJYNV?iPI{cHb;r9i8jP z0XT&dqPiki`q!f$4YM1!&dVFxVc?I8{`3yI(@hOpZm(}`o7yF)+`<-V9cldZFts`% zBF|NM*WvId&c`V=h;Zxx8;s|Z)0?jZ{%krbfvpdMhb8>S;LIFK&E&eA97kL#=Mreo z15lX*o1EthwR{EO=`C?fGiA%?+ z(Jl}~&c>>IocFusWOvshPcm3l#^1bg@yg7U(cM`o48xsz^s^BE2IB5tUwo z1rQMs5a|gW=_T|~Rf-hphMFKKMd`hTBE3d>?-P-ISy8-7rY)%B9RUdP^rv5G|+2<-VRBh$uLkR74KvH5% zn%ry{3m!JlKPcfZrETSVeV>f=)@?3Zja?cVO;WiBJ$J`B1u%pJtcvv|z}N%&Z>;|^ zLXbcKKUk?0th5k6aOnGi`Lmb5&X)EbB^2v#Yg*8hv52q7M&$pEWE^jB&=Vbd!$K*+ z2fvTRH$x8{Q^*g4T?9?jg258tIMY;sL4_{vdr_+TBQf41KSdP{v6m^19fs@IE1Y4r zov=GUEObBAYIgXAm)XeuG~wZd8%_iEc)qKafyBjpY@F*1T&CY&5wVWseDH@iDJic6 z9m^=eb0FBLfazzg^7K*I;JGbE^px!O5);PI``LNli>XEpzc&4+$EXG?Fk8Ci2Jg&? zu^+P#%??{EywsutZZ!W^>*1w}yK#p6LJ0qVs)- zNj{wlVMKSfm!^)lMvKa?I*-2#gki4~K9&m0o_LPCGa01Iy)BdyqprKz<8CV$Xm2je z>yTS1Ju@PO4y(^RlrLex3}`tcC{6pf1{t?BL?T82&D~N%D30a&>eNIWQo3S zSLDRo6BEW4tW{c_z3{Ay>OvY80Klz}iG)O%w`)1^Dtk*fJZx>Yxe2yh*K3b5T?D(Q zWoF4N-X>)lInBvAUUkt_TkH<%>;Tu({bc8SBp?3(d^4q_l&)!ijy7qkjhkmPyxTzi zf|l>aaQd7{oIm-_?F`)?K7{Pk{>3Hz1)7#dAf)mM&0u`Mb9nqZ_25`l00ljoS~b?{ za<4X~av{u-_)E{Ac~6Jm{OgaRDKEcNm_5-i5ms{40}ZhYm7-uhw>`LoPzHst@Ar2Y;;06(s= zn?7yuqw&5y6Xp!&ySzaOo;(t~!_@s`P7)3GTt&}R_N7$9Y446MZ*48)$;TKJGIz7v zL84ZT8)4)fl(l#;MC237;x6kTUgcf)`CFruSvQ}$<#LZ2?|C5`RnYebOq0cntW+3mO?m_S?YGKUcN>j!g_K&uP_ZF^BKOH`H6LX;_ z`Mc?M^YHdbM}_Y2?AP<_lJMBCr1tSQ0yD(jtaKO^6*1KC@`URkd#h~`yTM^Rb7CK+ z{PxY%(`u{4vV*^ z_XeylCgyr!*stm%p4`=gVhy=oM00u#I0?K(d0Nb?vM@C7+^_p$jb15gB zSlqlODs}2@us~eThEx5NxVnO01Zs`S<_j5C$mif>?0RA~ZwvML<%1XH`pDf%C$>2~ zc<{w$(-T2b_-vo7Bpq_(Meq{&u@`adEu!h;=@2uODc+k|0=0-tp%p)XG`EmAHyEcmU!7ZcjH?y7(0xoP>4jgz& zgRfsI+_4i3u$xNDsa9<={aPr0S(u*l3wn%mmm$_i#c1$NrC#r-L}90EvzlqB_SP>$ zT}38tZ3NFdxa~zeZKh6fP!SL>?+DO*>|MZ^X1P)lL}9ryGmU)`kBx9?P{{PRzM5jC zx@dSpQmj*0vujbZ@{E>WvDo8PRTR%65xKh&Q}))8X-&}29EQt+e)nRfILFNA6DgE2 z5P8O~x2YyJplMm?i6`rQ(Yt?bP`?2;_IG~dX$3!lu5M#o%S7Y$B=W#bgVKX+ILwA!>aHHFTt2Kn61Y1=0UIOHub_1}}tr z>jeEcw#Ao9K>N9U2(2LbOAjqcX{_gGv}(IGvNc|2q)V*viKV$#wZ`C>hP;Dr%%t2N z)$bG3km!CI@f%ZCZj9JVT1)br2r4GEN=8zCHU1VGJ}$ZzC#VV#Tw_pDsuunO`=48? zuvTfPv*YerVUlb*f}2pZTzCA*h^m8UL&NyduOI5qKD5sB z{+ZaJ3Hu?3_AQqqqlm>m`NNO9QuHg10Z!_Sqa3Oq#gb{D1AUvAVTR zUzAk|_rZfWESO-P^2G%H5YOT>p4!vMZ=(Aa3czn;>nmF_*q@TIzX8U1GQL+Ed;wQu zlkA&lgFd#@Gf2^Z{P3X@<{d;skC6L(EQf2L z1UbA>&14;xpfo@*H&BK=&>uwCjFWdl4R)Upn|(S@qE>W>Ia*le>@_SxA=&hL1Epeu z1L$!WrYP(~!@(`S7`IX-$L(9goGnOJCK0@st{D4`c20zuB4fv%v)CY?EQ5Jt_S03% zx&D0ywj8JFJ}o%JF8ofmI0s2QZG7+>Gn@s|a552bphZR|5o~Cl6;30?fc#vPu`+Bf zn$*Bx!bnKH-41&eXHl!a2gasu#lIUielmdbbLIa1Mzu zIC$>asqCxhb{}-rw%N?@;MZW}Z|0a59>aTi@N}D>=IpclSw*Nw^p89ztGXLKt~lydq7%N9mxLs7 zv4sM3BMNJ-KCZIW!fOpDeaPprm4(cSb6xXQ>G4PC3P{Y3s`x|%vclg&uO_2Y$1)~D zv1A*D4oW&Hh#`G;!E>szCM%!r{e8RC?(9`loAfSgn^w4Y^U()vfh`%|B7OJdiEhcS zy|ca&c)(TgT>NF2ha;%4e#7$oJjE9-s*v0hddj*V40p94A5r1M`o|R;{?%sWVUu-U zK5>Mbi7UU|J%TaL^Vfk3YobkiukN_J@i#~=gq$;cQEyp1L4aH@^O`y4^(-vqVbiBP z4W9sH#K1gttao-9BU! z?)Ww6W?(-Cd}-zL*gUacpj93+sVYNT{N#}px)ZPNn(l9u?_J&5eAAbRy|SqS+OhwQ zb~!)Iv}Yue)DeB@3oBzJ3htLnjLkH(wL+#K3Pvwlsv6EzWftZ|>}!;0E$E`c54gPs!`Pc!)} zCt@c%J9Qa?!5?XSuG2(cFoiF41l>YbbMwel3^bov^o_WOF{w@GiZ8!19lz~b*szdi zP@*xM1i3eXTUk*IZ?r1{UYjR_+I7DEOYIFK{TH&6Q*V-Wk~bR3LL0->8wWJ;vhAHG zyA=GV3-yh=&(L)uo+HbYerJ}xZDfIs=KOW7dzx@N(2NUfUNC$(0Kua4Bl-;p`tL%V*EFes4 z^2K=9cU-AA^73re1ac8y=4~WT$aGEtjoertf4>YaP6pIpic8ED_xv`ee?tnN%!?>b+GE!+TSxB zgXMK@Yj3#4|NLsy5c8H3V;|Je$e8v8jY|%2F9Nu|S(mSa>k3ZfK^`5W95*Z`ue?GP zGNw1{jMZ%Dpt>KwoX$%8^>F^)O-~&U-X{+<+9QqU28h zwYg7k_i7N8&NHKD)^vvyqzGO56cY~^m!lT$jkW#l^3#BJKdP~UF{-aofsld{MISlK zzT8~QTowP3_R-E2U-&mua581b-bk@&A#-oDpvS5IrJKMnzn+cNKZ8X>F|Z1T2;*}5 zgq>Ge{G~2s9A}9H8Ol1qZ|c#1Yvu295&k#Lhk(SO5Jw=Hs?PV{o&NVe zlTk{?QP1Zm#7@$ry14)0y2&(xC~UPr|Fv2$$*X^89CRUvc|Ve#Hy-AN@c=`y!9wl@ zd`d+BO>^ZWFE!Xx+!da9`QM%X_dc!?L@-3Fr)^8gR$avDKMcbxFVVyC2G#VQpZ_>e z1mv?6A%L);%Y%xx3-GHWUkjguqS!EWlZOaoP5z%==7g)o0;wPbq55xz^}ikK|Knf? znMQDjAFba-ozMSWe}&tn1}?O{Y@hydwEyYB|8;!v8NFHA{HOUqG5G&ge=}PAsLp(P z-1UFF)IXTU|MME3zyC73tQtg=;Q#AB|Mf1DpYX%LhoW?Bi6L}Xd7-Ra?F4`=v@0e!b^C!c7&w(U7;XpM5l7&a%1G` z%yRIblmaNigdCQ0;>Ecy-FkDW&~P+uvRJkO~ch zKDN%eH>G07TJ~VPKlxyT>bv#tHN3f?54u}+2anSr%n!#YGh;14RlB{LW9e^{UX`zG z#<(}XX?EanTL`XN$hUb zdq*QHt)CfG9w2I?bGcnqN2V8kfzVQK=4Y zS9&TON;E{avfp#GsDG{U1T!n|@me4^4r9upiAPjdPjQvK+HNBD0-|r(!Q8) zIyUFWl@EfuyUK0Y!rFJth0f}-UwHN;gLj;Hw?;(DzM^l6YNM2K=ILmPv#FPgM-_N= zH+bgEp|CO}rnp|*7m~_NTezPXzk8ujA%OdqseGa+`m3$t$auurDWE^H?B}fEhU3ma zO%ibzV9%2?p|zpcmb_5>iB#zT-v1d4`||8odC$m4IecIoCV>3t;F9soQ;hH@Pc`YZ zolCsU2`z)lcA3|%QOh3KxrNZF!NwEVC{As?&X_DrVGQ}G{tdaxFoSSH=l~*QX)%z2 z-Z^%)r^=Do#iJp|;pgiR$9g_4+1^-Dxgiqk=pGQ-vQuxv5W9@`*;Qq;A5;fYy-jDW+@vM?R?UfOrDX-s2n{TmGJ$$XDDd3f$ z!8OuxVh+N;4J>c!t7P8MZ|CA>>Sz<${$2CF-&u5GyQU$AgzD?;%tS$+qlx|9VYK|Is{YPyjwQ~Gp_^`_Mv95fIrdNWLc zkDV=Ly2h*5Cky*4uqx;tnyitLGDjK7N1Cxe^U(M2`I{Ec9ud|YmK0q^$>%cbf9>W& zj5yU8ew8cz(whUTTi9pQ(na7PMGMXF>8lO%Z!<-MSuLBw${#OwCS-xWdA`Nn$e*?; zY$GE@H93^Q&|2!I@}}K+X=mP%iy-IPj6G2%Owp0y9|sf4u@B97l-?ErObMbkOF24T zHYi|35&Diyaqatt<-OR~+gbq8z&~a0oOSjkD2EeLcTNRAl~dzrlMnjRI2FX@uRCnR z5xJ!R!Rynx6I<<^a>+g1O1L->7+t`qwRN$?ui}v^oE&P^zx(+`h_l#!hTmXF^*-;` z6!3VFnPyP471#*ce0FL<4CKTRY0>m@Trs2+}u?->s`BVz$qqZTd{WJF^S=U$hrT9LTrb;dXn`PLGeP zVSmG&^2IO3&Z{_@@kX+Q0Cml?cBplp#9(gDU@#!z_8GQ%UdTUh9UZF2TAqAUCm`?+ z%}`-{#NWX{&jTz;#FdS29V_ne9+l9)-=d6fzTEov#lYbzx9tX0nK~~E>}q!}gD=v* zXD$sm6l8`s_|FxhcIT=<{r(ASKsM(3EB$*$?2ldn8|K?Mz@96IZhx7)PJanyv;r`h z?p{5!f80_WY0gv#6`mT_4mn9{iRko?;Uh!sj9!C%z1}7wE+g@-K0yZ!F*e_rFQEHD zob0MiW*8(L%)$cs8{N8p0W+auv;q?VZ ztYDHgGYhmj4gta7+I;`DHXccU-@~J7G8UgJ3@2?#fKPtZfyK7SDtK5?-75wYsTWmO$E&Fh)hrfqs#& z|49DtutKe|4tMDSLi{BAN2btIKb*!D(K!~kdV6FCebH0HnPdDGC=$c13~0Ie6y%e$ zy%@$Tst9^#5-mS6UnC8DuhSgvygj7)itVCzmg#y^VjJnC`aBp#3vD9eNgP%lt_|9w z4La!53KNt-^r`=P2}_J5FPikqAn4AE{0+K)ecIdCCKx{MYyv+i%XY2yn?GV+chd!L zOCPboZ0HE54{&5szkX0HKP-R?o=$i+#FtY=Bl@ z`2HcQ!tFuHrSP_>Ab+UO4dpYbU1xP@Vd1uA0NdzZBNk+gx_+%mn4YDvG&mFKXKs9I zrQp41jvbp=2%FmZPB}+(cP_l{_57_cW?#D51w2@#XJ5ZfKk#h3^d1e=xU$`HLR6)>Ccw}3cwWpu zJ#>y7D3c8BJ2F~1G2|$~SMjdJ|f$eMw;tS`%lZ@Vlk z$?pbT(-DHziUl0foMTDGzSpsMH;^z2+xz;A^j68Duu1Kx|DEN>Sv+z|cUzK++@kSH zd+=DdxBKpmp>WTZUMc2W#jz`goiTTCBO+N3uot4hnnx!0;};w^I&&ZDe7qyjkFL(< z*dI))d3YT_8nW%1i>$~}cP}=zxHzAdt}&c5&1yjWF*TByoWQbF#apka{lN0 z-N;}h<;L5~QsThoNyBQ>P5o1FYsXF|(gJ%)@Mh)uxS87JiaiRu#s0wYS^GDHXP1xj z&9xSagW`xHHT7io@4^~sb-9a~3(TDmu3F^sH?|EWh{zy6l~tYSX#&rD@{qaaOPrqI zssgJBS!Vfaj>9~krlTb5qEAs(d9 zsH4hbP6I$e83@Bb@unxjD`$3riH@S{B9#)>g>zW}gWU%X^({g1lXioaQ_X=-m|$a% z$tJ1(&gX&4)jBkFW))tHfw}C)S!-wVukxGa8}o^oFsZVwi860-$%j9>8`+r8ufqVw zD~nSG)!jI5$s8G}y2%G}69&b$=84P~%a?_Pze?~kB%{15+45xmm#CEUoArq+lfO^t z?X()7kP3l!;#d8q0gsdCW&jO6euDweb{`XDE$A<9jJ!M987mp)+$-5u7+)>VLqzv| z_?qCQzp;i}AM#-Rg1eK_V1wpEHjQi1yitd~i!9rR=;0{(hzb)!AnC(XSvwKau}^4Z zI;#im^Um*$hLfZeK*=kW+aE7i_@B??!h&vQ{w)@;#p2wsE|z0JvNh5W80@B>4&o(} zJL^}#^H*u!kH?FL+)7e+x6~YPB%5b2dSX7W#v<;g(f`HL^gr+ zv%3!GK*gRd2GHs zW%Z0J5Whq}6hGXa8y-JbEHcN%SD@7b>SMEz4gI`g5N3AELS>D1eh;l!3*>o5u-bUB z-y@b!MWujA(uvwH12TqX&O4X2fp7Vld#@i3-imvZVdX)sy4SyZ=~#5e04dD05df9jnB3{+y1=WNs1vrO$$(=v*$YD ztkqA~q^f{>&{VmQKWqC>miuBR^fMInH>tKY`nqT@%Mw+_`0cWZ1N z#m3WvzHQEyKNLpDCUy>Np2RaG5+7SKKdiq3U&MvO0c_Itnkg7u4eZX*<$9As~%axsC=p#`hA zhQpU;Yvzf*e5*S51S#0>o%dvTaN7#dC)ATw;7(*-?#xZ9BR)qIqFANF^+J8*(#NTb;@|(KK<-^A7QAS6+u(BEAxmEW2JC2iGbk4>S0@Jp~?Go|4 zM7zG;Sxs(q{FTA#oz)V+)^)pLwS$vM{FltF3SGp_e%>HY$#bMT|a3tzb*}q5LwM2*MkTtN-$JXOcx|;&aVX7{$6Ew7xv-i@%woo!usW{lA*EJXJpQK27omsg7Dd2#ndL*$q_YE z(%j?_wMS2%X;v+`HyqrN&dNWJk}gwj8@=ZK_*;nYskrcNbI#}FRW_T#|qm)2!9DNTF3UV zkqOCTG3TP}oc=TEuY2OX0UtS=8UZI3V^e=UXz#|onsvd8vcCSv3v?oZV};{aw;TuH zFh8Z?5_ct$KIQaLMDtT~M$HWvnM8zrfAbY58ew08D6i7l+kWn4MKuU|^}8#p ze7?5!c1GyIy|@eM%BGAmvtrBXn@xGsNW{NfZy-?Kf2#Iss<|76eFvP1*T!=lRwD6S zha(i|+wn&XxEYA6AQN3!yo$a&*c)NKFY?sk?lMG`5*(CJ=DyI8;uQd2d`xKooUzqM zsY|=Xi3irXtTTJ3!H7+-%-b6eLw(E6Kr}p%&q!j7hft!R3ZGwY9!|GpO3Tvz(~vd>Q18~80~Xh!SAKlLeG`2}kJ zc2g7dVOW7;p1fVs%o^LdH6RzHg*Bqyc>u!!?mBruAoE>fxjZajYb{?X zqe_6hL4YNZUR*90jAvkM7X3Q|BXpy0_K&xHXpw}-6Q?pzrs{)1SS9m&N6>D?nO;Y? zRzeP}QUUu%bH?AYEx%rW$TBc+xl(DO##8#CCFL{klydu3wYRKngKElP&7;A_Yj1tp z2neoI&M9)dCVg>UiOklW%Gt!ef5a#U?D56_z~rV!qUavV7&YxP(Rb9ZDzX1q!>tK< z274@pq3+Qyz1CU0Hin`;IMBIpJhy~JF`F-YKyaVM#P{{rC0<>HV^5u?uF}yQ%up~gq9#%aE93{1(Ijbg4=olgjTQjyC+49bx!xg?gtgFt0f;PiT8 z44za^rQNlYV#+^MsD{@1glG?zdI@WNoEJ*_EC2IaeyMjCF|oI__x>%B%=^Q8%ik4% z_A;`?FP!ygV7f!|C)c~pGyt~ddaqNhi(!rrd{&{fkD51C>AKb%2|NH_W92%MZP&3K zFB}Ne!?%7e#g=3=-7Y^(H5J;J@}npC3MzFmtyGika{sC!(3Dee>bh(?N~uZrmMw7F zv~8Tb=brY<;D8-bBc;DjsK0<_*%2kHeK5iYGJ3roWZ*!TYaX33Z>~j>Nj(RTNN6KM zdU_VV@L7x~OL0<_2=Cww#@*G5ozTD{cD+dM91m>$VHR8`-mdK*kF`=6oZVS*{OuYgtCm#h{gV(Fv(3rDEA zdvc2|IC;Q)BfzpKDszjLIAe+$_&dl==|Ziwt$8oGMlHuvM)+SoOX+IIICUVLp}WHR znXlLTh)ivTH~`NW^GZeU$G_90$H86Y_p?uWEwT6;@lu$^X;;IuKlrQ7u@sJP1eykl}XYHJRm__%(7x~8B_ z!NLty^JKkrT&vTIN{C8C_R#Yi0@}7-<^Ss@U|JFT6wbNyB-6lZ%Tj*&C-Q1+KN&oD z#(;C^b8z}0C9%Kvn{w)x=S%yRLh=)=;ro(1R(1+rYZk~xi%nvHPDo>{g`wkn?(QO; z)Z{o*M}~&12Pf?kxo1~L))VBQmY3O*Uxk2!q%`X!J)PdOpEzlOvW(G5U(4ux41rE} zruuwi0zdXI#2WipAcDs)$w{!ws*|`6L(|R?A!ce6L?h@nH6p`SD%Zg)l9xmeljw#9 zlM@!%P*;h~EN~V|chR}DrdKo>u_SlP)VK^6Wd`(arYw-zx66eiq`hfw7}OGId7_L; zn`0ED0x4+UX!2>7PT3QM98qolwtF7aY*5)j$-bL_G=?sDEavhBsDKtukB=*&?krGU zX9OuV7cDRAa}A=yC4ZxpjYew4S^SSUXPO>M2Vt zJRL6wv6A|SEV`+1ZIVJ9UU4b8M^u*ZTMkqxRjh1Wl0e+EMvh16KbCYgDHHH2-@Drz zAm%wIaVW{{)zQ`z#NR=C)<7-SY&D~UyzOrkXA6Fuq;xhE8T#@+s*RM{>PD$`VYrNLPxb0$1xdF7DWj-;uN=4_Z6sKV76S|y_Y!=d!vva8^v%+ zn{7M*ztrpP(6|EWiHhs7gzBFR(Q}F97lMg}1;C2jKqnEp?&L($#G)4G*=}A!nyeM1 zNy@j;WH!ysn7I*18u21oA2jRMM}F?xZxFEsN}}bJJzs8F)dF=<@gy01)ZtTGKNj!UjCBR`hY3*8n)b9Me2<{Pj;egL86TmF3W zMN^uG$kQ)`h7bKg)!22|MxbKlSKP)eTDBH`GXES7Onk)4^&VMyC^oB{xzQo~zsVhY zO~;rWu@mnNcjxV2X|Z|6uP=VLt7!+2x+m*X_fNF0nQ}lY9j~`<>j-izvF5v5qx0D=yX?>N=d@AIK3CnnXJj zA55&-&N9yo1CH(LT|>VfxBt1gpfF5n05L`9H;b8{yXRB!LNTKLpPD~C6uq(gZb=A6 z)o_%gZNb5^s$9O;!h#ODQ!tt&K>R37(j{>m9=2OS+rlIPpH>{KjzYF`5(bj0gB@cm^#>AXyESJcAHvf{!YT(-alb^fCOT6 z4lJWpZQ)M=C3yZ+?d8;0Vq2Ed9PW2YLD}$#%8J0}hG~V>s#831baaXtf^^cTN^b$1 z1(eoq!n;Bu&$XFFODV!D3TlkvsP1|+PwapPZPtf$mzDtEQ1=tANg)4dccVKRlswvxmn7ZcQOc;%kN837@&Oc@;&Rs-K z+VHD#t>U7~ML0pwpLFk`L~q5)XYm0^0mf!PlVa&eJuSbZxaExFxz2hE1#jSNqis?W zB3*;|+Z%|4W%}jRYm`Xj#-YDiaVerKBf3HCr;EPVV#%-X!E|zhn_mnXCc8k#3a>jh zgeWj|jOAROzmXm}P5Y)^L@3g~dkT{;FM`+i`jML}H>W{^K&0cW@2RawbzIdoyW)NK z@ymIe8(BX@E^Y^|+Hx}$Zpj(;eKx(Bzn8wH4tx3ikXL62TG-t3F>}qK~hwk!KK1nI#m}62eja?%{Z2 z6#>QVLVxq*TPJ75GtF%&pfS1f*f`6oKPkqqADB2=h>&XCsr?#W-tQ53F3uzKR%R4Bm|M454f z*|-ju7RIMDFsEmk0fT|$-w&f^%Ct6NlBiUzm(A6s1exWltno5^Hlr|f-;YztdcmB$ zxz9*g@as;Vza~#4D_(r(cv|z^XIcEax7U+vyqr%Id0s%FaoLMgqZMe5KPkwlzFpjx zqjy&wjK-D($v2)X*?wBR(Q-0b=$31%DKDy#rhQ3f=WlA*vNrkL()2l4%#`Cl$R>x@ z66#G^PHq!^yQIQT<5XsU8E8#(IMehPR6qr|^A}&lDZ(rPBjb#9uSLk%#`MMXxw=-) zMPG9|;H;f4?2POcNrt4Ns=kN%4numFc{*i$u4T-;7aQq(7_zz=ePNkDmTzxIu})7z z*PMpYRHhXVeq;OiU85tW?_jBffFd7?3WT|iEv!bRaAGeg6+yezCL;ZUcl*X4&1Flj z#3@_m>FUSV`IRLq%D9+r`5t(E=w{;bmFXiLH1df&_5zlP*a98hA-1!=qwp~?9&vmuHyxzOQ8@2} zpy*RrK8!Bb4HUBltT#PA%d{7OI|DdCXjrn$(rSs!cL>)1b7j*Bc9E3Xo<53pI6^$L z%tl|uG{7@$=RS+ch~%I!-EZG=qCJ$XBBru!)(>Z)Q02~PdPm3~u+Pb{blK(t)mki!UM-gv~6E|7=vW*z&o3A<`jRhCX=12uD0e5ek z%D*BL_fqWT_*HRts3-o13-*0X@>@21v{*Zkd+hdlFJDe&-qr$d$cu_SO$+Ubsqqxn z$)nzJKj6~S*Pr(LYnu^)4yX6d{kHXD+7#I2z3{mYdpQv0=O8!tRosPb814M&^?nMk z#&@2B+>-Jl=+gb+k~JmIvJAVuUuPXwn&Y0z&sQAN2N2oPUz^|KMlsmC>*7$`Q~#bn z(qmeVR{Yz#1x@NN6>N4)9}()GD<^AI1vm!qhx~Av?uvAsJ)G({(mK*cEd!eU#sdi1 zOSSR5kmsYvSn;Db#^^_MP^UUe?;01?nlxFuV_#yo-1ecy&&3pXzLkN;U5xsP`_B$qk5FLI%zs ze%n2<7@pgcOi)Ay)Uftk3^rIUbbLkmmdjjUskZGOs(|m`L10VbMiU5_XkzGd;jTN=whTvH3jo6jXnQWkG07h5q(?W~` zfaqz{oOwqnih8u`)yd_l%hx6y&nOsJbNZDAC+CgA#!;a{^Y@FmEUkq$TbB4jQ{F<# zXfmAWxC7(QDx#8L8b%?PH$M@|kIefSX^DPnP7~2Bu{agYPgj1L5CF_@&q)~)4C&$o z4fls>Hc2Z9SPj$=T_*|ea{GM_xBB(JmHk{Q5bub*6Jm-yu%jI={^D{CQ)x zP7Ttwq1cgecW?jT=nd5N)h9IWt41c#Wwm!74mZfiAoUrGdD_+;s$2Ifzkjpd*JL3! zoRb>B))e`(Xv}goP(y#x8H|9n;wA|f1)=3DjFBQ1c6~~VqJx$<@VN(q=0!4Qdvxg% z)FP92jKPE#I&Rb^M0g~igCVPj3!@GE@_juZoA@&k4?vRaPS^^U7Sm=|@M>t+^TUy9 z+$GK|zT}ZYgF#!Nw8ZGq-89fB7Hl7YK5LLP`r?4}Hw4|A`9>J< zb3-5?CdU!~cK~Zs&qPQw8)^rvxhp&zIp+~y1xCg>84vfe^T@3{iaO(x#iz{GDIZ`v zdo7Uo%0XBAmyj$2)Zmww0bKj}=0romX>Lj`Pg66edk z+7tPwhS1_z&h0_hQn{0^SALURuEVb$PW7@6+uWazo66TbInkL`EzwZxiFDwBRCUSE zE(IRBhKrn8!%HY9>y*XDomMvAzgX~f|EmY~J$mR>&N7imCrIC7b9; zQN<003(LKI4k*KGn;v=)l{5D#WO)2ax~Ev8IV?BdX!K|^YIPN#LMcIhS|{P* zFnX+6Q8j!NG57@|Z9c7b<+uId!=7f2tyS^ZK*fCOy)0!rMnYxWOPm%{STr)xDNdMD zWti*$v+Oc(D}m}O`zOmC4)O2p53Icn3eik(`wP8;uN%QTc)9nh&B^yeokrN`Sbf

=z@|mcvE%yYCZN-q`l*#+;T_wKQCRb~|01EZE|enaD>y z{+ilIpPAcmGp90~H!*2N(az1e`SMk@q4reF2i$TY!O_zlnZKZKoG)O|4gl<@-$_;y z@!r&J;F6)+;3>yo^p)h?q)79X58S3Bm{)3?qZ2?O(8$sDE=$21$w8JA?&on z9I8=pBVCr=e!a5b6+08Cw^uzeI2DiQuCxRQ;Q1sMjSVKO|NH|jgCs2_LARQ^tB#1I z+5XFQEirVpdjOx8P4B&VjmsCF-UDoRrSbmTeFyPWi<&2{H2;ge_l#<)YuI(EBGMF< zNCyEyDN#^*vmhb}DAJ^aj`SXC0*Huo6s1TLX(GMX0MbGaz4sP+3rQd$WXI?I-o2mi zoc%rL?0x>7v41c|24$>dWo9wgocDEKs{a7eWYCf+t?8b7Y8?n#>`BNJNB<29`7Yqf z)GRM}#xHQ@vCO2ReCeqbN?S5*n*3fG*K@jc#KmmTSE$9E@Fp4!uxQzZh8Kc%e zybNyIYoLdQBKzS>9W{z<>G)xYvDAM8hz2uS3`(&nOY8wdSxVFFJ8q;^|godEMcPah@{_#&gY9UcU3K2Aadu#p+F6n;{P1`e@deLKYsK7*XP{z1x4UFa-YG4UWs#_Fp%Y-xpJZNeD`2{fname z351Hv`%+dcUHkhd;yK0fm=F`c>&=!5Wi_5)ye4T#V(#c? z=MeCOb9hpKzwe2a&wnTU*!|lK|Me+Iy*NuF7hu>%VgLdOjwio`U6?YsYv=5D)>}q&lbmN<>REa!uu^xBX7>n3#)q21SA+G9yqN_rKLRH$MVvQ zuLU?hp5CcSlG-;ZX(7tU*H?~z1P{UD5P8YCuey}>O}4edKg{Xw!<|WZ&jBA zDrjCpLJRlH^Wf95z2dZXl?9VUE@>Par#){Vk;YA?sAG+1t{Dw=c; z?0XpP6=Wq9XPmKtG?xvSV~d3CERR?+x98obI3&dFLBGpSI;RZs4a_)|p9cF~&r&&%m}?Ywts35PzveFd-7n5qgU);0dkfV*T~))9yplfp7<&*GM{{ z`l~Y(yB7p_26jODna;>!0an>P(7&*pD*ljW?l*C_0embiF5}@nw4r_vziewdt-0$i zJVEp*BdHyy)A&lJPnC~rg-Z^z``=tsu-=1S-!?24+qT9U)8hB1;qD=T-4a(W{mEx8 zMF!<=bvDf=Qy0`flql#s?qA)=_b(ip_3&NTHOV$$!qBiUN75C{$~08nBsT)bskb!l zO>GDC$<>7s%Z`|BLkWiuKA*q0ZXh*&#rl7+I@_9QcpwaWYJ`YQUjGssCOrPN_%rVQ zrK@oOekuItL;)Qch=254Q^&oD?bDH8(@=147N_dIAZ3kBn5DQxM!bpVTH5LJ{=S?< z<4#Pd7M7<}C7n8XqCe0OQPJ;w&XB3C6kpA5s01EkqgJ95friJM(`6`IDCv>pK3Pz$ z!YCB(h1Ep57>*~~p;$Za^MN4-ON*7~envlyXij*4)QaWWUY$LiL|@Ff&~OOcw%&Wy zD0zLYKLRa|m5MSdcBPQ!r`>t_r)*+4y!e_)erjBAngI7$OU{C}T>ob5f>WTL2f=t| zikqQ5!}QHrwyvFYcKZ+q6)<@!(V&~hyp8Z!;JkcYRf|40@NjC$_Q6hXI2x~i<9Az9 z26yKdU|bKsr<~yaJC`&3U%ZR2uBm*N=0VpA5@dRwZIjG1CBPc#yx4;TyIIi*db;h6 zO1QpB>Vavj8D8WsQy&UZ{leu|&FAs<2`@;n%B$v+3`Wpsc z#&ai&aMZ>$CFZEcO5CLO@tv#aJrzo9iX?uDLUj860zKL0u1PQu2k{yJn%9?e@RpBm z+U9DW{{lWaKPqUWaR~_YDg2uC7f;h<>^W{dwooMT$|M`yG)W7Kh6eJR$9_E?LWL!> zfIR%qhIj)}D$N-xbNHM42|KQ@FMw-OhGEO*2bGlI6~$dhl(a#-T0?)@uThHlu5~j@j1R@K4H$U(~z5yR$=!tPWrEj%9c5wwUjFKd1p9QK~e#qq-&$fCYsCI!TfoBpc3@1*j$H zd#|T%qH=H3AtjTKs)u$o5vP6546p7@k5(I)M$vXSD@>Ix>szMh*YQt4#%#Eneckg* zw9*l)2WL!S%{UVB62IPxPeuwH@Wyq&YZ1BJS8U9#l<}t3w~v^Y6uSfiU~&>4nTv>m%SI3@57yM zoDLUqrZWg&C! z5)BzlN?Z$I5aF<-cGo0qo)q@ZpZBy~F+AHSizOk$;~FCkc~yEmGZIyz1YaT2as$r_X=^_K=FCG_+@16SD-jX?XnNRJ=9cVn_qoI5WT{t z05Awkr0R4`GDig0wWE8RIh!VXEEGIlocHSr_cCD>m{+4kI+WYOm&gN4*_st*h;Cx^ zRHQp^U_w>oQPUuMwfDFFR{;Wx!wXmdW8`!A+bPt&cl47DpZ&*)LZ7!2_eKd0D-~-^ zK1#v`ojEP((WtgvlX2&ga8UFJ!{kuDS3A63VP+8}*I!pko!)eLHQ-CdpXg6mYvXc9 zQ}|(Kb1$8v^+F4m2%5r&tE%Z?T@wt_NGU-vdDE{6x(I1u6RtTj4hX?a#)dU z-8;kx&M%oGcw$`|*};_{Y=<2|mEFYyPs_CTjN>!)_WB-(dS7B=pEc`O4TEb>NNcuC z@J`+jhA>-moiaxsJ*cRhBMV?J+uvbReb<{PyWiST!__P6&X)$z60APY=J6@*Yk1Vq z4?U5&?`^@!-PP&aaqS5#Z)Vkw0Fe81U^q^p9MGY%^NPW~3$rGWbvTi`eug~r(ooTV zK?sAev`h%CGfvrFJRn_=CW;_?yk9z0J)T6$_YcP&d+K)C8mv6mIaA_qk~0U<&W2bhT8b-zj=UG_Xv|U+p2onz2%qq_wXXG! zvn4mQU}g-c>C-#YesdIOi@n%Wi)=<_ithSbgU9baVe&K*j}t8M;*PG*dfo3;cyhle zy&#a=>9!&+MI4QXGLMp&U0FK7b%l4UxYS9I@$S9(&l;~l59?>u$$UUbPR!za)}Na8 zr%&B>RY{~r0Kk^=nIG_7V^GtS=1>KaFSz#47=1$Xtne3@cbzGr!oWC&GSL7Nx>e`B zF^oSewB7HcYt!MT3#2dzE12kv8IhHTD&|7MCmWvsXgv?pBa=bj**t1xX~Oj#%t*n3 zAL)oy`29UqQDS}IjZ*JTnDC_ufvLVu@2+$HfWjGxXWyNe06XdWe$L*1of>Nmmyn z&RBnDwfrNnKLZ1~q6Cd)zT;E`K#lsDHSRzUNjC)hu}XBe&jbe^1RR>+8pBRi92f2j zw#3RPZk;|3Q#~_QeMcP4mS;)?(ULo!9bVA_2LWD=<$H^bqZ~ySU3zt#);FU^Jcm(^ zYmsRA1=m#1JfB%~-d0@~s7FCV^`ne@N+AnU&$h}S&%x!I61bm@nz!xJO}&jE-4`_@ z#4YRf2qw%~ERFwp=;)1OhR=6h8-8_5 zzbeEI9E|RRipTY#Bmo)joztZ4;ea_qKB#P-`FtGN1`TG4Qk6UCkF&i7)}?(In-llEs%@z*kd?`*Mj^m&Z8vP{@sN)h|7MI-4ej|DRm z-;DU_T@%7oUc@$(K355Z>8>TLS z^-V%IOiuLoWEmmqh-@!J59;MdD*OF$9JdEv&(_Gl(bzUQZVY#Sg)Z1P(E|2fOUflA zU~>yz5^-I5S)gRYbHa&AWd&nek~gWln(mAB z)aZCBa2f`Kz<#q@qtC2w51+3dL0Hl%3%O;luMq6d|DY0r&A8*RP$wZvKR&GXdQU=! zbdKiE>z_K0BDc0f=Y){2Pj1XbEF{!@9!?zFwd33$?eK}&&$~Fr=IgFi=E^HlGU8)i zF1jjyBw5rbhAzOAr{)@19vtnj7PNelCW_!M#qCUoTIgvE(LDfn^hHV z@bY?izxvm@$mv3zuQOZCdufz8smjBYiUe^msZ|2?7=$DWo^w6B+YPGSFkF=seGcs% z(30G6LHcYVU@-QB9dk2i#4*xnQh?lo5gKL0_be)A-QM}J}KI`G~nLLBg3O+YWh zR2>>oSeJ_CR70G;0@|w}4b;GT{(h6a`lQa)6@C0!ON^a5B&X2|SaE;WTI+#IYqFacZ78Hew zqJs!t$fHm)lJtTKf7U@x@=5HPJXl{ik6ofE5oxA)3+GKfL5oy```q5PB+s)#ss_!% z&7V%aCsdZiM`;k`38eWW@pQ>NRxpr9r!a4e0WPTH8Y2F6| z^oe6AB(ZNlR3``jX5=o$R>~+wpG@lXv(c(1bGz~4C z;g7LW7ap(JlbBXZMCyjSOy6AV1w0akNUn+fez&?sU*jHlIy|t0^ut&fudg8JppIb= z5-wIW>6n5C6T+9j-r*YatV(!Hx+YHtcrHNeUJ*5&&`=&IeLb~Fy2o9et_t6FE%fjCB@GPWzm7?rI2`H?#K>j-xK{a`B4 zjhN6qApWy1RfU9I@L=MTO+W3Jf2J7sTPyf?79dRWR3Ab;;BJF(0Mlu}6t)M%cs$;? zJ-sKG=0AK->3A3P3a!z&Nc-V+nU69^OQ)!(nNrg7Rj=O4+`oV_cvK6QIRLr?T?)CIH+;_#>Peai z-g05FHrp{vv@Z8N>m9DAnA^rykUda@3d@Ra8A8ONX+KN{1&Q?leK64yBr*hwW2_|u zU^OWI{}5N`H&+^*#1H$HTA=PqQO`K|z&|5i(l7K_}3B~kD z2IebfeB76s>`?b%eb?tYvxj+!fnSI*MKjxSnzoL?(W29G=(R_US{eXf!e(I2ZqY=^ zlh-JJ_u~RR=cSK3GbhzE;aA2W#}N6cTpYC`l*=q%XS@@sn8)ZZ1P-3Eo{*X)?dXYJ zD@W?C-;Kx^x&~>;;pho{q+X|vFQWtTS9JR7=mWx>1T4a!I>0otvdCKEo=L9>>0d=a z()Y9lXLbb>ULxlaLwU!u@e(&rir`c7Rrw7?ygx}>@q_X~cw?w>Er##u^0B8AD1mwjJX9`eVE`%` zB(7=IAB)yb=pkpiUWuKP;C<63`juuoYigUn%oS0UrTuIaW=cV$?Z(-b8QgXmCaS zi~Hg4#07TKlY@#8ZQTXP)vPhLv_z?d!|ZDdjyIZ}mn?b3E4n3;oDimqBBVr)^a9?3 z>BrU48wN&+W%D+Tp{ZX3!z}k-Nbc@|BX&03yp9mrZ)}?rnfBIgpWu8U8&l@Kij#F! zO|l7qAMasQJDhKBP@DMFNpcE}OMr5Zo5`e6)Yv-}y!j_%D8yFyN2P*-bt^x>b|&>` zo{SO5C|U3N(AA$BHq`^pA>C1O&GUyozA?@Ck~TUWH!lypS&8~m%5MnMt#AMRcK@Qk zXlh6$_l!d{l3@U+Nw9vt@v;;?4`F!Sr^v8&>NEC*87x3<*AMM;>k);OE_L{pi@6~- zJ{9R~Zi01Zj?Y>~u<9B&GWp@XH%teeatlX1tR~R(5cYlfB zjpUd4wJax9mbRc2xE2!(vQzFLcrf!Dh#P=UIu%Y153)w6-)N5yYu4hy;mve4==aFu z>lG$CjoOtRK ztz-n=UW~OWCB=8Qy)3j9bqYJ5l1vpW;Ad(b)@HVXWEA*8ygN^SB!ewQm!X`}3xC^- zjINCQ4WBVIY5a{}pHZDnzhG{SJ9`V@j8M9VtJ6BS@v&*VQE3rgxksBCYicjB7`XGQ zQ2!eIS)!ZXrzr2bFrLR&FV#6*t~RqN5xzZN>*EHiud zcQcGc5hTt^w=0r%TLWLlIU)T@1DRCcb!9kQ&-f;io7C>wWPN5VyxpcLmd1rNzp2cm&LAn{A7+9GthMQn#=J4%A`zaPr;3pj?!+M?X(h zY*46w!2^0=5|aFN=uMdTu5|8a(d1oS`sA1G-fTIG?DsXHuH(ne9f;l*7xzRB%FvW? zcE~5EOVYXVg`vRiS24D%^iSt+#fU)nZ58rLlC-`Vx;cM=eGN&FmC4yHOI%zC+ejEV z9QA&k7vj-FeJd=kp951bVQ@7%FWII@- z&h^_5cQJnuW1*~v<1Eg#7@jvQh#&@yB3nn6av=%nf95R_QsIdjh^5m$F*UpKk`bg72B}}P;QDsv z*cr3XVZmFqA1`fp#5MNK-lR2KY>?UV_jWT4M6}Pag!)B)YOj$r`}kW5H(R!30qYLB zurAGz4sO~@8N#GCS=7Ah9xPLcbJXFGCeAwUeyJb!^W`bRHuARc_)IZ$-GZ<*hC2lA z-}ZRgG9xPVc7nXi<8m}mZqZb%v*TFz*6o)UOy*OZ9&&80@N(0T00F}Ay~&yWj7=3R z@9~HZ_H82>!4t<}qR`h?0`cjcU7B~fKTGG2jHliRmqVj3rJknjWS#RF?c zj_9UyAkpbx+p7h27eLLA-Au3wenOm@XX+3y{Rs3CNc%|4#+ne@b?40_OCnGS?11y! z<@_1OIA>w$FFCj9oxFXuZ3Pqn9t5lfhBq2b1IaN0`F!_2^d6dko}kGq@WI_O%D;TG z2Zf&w4LJd}Pi-Ie VPl$?;^mwK#{n@Ky&f7LjN@5Llg(gZX%K^I!k_HkU<45LDtfF@Zf_jEgHB99EO#4T#=3qYstRPv6r z36&HwC4c*}2YiZpX5Ggu;tCoFfLp*sR}#34TK~jPTL1Vb0QK#rBHbkiVYdsP&8t$< z_-Otb8Io4sqc*|_x&}0DV@f=?^vMwYH4#mv?-#Gn{M8 zL>_;=+GT2wKPtIUnXX7au;WH;s&oh0@@=T@J?5I$EU;b$xpG0J^Zn4hUt%}KZM!u& zlYl_YGJemua*t%m;_!B^z{{FjHqSuT15c9|8Mg-Qx(8?Z@f3gxe_N?ir9@w<0Y1vc zjiKqK(~o8U;CtYGj9$xubMX}WdKvh)n54-j~VeoK{n(~70b zBHT|L(2ZA3hjl$H%QXdy-uYI)BWTGb5t9$PQ53mctRGd`q>T{ri*o*m3n(^Vs^cDX zbQ-tatX_Srn#9c$b#a40mm*A{7kjPkV>rzz%l%A@aw_8skGW*kmaGf_0sT3G4d`eI zXhmin34s2pD+{}cGbk3T6786)blN^rggBe(6dVN<`5Fqc03tjt9GMt`+=m9@JG``~ zzL7drFHw=GwUiM2`y&o=>ZtUNiu&&;G1#w|=ai*I4@Ho*_di(2@%Lq2JX!i)rok+v zwm*i~50F`X|DgMB%Fdn0g?E;=%*s=di&sVonA}x>hQ{dE2yRHvpDIamy zVCl@n-6;%d;Mdx&ScW76o%nhQB>~JKHMePl`Bm%9E?ibo{LD|1P*i$Xah7}+#&|cy z$pl7ISJO=;nxEZx+VoaPEFU4+KK_{8KX0q+>3$t}Nn^Ica@X$JcKYP0P+0U-!w>5& z;=!e3LBks)y5KN=sk`^?hk3Ex0{#LLUl86~2y?KMap+@M*5@}pvhp~S+p!cF^Gmd0 zoq@6$pcMj7$0|y=rS*rWUclF>lYYd)xjCCYqeHJE44}3=n62^$OgUHgMUpv=e-wx` zxjMSCc8!dzECk-C2L|4k$L9EgPa|FUN>(ncGkb`d5sJX58Y7SxY zjLis~Yyp$?PHCKR{o3oogj@lVzCImfo5Ug4?P6z@NY;shO0NiR;I$Y(->2Soos=8U zx8-cC4KYkGW4~_#a% z?k*Fq4RuL^BU??a$2uh`H)nKtvc|yW(~Y|c{49gP!G2RkQn`3qZ%7P;A|CTdb+O_S~@W%IRG8YE2-#kH9Qggr|5okscvQyU7ckKy31zgfoAD6S$ivVo_ zLZ&}iu78z*5aDQo&&kk1E3a=pDGATN>7mtmMee$o>46nom|wdxT$FN1DkKqbsh+ul zxgdC%x@FIJooABDADGp>pj?_LmQ+Vx;p&oic}$_hsP{sAnEn1}#e=P1kNeNrSF}Va zFusqrPeZm?YQvvAc8Qi<>0bDgw{k$K>pVuk>M_4HP_F?)ZOpWdw3UQuHI;%cy(t7( zCyy%}qRL3u>E;Brs^ zIzYa2ME+jum;Ud*P)Ag6{2<=gh@278Da=$r@ z%=3tjbWthRAhyJhb*cN2rJQ~;V;oCEX4(SvxBIk&CF6F#xIO&XcYSSIEi7LhG|~+4 zj}3WlJG_WGF4(7UOPK~3Mpd}oKz5WgDl`TPaU6n_U%wSur780=y^=B)woe%xc4>-E zWq$)_Z~Vt$C)4=sjP_2+qGxZ($LB8^748RKd6ZgElZaFF!x_&#R%PnKbdC@uik`(( zq@?PmQ1|Ipb1)LRT0_bX5_6u)mYF|K^IXTz__2iT61W1>A-IvVff#t0(A*<#5jkr!uWN=2(!o z_?QgvB~f~NIChdk-TCM3wL>oYN_DJx$*8fN0k=0e^%V0AuFDLYR}8#4*!<7Q{A^(N zhM?Lqh+EKqX&N6Ya>(PDa3%L5%+1qYXcNeykSVt4p zaDrp?d`SBS3s`w6S!_r)gc|5mTv6t2`MSHr#7i&gM*;tp{qoh(qO_II__#IXkz=kw zNw{dQz)E>K)%CiQ`zx%G0>4U*fs`FU%pjd3i2Hr>D)ZbWU?A;rVgw#g_Pa97XQ}Ic zm0jRj>eJE0GHcRg=6+hxsdmdP9%}l8c{{{}N}>jS>AC=ghq3(4L*35X$h_0o>y+O! zPywZoK3rMwgX+VekkJ$@1|(F0=HQ>n_rJIMnC?p8m9P4*6Qw#>4t(YN+QP%0&n@lB z)c?ZC7$fcd?`%m~n&ie_`^a>Nwjl)J3ewhnr1T2CQw!XiaNR@b0rcf|1CS5a2R@$eo z|6_Nw_3&C@Qv!~lFqU-)bN+3fD zi!xJ<`IGy!8dT5cP?x>n5RwohBNQkyo;6;Nr&`Yl;B?Lr@*`b>3@l{y)kEA+CQ4qr1k^MgdO#9Ti@?;|+ViLsoZo+FC7#6;@jj z*!Xw-KWDCcO+Cp!F*gkyNT6~KZ6VwqZ@B)$s6V8`&*+nNGoHEj9>|IGgl>;}5T+sC zAF_5|EOrjG&i5jH_z%cPr(>VoQ9SY4e~kkCLu%XA>BiCC-Jxy=oG&{z+-`U$YIfmq^?OC)lE3x!q{WYT$p< zic(K?B2mkJ;L@yJ`}^|F=YRUV{XO#jpB^tVULg(b3n?8IFoy4cHsk(IyZpaDi@QQ!1`tXpX97;qK_Jh=i490yZ^5riA!R~lwsi+zMA2zt7 zbCC$)zxjSbS^%Wg_mh7ExHpAJ%jYJve+fEHkB!>kK!vL9QjR~`4h+fO;}W8?2`K0PwOFMRmDeN!8dUA`gX4|aM@lRXehozMLSvghg z2$r;Nz3VO7;KEem54hBj5qobi5bQ&Yv?(@OwQYRj`yVNO(Ob;ba%wxLHncm?beDv7 z%CyM@obJQvhwHcS=o3`4#l05Fud_xV@HD(09qI-pUB6PU)H1mttr?8qdcbZ+__e3=NpxCP)nQi%5yGWdt&5l zi=ww!N2AN!-fWNvJWrXVlE!9H6XuO3;hQnrcN6`6Vtwh}%g`{__Wot+kclFhI^6B} z-??zghz@B>en@M6l*y)+_quF8BCfump;DzXf8sth;v3B28>fgw#RB!j0 z2PN&872uT2HT;n9UW$f|2^M{luw$9Rswl9@RQfMnhl2A%lCC4P3>zL`T+qp`kwml19Fe>Y= zS@3i+_Aus6P>pPDIkr(^t?jOrpxCwGlTW*{`5G#j!fimx`_q^jL{{#-Ei=_ho@tGR zl{P&pc1;GrJ%h3>N1F6nxOE}XU5D!xa~J!LLb{`q5K*|3IUARmpOCYWXh0TtAa{OJ$E)u@IGn9&h%~tGC87c>ZQa+(_w-N|QTwEpE*^*c7qn{? zgT~!TYq#4W~8ONtDgd24NyE0$2nfB?}9bcH0pw1R|)9OB{H8URcpVbIq2TRb) z{-jKKVyZ7OVArn7;?h)c1L>Y^Lz(n0cK?6@ueGe#X)>by@|ikZvElE$By2@hw$AkMICAP zXj2g3C5$Rn!vD?!2wN&dYAqy3{!H$L^rU!A{#FM7>J~)i_wUcdh}VR27c`l98$y*# zyf%j``t6rqh+kknhQJy$6*Ue-y@&MEo*XoJRUA(c0&IZI<_-xnsfR_juySIg;sLgd zZBsMA(8YRQI%IsfBo5ZsUY;Dz;w0u@7Ys9$sR|oCdxJw(Hy0w;6!??T)1$A(iVP6_ zSPG;A9PpECcxw5L+G6q5Khs@Lt6$$sUl%{jJ8Yx`gk&}GG73F+kfIa7on#?CAeuL@ z9*l6Zjelu3=%cdRKcT_#kfj7q3#nP9W1fDOAo)Mm4Rh|%Y@%B3zpNV&!(IV%bT79H z_e^&GNJ*0fy-SP|exp0d>y!V4Sk_%yZG_A?hkp;QJk#%jrb%Kr3H}h_tW%hO(x&%I zi5jqdy?a+xi{p5C3HdhU;pU1?!>Y>jy~bi{{+bE2^Q!zh@D*5wdu_O)JN@!`HjE9f z-F$gvCV22#N|=NTx}<_cZGjBWp13BPfi{VS!N05RC>m02r{ydI>0!Gk6p6s?hZybT z);|u829ks0Q&3|J3;|)VHzp#Q8_#IX#1a2!l>=_=u=20MxRq2hdVH$ zre$L>yljERww|1FmC$+J^kuQtn>Z2H{k2Pq0L8j(C12YJwCs=R*4Q~E{hxTA>zdEej~+sxMRJ=5^Z< z_Auh-DKX!7l4GU_fKH;WUrrvKzXJ_CiIgINe^aDYf>ag4C0j^-(qM!W{7_j@Hwra; z0N2+9%G4b`oQC)8pjn*c1I@W4&^dh)8B3?!jDhH-nKr=}zYjO%Nji+fQ$!e+Nm0qT zP)f641rWQg>vvSy!6pAi5mxcB$3i5QC8q%^0DrKf{U!XQF;QwuylZYRyBXFzBi}Q% z4;gR{HuhG+T&jD7AnI7pck^x#WWkTEwI`3d_YZvL-o?$%$XpAJ*GU(N)5jgd_8Exh zHQWpcNIvwQ+VKqIl{)NtCz6 zoj9~XcZ{$#bZ=HM21E+VZZIbbbCafZwdzzD;JCzw6(>!FNmzEk91+CTJXx((W_p-Y zOj3f(f=_P|BsCqu#8S!;{`W)_;sdu;BswDUJ83|5J};!&6v`qVPqZ}v6}3kbuPzB&IT*j5wy{RQrG)eAX<>gmB6y}dTG@`gaZy*%LEgF5+Nd3r29N(AE5Mb{ ztH$_li3vbyKzE_Dh+uPWY$=SLW}qjPFmiQXt4GCCE5qtd%RIHGP;q@Z_~uOUvG5rn z)z#mUrbY@}9+;L>IkNZRA_Q+*$)_vBd~5UD^cSlTBZ?_}OBiA%QIXKebtMx8dS zMTP<21}yrH;w})Rtu_O4dC5xAyH+W^N2s?$kOMUJaMr3^H_qKWjOAW5=$hy>>;Y(* zI^jkHU+#k!$c9tDoKtIo+nwx4+sGFKPMCAVx~nBGvGK`h z40W8R=C!#`TnM%5mC+JNfy{eJg?~$$;UYiePnL6zB7CXy>b4HY z#l$B4Bv$KO89AiRaLcW5L^SgIP7K8F=_e!2 z$-A)#?cydyte-vFgC_x9uSeu+{>NFytr_eU^pe#mOs2#*$VrX3Z(Eq~;Pg z>ETc&q=`u-oF2lRT$;vM@n+`!vxkBV#!=j(A)K_aqV#XawPPW2;$@*_>CL}BP+eT? z2-Pw3AksG>-vg=Pbn~Mb?_Vh$mZSxOfFywh1yOcUb#<+A@7DK2PFsaKPu~0ik2=85 zMo}@kplnmLNA5SaU@`K6XjpqBD@Quc;DCCZ{ND4KLQ?yT_BfJC{TkQIhe)*@tJ;85 zoyme_q&ADi;9heBch%W8GDk)*AvFhE{Onk*Jh_Sl0Qjg;vh1vJChCf={3*Gm*qds| zDMCZuwC(ZPBI08`xuet3J9E}tZpkSSBgj4bM@pIgekaH){u7ps0?AhN)vZl^ieGFO zC@}+{K>wCkkdOvG*VCF0S>86@r*gI9J303MLtKF;ADQPQ2=-;w{Eibc%N4EnB zzKP2Xj(r67c63%SY=$>>ee2q5{cwb+m$n_il-pDh^rR-b>SPY35>ygm!!OFFdQBGd zuXXH3N6@)or)GX@{tp8n^Y#lJie=17R*=#v9-7P`U2_}JMylX_f8IXX8_%h>ekpL} z6wU;C%o*5C##sVol(SW4^oXP={L+^;LDLnZuNUfxcK9q7Xrcw?AtyjMBM=y zb`|(T@62nad(avFmZbB$bKN^u!CxH7;>S8d>B&(ky_s1EGDNTS>iI##KX{np(w6cU zngh;w;LA$U`2cuCGufDypkRW({qk059r8hk-@i6q4^_@Ln~zj7HWyeN_n()U-n1Qd zp8RGYS$CSm36v!gXXG*x{mYl5yw7R$JCn$-%nIK9PA7F|<)$Ueg@@%=Y#&^vrEIg3 zx+q5f?&BjpzA37geK)h{!upg4Rm>8m)IRXXKk@z?_%9Qlmq=N{ z&yVCGPG6_V>U#ZZ0Pa;pVV@$YEmOXdg3E8fyY|Dt$Jpeq>lw(hh|)#_m+JHsqY^32 zyBY6y?foB}${)+A*Ex~O@a+SaoOYwX%51Y;xg3nXIICU8#b{P!_c+Y*<*Rx3R{NE{ z$igY@?x5XTwQ5bTvrb;mxACm#55^*_EK*z58*q<1)suyp>ENH?PbSJ>$p>%K${tv! z4uw6KzKQhqV9vGGlw*H%H7iz>kbCZ1}!u57OaD~do*-($s+sZflwWqQtT z0k|$a@H2B=8Fwi6F;~hpKUt5>%ZR6|{q4KcZddh?wjuvp1|}qQJ8Lsh)Kpomds91a zdoO@6DyAZyj;JuPG|k9VWa}d}{Rsmmj>)G_fKVlGN6~m~6_qQN-1~u0ALQ})#DkrB zTq&)HF(O)|YN4Mt1ADeeI}u*Oz?2HEahtj3`1=*!Fa1HoLQ$T_IJ-+LzwQvm6%TQ= zoFq8!2xPo{j1gU0(5kmM9)iiOWb6P<3k(pkv{}A=6UxWqp_m$;--ej7IFS;xTw-5N zfV;ybr?UB8BE-jX>8%uq_1v3i9UQ`-;BJy>zyx2PPt}(eX-K%Sztj{pcX7t}R;=DQ z8qCDKcCWW4dL_{EM$=GWkBd#-^yK}64jg5d-Ok+f{F5ET*2{6h8YCf!yx0ZHs^4~hi*4$sxa~)G|zJDGOH1b>LT#6d-p0IFeRv;+&wK` zsSuGGM76cZq|H2FoGRL-eCjV1FMK?1VksxiB=)ra#`;wBpGkcm=?W zJF>?nNN1!X<{r(o-#2qt|CzDH%D7c?g4Yn7c`&*Y;^9kG*9&&K((zU|#W>admqAH0 zjmNz0buw80c|qvTZ!a`0R8hDwDE%lGPO z#EIi~WhDjLuLW=NPJSPsX-+ep_*0GUvl}ARtyi9H|>UldZ+tG&G%h18#l0PHGOd00zy zzI1t@c><<=e;AEe^h|^;y*TckxI4rhtR>yoH5C0Bry2J?zE)G4uJau(hiSj8(6%_epw>7;HN<>6k z-n2g{qak0ggA6eFW7RP1ZW$mgh!k<(RoYt-69RNIZPesd;>TUSRa zQodlN1Db{vzL-shqGp_8A>`~q$56>Ot1Bj65Kkm8JS_QE2<^CzY=S6V1I#sC$+L8> zg5p`87Qd8F`tnA{g$}>L8aL#-OH^fpdj$qSA?C2tyL}WjWOWpIltK%0Uh1gGT&<57 z9m~c(gL{Him&vdAP*F&|Dui>T9(UcgWVyc0SuYV!{hn`8fOVW?qGr0n{|)v&ID?9p zapP5r`U_|ug*%wQWNc%XotPO*8DrlEV_#;5nc+9@-{0Tfe|(U7fxb<0Q^%6+ZWWU81}@8&y@IekUXiR(Vlyx~(WN zxuLcLPF}7oy{Yh=J;YSMzx_&id5OP7+>>oVq$JAgVbI_YceyZCQG-i(3r5j& z4SVyYhj9zCpQTU-c7l zkC}I+>JzS_-!U7y+|zm~Q@Wa`8Z4vK2&%+<;7DU`_+s}e+=$`Qr!N2Sl32gFgP?iG zHrK-is35s%pD(rXY?B{x-y=D%o(UpBUL#&0?q^kMS;T}GZgSx*$iO__zGULZW7yWY z#@@>|3R<0x+sx!f)GwXYt8k+~EC-3ww@EmXVGA{fJhHhRN^udiK38CIaXXvP10;To zXY|w`U+=a(S<&mvY771xqNXa=lM))KX&5mCEaSttwIdIgXe?`I_T}(caQ`?`jmDQu zs$bk);T{awQ=b|9LC=W6ykD4}3h5XZL{LBd?!y2Fgxfn+e1`l^1-o+yf%SeNby!Mn zl;3SQLQv@jg@f>plUqZ18jzGKI>stkG}!zp@jdrBHSp63^ha6}Io^rSmX0|=-I!0R zcLGx|#){e``m$snkozmRxS2fiTYX&(us3{HuoFYHC5}_&EX1U3?q+R=*^{6cfAF7a z+tudR=+Nq!$M>x{`qCzo4b$z2=h3ynDDXynSl182X4 zS{4rV8XqUnO840w{yZ6v3BX8lZ>#!GPgjeyMc62aQUv1D3m=|k3;MeMp(_{qhaR!& z4W1LC8qS&}yxKi~_VmWQZHDjHt?InPK#93rRQ|d~6$j)Bd7LEDS+wueic@hY4~%=k z`l6!G$ex_fwDQ1h%qsSeJ*@h(?ch?#1UOVHVm3`FPLeU7C1x>RXm5e#+5f5 zeLS*D4q-z}+7Dc>C?hXE-$Otje!74(_?2Z+GQhz_$Buc_n^&e5FVf*cBSpAUXO145 z6{#zCW1*9|Jj88zO{DqCNZLY-PR~|*%KWMob5{8q@RK{507d2K>a)tTSI#~3S$Yq^ zU*-R7tW%UEO5Gj%z?GLAGMKo{E}0ZRBKip$bv@uw{Nd9md+YZ@xWr3%8C?fF^S9L@ zDK2x3w&)>sXJk#SOJnWNH&52 z^^dRy;q!hpj3xtK_@azRHZ|s8hIcz?1sgVw4p@~(hWt+HW#F`Z{!E;F#v{f&jV%~$^00e#&v zA5jH=d~+0h>y~2|P`DRmI0$N2C*@AIrEwW%1$hiN-G85*bhmsxO~rDd7a*JVmdOM^ zIA|-a`aFJp_lhn5-KDnfub`2YDO)tWx&O8r!6i zy>oR@_al`PeSWrzY+DEP!Clw|JPJ_Q_<{x+0<$&T9ISiqY5R^sRi@IM0&Zd4J!Ht1 z%vMn>o(!UUgK-pCz5S!Vyuo1*aKy=aL!T6a5s|H}0zG6HXqx|%cq;@dsn{382;GR3 z5=x$Qj>_JVxtjC%M^03hbO1TN*XxqaJjatA+gR!R&c_|SdXHm39<0Cbrz=f}+vilE zYnbc%j0CDpq&bwQ^W^Q&1jx-XCZ(6ZB~p0{0k{<^fayYcbBRcPTr20Bl>kTD{(xIO ztO>k!jr}&j#BB5r&3pP-WT5TZZUDkxVJ(6F%uPLvXe)~U z$xT*SEWD$oJ=LY@6u5NZa;X0JQQFM7WBERrz&uH3;~%DqB-M5&dkTw#%`J+SaHbUO&9gP=UktM~a}-{P<(;rJp` zY(Bga`qr8PrKAM1uSQBaqZK3YJ+VNHrVYpds3Q16B0SN=KPLw4O5R1p@>H{`4Cjt0 zT=%#4%jUBL^Whsh8j}3>yMe0H17a4gKMos0r{Sa;*VN#}Swx#GC7-Es{j3kYMa!Q- zb_2-N$gv<)mvk>0X`?)!V2fR!Drgo1NSR3rIo1E|aSIPme`YC)sK?2lLLH69q2Ic)~ycfo&{n(}@(k6CBgZYO&H2p<~ny44h+ z7{fLk>!6iK?;g&c!L#2Uz(RIgWo_(_>^)iBDZZ(XS_tkzn)*iL-;Y!q7U;CGy-`b& zzaxI{FPybnvu_y_5pv+u_*_=3M*TSdg8-ZD1XmF_6m@KP(q=%@!iw*aNZ)=T3z=3? zEM~gKr8{`aCX$$a6$8Tiwhf)mVYmapOW_O@LlE`%PCrLDgk+8LTEQyMZZ+N|#(haz z;CrOw9Lua<~@1=HL8FGxK6K~-;^93Fb?fc zGAq|qXAp?|`8?q;gFCp~IZ%&o?gd|CfuW7DCWD^5(8XJR6ojUD`s`M2Vj?z^L9bbv zN5r`$Dqd#gao9N?^n6ESV}q8aVW3ik3n!1AZF3$YmdhTw{~4MOHY~3 zqFSz8r4q2do89QJX2~r4?$QpIXCd<8qPQTyVGIh>*JDiFkt%L5viR|kM*AXHmr4nq zXPtd2g&TM1ko`So_MmqK(WN*$HHbgQME~h<5MPxu#5Ti1I70>=UNC{*`BE!jZBRy} z{<%U{Y~c?{*=l@?(rSHHkkWL53;88mS8QKA9|mP-PSAC8Df^(}^VEJ{_eu#|COAEX zIkn|FWe2*>T$SqPY<@2 zY*?B9b3Ti`wH~^>4n`Z?h~jVVbImHDlw#X|W{!tWkGlY6qpk`$-^uVl_9~f+8s4&Q zV?pBnc6X|j>iY&H91qBhs@6RSWR1%0cG-K64b`ZAG5bV9&aZ|^?|W(Y*b8CPw+2`b zOBFD0%Z%^EIO*kG1zNS`8lVQNU9F~}C2~bt9A0_!tva~z4su~i!%IjW#1Wf&1nzVV zf0j|{W0!^;*L!aEtZqSh%Cl|VK?VbLzKaMUJRrUy^p#%XJ(6}WC35@#Ehl&jyQyJy z#Rr|rZ;yK0GM+MSYFoT@k)*%w*~UN{*`f4L;_jzjO*md>d1kz;iFw-IADLdoR|Cox z6s-kck&0S4KiK zzfLF(&L1$oju1R2p%}8D+&sgikt#i_%BjHXTG%oc#wC1WY6&~zaCUThN~GN}aA$5D z>a#Gv;~V@XuK23kP`8(R6awBU8R|lMNxTW66#BP?-NokA6nmA0G)+!SMQ_qaF1zjK z<@EvG8xDv4N6SCFH8A9N&ZAE*VCU$RYeFwFFfqGdb6P7UhbUdH=@Is3I*9@Y6ohzG z!U96drL_yQ7PtR@%qOshSVon2;zDp8pWq`>P7c4$Lx*vh>>aw?@kXYK z1WDl}6GCl^EPPgB_kv0N%gM%`aFq?DK=As=!yULtAH11x#CS^4BQI*!up-Nsm#(M|PU=9J)AorF1Cqex=v~%335HYE>-pfqpXl_}>cY9fu(`%4gd9w}NpcUU- zk11L1#9%MiqOHMmTm=d3fO;o*Q#R2jVI<^$t+X1hxo~dDnKk@q4FMHwrgQ}PiUx$Z zecH=OPcLTmR0blaz?j1{G)0E$<})Q|1Ncrun$Nr9m(F zXfbJRAy2G*3!{;QGM(g2hanY{m|!Yza}qHTM(zu%X$#yAh|bk#Re!73eD6y`63_zU z&mQlkCdq5R6|jL?wiFNi#LlH~u8WpzPv@Yf5W+9iJ9-=!!2)|M%k{N!y3PSlxw_Zr&hwDe~Gp1B5#1q+gSlM6|J{K9Tk2GzhpGy`2IEg z&9z}vGihsQBp|HS25!^k*u2XcE*ok`I@&=C_GpbryE|BXp;649s!Cb#)1U#jyYEW5UX6B~X&!+@< zMJ>Ft$}dxhy6I-?ID0Scm#C}=XVK!d!o)q0&feqdxVwB9-{}g!xDN7>(cMI^rqqyc zJowGnNZ&vFPQYj*Ghv?+&+H}%q+j>M_t#-8*j#mRCcMhptjeyUCgT8BwcN}38|1tM z=HI?}d_Vo3Nh{d$&v!)(nSFHfewbTu37PdyL&s`l+f2nAlfz1Z|8kTsYT7DQf5_?# zCw7i5buCkF;?$07trS;^$MyEz~W9y_PMt!v98^%*ys#=$7W ztw^zv7@GugEIa@D;*s8e8=3VqWNE7k8~6#HjL-lNf>=rCi+ZtIc*Pnk{5t1I-{WL_|guf;&2lzXg78&uZ;tdhDM6OA$@Spz1j-4rf@V)oR~#i1EbZb$q>3${%0 zMK;|TtNy-B=M4ru;YFRW%lFq?!Ue8aPh{y;Dm1W*d)i? zt7oxjc|z42=ETF?K7Gt`JZ?AGL6R@)XP>=9XMsU2fSqa0!k;P0wX}{l?c}lbg6O;J zI5v0N1LWXOb&l^>J#cr>LU4ikAK-KmG=1H_8(?#`(9PPleOUoIYLO_!e&C7WX=Xq=W&DzoWky-J|7b+qfyGcV@#+7n`= z!-i^?LKca@8x?Csar@2|*HJ*VOMWv>$WOw@I0`__8h=eT4Q$wScY z`SQ!56_tX5`;`rMxe~c>Ae3KZrBJ$g^e_)90~9jDtp%>h=!8F05C*$fsUAnJdWGrP zKh}=1;Hxx%&&Z3OmcOfgF4yLw3!gCo*Ik87S|a+F6{>e93NGO{9|n=>o%WxVAv4>& zVVdGu|Gbeb4Y*LBONU0DdwcT?>RyS^j$dmj*Nz)SBv2vQ867p4v--M4rUMqPb~VfGc1)Far?S#}t$CX!ww}B}C(J zRA=&rwNJl1v;OSws!(UhM9PhqYLRDypm@};nVrfJ2FN8e9>gV7Bmn<2R z6*(iF4Ag1a?c;1|h5GZ4v}XNHUeoWC-w7}w4TlPIK8@%OO);PH>F>B`by+b&GvV7T z=Gx9iqQ-WQO`T^&~b)eh66k&CbC7h9<(IB0+kRU8ukPeR0bWe!?Tg$Q_$iEI4_VE_KA zLg!xv|HL?X%0-k){4d{_OJ*)H2cMn)Mk`AG8!CS@sIANhn&!;||7rXy=>NkW;(kId z8pCHGeYyp-xWoV5LH}7_527dAx2!o?9d4&=XdNdK!w{#Z&jbR>1pn6tvHXjfH}y5V z9Kg1y?B6l}+i&r6k=}%#w#}ZNB=`QvPyG9c@Sn;-a?zB7Ow>91KPo4H2C$|8W*5re xOz*$z<&0 Date: Tue, 14 Apr 2026 06:56:37 +0200 Subject: [PATCH 02/11] .github: bump GitHub Actions to Node.js 24 compatible versions Fixes #1437 Signed-off-by: Joachim Wiberg --- .github/workflows/bitsign.yml | 6 +++--- .github/workflows/build-boot.yml | 6 +++--- .github/workflows/build-image.yml | 4 ++-- .github/workflows/build-release.yml | 6 +++--- .github/workflows/build.yml | 4 ++-- .github/workflows/check-kernel-release.yml | 4 ++-- .github/workflows/coverity.yml | 6 +++--- .github/workflows/docs.yml | 4 ++-- .github/workflows/generic-x86-build.yml | 4 ++-- .github/workflows/inventory.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/test.yml | 6 +++--- .github/workflows/unit-test.yml | 2 +- .github/workflows/weekly.yml | 2 +- 14 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/bitsign.yml b/.github/workflows/bitsign.yml index 7af7e6ffe..836ccb2ba 100644 --- a/.github/workflows/bitsign.yml +++ b/.github/workflows/bitsign.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Validate release version format id: validate @@ -170,7 +170,7 @@ jobs: fi - name: Upload signed artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: signed-infix-${{ steps.validate.outputs.file_version }} path: ${{ steps.download_signed.outputs.signed_filename }} @@ -212,4 +212,4 @@ jobs: --- **Next Steps:** The signed release can now be distributed with cryptographic verification of authenticity. - EOF \ No newline at end of file + EOF diff --git a/.github/workflows/build-boot.yml b/.github/workflows/build-boot.yml index 4cd741e21..cf983c079 100644 --- a/.github/workflows/build-boot.yml +++ b/.github/workflows/build-boot.yml @@ -41,7 +41,7 @@ jobs: ls -la ./ - name: Checkout infix repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref }} clean: true @@ -96,7 +96,7 @@ jobs: mv images ${{ steps.vars.outputs.dirname }} tar cfz ${{ steps.vars.outputs.archive }} ${{ steps.vars.outputs.dirname }}/ - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: path: output/${{ steps.vars.outputs.archive }} name: artifact-${{ matrix.defconfig }} @@ -108,7 +108,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v7 with: pattern: "artifact-*" merge-multiple: true diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 16c17b7de..f3409148f 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: clean: true fetch-depth: 0 @@ -277,7 +277,7 @@ jobs: fi - name: Upload images as artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: images-${{ inputs.board }} path: | diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f8f039fc1..2e6000390 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -23,7 +23,7 @@ jobs: target: [aarch64, arm, x86_64] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: clean: true submodules: recursive @@ -86,12 +86,12 @@ jobs: ln -s ${{ steps.vars.outputs.dir }} images tar cfz ${{ steps.vars.outputs.tgz }} ${{ steps.vars.outputs.dir }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: artifact-${{ matrix.target }} path: output/*.tar.gz - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: artifact-disk-image-${{ matrix.target }} path: output/images/*.qcow2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d60d3353..47cc271d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,7 +80,7 @@ jobs: ls -la ./ - name: Checkout infix repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.INFIX_REPO }} ref: ${{ env.INFIX_BRANCH != '' && env.INFIX_BRANCH || github.ref }} @@ -178,7 +178,7 @@ jobs: ln -s ${{ steps.vars.outputs.dir }} images tar cfz ${{ steps.vars.outputs.tgz }} ${{ steps.vars.outputs.dir }} - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: path: output/${{ steps.vars.outputs.tgz }} name: artifact-${{ env.TARGET }} diff --git a/.github/workflows/check-kernel-release.yml b/.github/workflows/check-kernel-release.yml index 4893f0caa..5b91581f3 100644 --- a/.github/workflows/check-kernel-release.yml +++ b/.github/workflows/check-kernel-release.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Check out infix repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.KERNEL_UPDATE_TOKEN }} @@ -103,7 +103,7 @@ jobs: - name: Create pull request if: steps.check.outputs.new_release == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.KERNEL_UPDATE_TOKEN }} script: | diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index 187d7636e..8f53b2eed 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -16,7 +16,7 @@ jobs: if: ${{github.repository_owner == 'kernelkit'}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Fetch latest Coverity Scan MD5 id: var env: @@ -27,7 +27,7 @@ jobs: -O coverity-latest.tar.gz.md5 echo "md5=$(cat coverity-latest.tar.gz.md5)" | tee -a $GITHUB_OUTPUT - - uses: actions/cache@v4 + - uses: actions/cache@v5 id: cache with: path: coverity-latest.tar.gz @@ -98,7 +98,7 @@ jobs: https://scan.coverity.com/builds?project=${PROJECT_NAME} - name: Upload build.log - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: coverity-build.log path: cov-int/build-log.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0fec0f84b..e080aeb77 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,12 +24,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/generic-x86-build.yml b/.github/workflows/generic-x86-build.yml index 8dc067050..d6436644f 100644 --- a/.github/workflows/generic-x86-build.yml +++ b/.github/workflows/generic-x86-build.yml @@ -33,7 +33,7 @@ jobs: mtools - name: Checkout infix repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }} ref: ${{ github.ref }} @@ -77,7 +77,7 @@ jobs: ln -s Infix-x86_64 images tar cfz Infix-x86_64.tar.gz Infix-x86_64 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: path: output/Infix-x86_64.tar.gz name: artifact-x86_64 diff --git a/.github/workflows/inventory.yml b/.github/workflows/inventory.yml index 9cf610fd5..205dba2e5 100644 --- a/.github/workflows/inventory.yml +++ b/.github/workflows/inventory.yml @@ -44,7 +44,7 @@ jobs: checkout: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: kernelkit/actions/cache-restore@v1 with: target: x86_64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26bb57415..f50a0b59b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,7 @@ jobs: contents: write discussions: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: 'true' @@ -78,7 +78,7 @@ jobs: echo "pre=${{ steps.rel.outputs.pre }}" echo "latest=${{ steps.rel.outputs.latest }}" - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v7 with: pattern: "artifact-*" merge-multiple: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13cfd09c1..6e6894380 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: runs-on: [self-hosted, regression] steps: - name: Checkout infix repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.INFIX_REPO }} ref: ${{ env.INFIX_BRANCH != '' && env.INFIX_BRANCH || github.ref }} @@ -93,7 +93,7 @@ jobs: run: | make ${{ env.TARGET }}_defconfig - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v7 with: pattern: "artifact-*" merge-multiple: true @@ -131,7 +131,7 @@ jobs: make test-dir="$(pwd)/$TEST_PATH" test-report - name: Upload Test Report as Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: test-report path: output/images/test-report.pdf diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 4f8febe5f..1c81094d8 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -78,7 +78,7 @@ jobs: ls -la ./ - name: Checkout infix repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ env.INFIX_REPO }} ref: ${{ env.INFIX_BRANCH != '' && env.INFIX_BRANCH || github.ref }} diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index e02ef647c..569e921e2 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -22,7 +22,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v6 with: pattern: "artifact-*" merge-multiple: true From a96e859bf3966e041c3585a7db23edc89dc12363 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 9 Apr 2026 10:23:49 +0200 Subject: [PATCH 03/11] board/aarch64: drag Marvell ESPRESSObin in from the cold - Add a board README to explain how to get Infix running - Update mkimage.sh to allow creating images without boot loader Signed-off-by: Joachim Wiberg --- board/aarch64/README.md | 1 + board/aarch64/marvell-espressobin/README.md | 232 ++++++++++++++++++ .../marvell-espressobin/espressobin.png | Bin 0 -> 215410 bytes .../aarch64/marvell-espressobin/genimage.cfg | 25 -- .../marvell-espressobin/genimage.cfg.in | 61 +++++ utils/mkimage.sh | 36 ++- 6 files changed, 317 insertions(+), 38 deletions(-) create mode 100644 board/aarch64/marvell-espressobin/README.md create mode 100644 board/aarch64/marvell-espressobin/espressobin.png delete mode 100644 board/aarch64/marvell-espressobin/genimage.cfg create mode 100644 board/aarch64/marvell-espressobin/genimage.cfg.in diff --git a/board/aarch64/README.md b/board/aarch64/README.md index 61dca726b..bd6cf79e5 100644 --- a/board/aarch64/README.md +++ b/board/aarch64/README.md @@ -8,6 +8,7 @@ Board Specific Documentation - [Banana Pi BPi-R4](bananapi-bpi-r4/) - [Banana Pi BPi-R64](bananapi-bpi-r64/) - [Marvell CN9130-CRB](marvell-cn9130-crb/) +- [Marvell ESPRESSObin](marvell-espressobin/) - [Microchip SparX-5i PCB135 (eMMC)](microchip-sparx5-pcb135/) - [NanoPi R2S](friendlyarm-nanopi-r2s/) - [Raspberry Pi 64-bit](raspberrypi-rpi64/) diff --git a/board/aarch64/marvell-espressobin/README.md b/board/aarch64/marvell-espressobin/README.md new file mode 100644 index 000000000..409c1aeff --- /dev/null +++ b/board/aarch64/marvell-espressobin/README.md @@ -0,0 +1,232 @@ +# Marvell ESPRESSObin + +The board + +The [ESPRESSObin][0] is a single-board computer based on the [Marvell Armada +3720][1] (dual Cortex-A53, AArch64) SoC and the [Marvell 88E6341][2] (Topaz) +switch, oriented toward networking applications. + +The board design is old but the switch offers full Linux support, including +advanced TSN features for [IEEE 1588-2019][3] (PTP) and [IEEE 802.1AS-2020][4] +(gPTP). + +## Board Variants + +The board has gone through several hardware revisions: + +| Revision | Storage | Notes | +|------------|---------------------|--------------------------------| +| v1, v3, v5 | SPI NOR only | Obsolete; U-Boot always in SPI | +| v7 | SPI NOR + 4 GB eMMC | Current; SD and eMMC usable | +| Ultra | SPI NOR + 4 GB eMMC | High-end variant | + +On **all revisions** the Boot ROM is hardwired to load U-Boot from SPI NOR +flash. There is no strap or jumper to make the Boot ROM load directly from an +SD card. The SD card (or eMMC on v7/Ultra) is used only for the operating +system. + +## Building + +The ESPRESSObin uses ext4 for its rootfs partitions rather than the default +squashfs, because the stock SPI U-Boot lacks squashfs and `blkmap` support. +The `ext4` configuration snippet enables this. Apply it once after selecting +the defconfig, then build and compose the SD card image: + +```sh +make O=x-aarch64 aarch64_defconfig +make O=x-aarch64 apply-ext4 +make O=x-aarch64 + +utils/mkimage.sh -r x-aarch64 marvell-espressobin +``` + +The resulting image (`x-aarch64/images/infix-espressobin-sdcard.img`) contains +a GPT disk with the standard Infix partition layout, using ext4 instead of the +read-only squashfs: + +| Partition | Label | Contents | +|-----------|-----------|------------------------------| +| 1 | aux | RAUC upgrade state (ext4) | +| 2 | primary | Rootfs slot primary (ext4) | +| 3 | secondary | Rootfs slot secondary (ext4) | +| 4 | cfg | Persistent config (ext4) | +| 5 | var | Runtime data (ext4) | + +## Writing to SD Card + +```sh +dd if=infix-espressobin-sdcard.img of=/dev/sdX bs=4M status=progress conv=fsync +``` + +## Upgrading + +The build produces `x-aarch64/images/infix-aarch64-ext4.pkg`, a RAUC bundle +containing the ext4 rootfs. Once the board is running Infix, upgrade over the +network in the usual way: + +``` +upgrade ftp://192.168.1.1/infix-aarch64-ext4.pkg +``` + +RAUC writes the new rootfs to the inactive slot, updates `BOOT_ORDER` in +`/mnt/aux/uboot.env`, and the next boot picks it up automatically. + +> [!NOTE] +> Use `infix-aarch64-ext4.pkg`, not the standard `infix-aarch64.pkg`. The +> standard bundle contains a squashfs rootfs which the stock U-Boot cannot +> boot. + +## Booting with the Stock SPI U-Boot + +The stock Marvell U-Boot has `ext4load` and the standard variables +(`$kernel_addr`, `$fdt_addr`, `$loadaddr`, `$console`, `$image_name`, +`$fdt_name`) already set sensibly. Connect to the board's console port, the +micro USB connector, at 115200 8N1, interrupt autoboot, and paste the commands +below. + +### Environment Variable Reference + +These variables are pre-set in the stock U-Boot environment. Restore them +with these values if they are ever lost or corrupted: + +``` +setenv kernel_addr 0x5000000 +setenv fdt_addr 0x4f00000 +setenv loadaddr 0x5000000 +setenv console 'console=ttyMV0,115200 earlycon=ar3700_uart,0xd0012000' +setenv image_name boot/Image +setenv extra_params quiet +``` + +`$fdt_name` selects the device tree for your specific board revision: + +| Board revision | `fdt_name` value | +|----------------|-----------------------------------------------------| +| v3 / v5 | `boot/marvell/armada-3720-espressobin.dtb` | +| v7 | `boot/marvell/armada-3720-espressobin-v7.dtb` | +| Ultra | `boot/marvell/armada-3720-espressobin-ultra.dtb` | +| v3/v5 eMMC | `boot/marvell/armada-3720-espressobin-emmc.dtb` | +| v7 eMMC | `boot/marvell/armada-3720-espressobin-v7-emmc.dtb` | + +``` +setenv fdt_name boot/marvell/armada-3720-espressobin.dtb # adjust for your board +``` + +### Simple Boot + +Fixed boot from the primary slot, useful for initial bring-up: + +``` +setenv bootcmd 'mmc dev 0; \ + ext4load mmc 0:2 $kernel_addr $image_name; \ + ext4load mmc 0:2 $fdt_addr $fdt_name; \ + setenv bootargs $console root=PARTLABEL=primary rw rootwait $extra_params rauc.slot=primary; \ + booti $kernel_addr - $fdt_addr' +saveenv +``` + +### Automatic Slot Selection (RAUC Integration) + +The CLI `upgrade` command writes to the inactive slot and updates `uboot.env` +on the `aux` partition with the new boot order. On the next boot U-Boot reads +`BOOT_ORDER` from the aux partition and selects the appropriate slot. The +setup below also defines `bootcmd_primary`, `bootcmd_secondary`, and +`bootcmd_net` for manual use (see [Manual Slot Selection](#manual-slot-selection) +and [Netbooting](#netbooting)): + +``` +setenv bootcmd_boot \ + 'mmc dev 0; \ + ext4load mmc 0:$bootpart $kernel_addr $image_name; \ + ext4load mmc 0:$bootpart $fdt_addr $fdt_name; \ + setenv bootargs $console root=PARTLABEL=$bootslot rw rootwait $extra_params rauc.slot=$bootslot; \ + booti $kernel_addr - $fdt_addr' + +setenv bootcmd_primary 'setenv bootpart 2; setenv bootslot primary; run bootcmd_boot' +setenv bootcmd_secondary 'setenv bootpart 3; setenv bootslot secondary; run bootcmd_boot' + +setenv bootcmd_net \ + 'dhcp $kernel_addr $image_name; \ + tftpboot $fdt_addr $fdt_name; \ + setenv bootargs $console root=PARTLABEL=primary rw rootwait $extra_params rauc.slot=primary; \ + booti $kernel_addr - $fdt_addr' + +setenv bootcmd \ + 'setenv bootpart 2; setenv bootslot primary; setenv auxpart 1; \ + if ext4load mmc 0:$auxpart $loadaddr /uboot.env; then \ + env import -b $loadaddr $filesize BOOT_ORDER; \ + fi; \ + if test "$BOOT_ORDER" = "secondary primary" || \ + test "$BOOT_ORDER" = "secondary primary net"; then \ + setenv bootpart 3; setenv bootslot secondary; \ + fi; \ + if test "$BOOT_ORDER" = "net" || \ + test "$BOOT_ORDER" = "net primary" || \ + test "$BOOT_ORDER" = "net secondary primary"; then \ + run bootcmd_net; \ + fi; \ + echo ">> Booting $bootslot from mmc 0:$bootpart ..."; \ + run bootcmd_boot' + +saveenv +``` + +### Manual Slot Selection + +To boot a specific slot without waiting for the autoboot countdown, interrupt +the bootloader (press any key) and run one of the convenience commands defined +above: + +``` +run bootcmd_primary # boot from primary (partition 2) +run bootcmd_secondary # boot from secondary (partition 3) +run bootcmd_net # netboot via DHCP/TFTP +``` + +You can also force a permanent change to which slot boots next by setting +`BOOT_ORDER` directly from Linux (the change persists across reboots): + +```sh +fw_setenv BOOT_ORDER "secondary primary" # next boot: secondary +fw_setenv BOOT_ORDER "primary secondary" # next boot: primary (default) +``` + +### Netbooting + +The stock U-Boot supports TFTP. This is useful for testing a new kernel or +device tree without reflashing the SD card. Set up a TFTP server with the +contents of the Infix `boot/` directory (from the built rootfs at +`x-aarch64/target/boot/`) and configure the variables: + +``` +setenv serverip 192.168.1.1 # IP of your TFTP server +setenv ipaddr 192.168.1.100 # board IP (omit if using dhcp) +saveenv +``` + +Then netboot manually: + +``` +run bootcmd_net +``` + +`bootcmd_net` uses `dhcp` to obtain an IP address and the `$serverip` from +the DHCP server (option 66), then downloads `$image_name` and `$fdt_name` +via TFTP. The kernel mounts the primary SD card slot as root, so the SD +card must still be present. + +To make netbooting the default on next boot (e.g. for iterative kernel +development), set `BOOT_ORDER` from Linux: + +```sh +fw_setenv BOOT_ORDER "net primary secondary" +``` + +This causes `bootcmd` to attempt netboot first; on failure it falls through +to the primary slot on the SD card. + +[0]: https://wiki.espressobin.net/ +[1]: https://www.marvell.com/content/dam/marvell/en/public-collateral/embedded-processors/marvell-embedded-processors-armada-37xx-hardware-specifications.pdf +[2]: https://www.marvell.com/content/dam/marvell/en/public-collateral/switching/marvell-link-street-88E6341-product-brief.pdf +[3]: https://standards.ieee.org/ieee/1588/6825/ +[4]: https://standards.ieee.org/ieee/802.1AS/7121/ diff --git a/board/aarch64/marvell-espressobin/espressobin.png b/board/aarch64/marvell-espressobin/espressobin.png new file mode 100644 index 0000000000000000000000000000000000000000..14147c562d8f34c483fdb6ae579814fe817ab98b GIT binary patch literal 215410 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
F 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%0DNnTP(cm>!{psbVpW)1A{HabmGu=d1AJmL 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�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 X( zomaVrv^B%)n@lGPMK26+$;HA5r?y|%Vn3;oCN-ok&?L^JHnKTZT|-7jQRGArt2EaT zEfnL7Vp`B{I=l`96Hq3A_B3uK`Sw~kuT8sVJR32Y9x|DX2zZv4D>n6-%;ZexhxpFX zb~VAn-o9+g=jUf+S?8l4%e&61T`;xLCmS?r`gCZx-I31YC;lAB~1sJ22e z5jblI;^nE6*KbUGSsB-Ml*yx0B0;+$idQS4bZMJOTsq0c#bokkPJ|9p)dHy_s07A1fF$MUT44W*; z%0hfYjRKPwcpoHbPbsvusANypPvjzDKmko=hc$XSo=B16+EkzjH`cWskTaQxyGZZu zBS!n8zv$0RQ54i&JMbl^)9El~FN$m^w)%+H{!8>8B2Jr5Mp&JM&B0@Imd5DPT-i)x z?cUI&WC`IWYRXYAjj|Q)tPjG8F3WNdEJ@*=Cfu>N-;+s$_a2jHLt))J1oen&)N1f+ zZ4!M-N4=#rrahwPV%t0$gx3<;?FV68kH%VUhN8Ile(j6XzIN;R$ZcIqfj=1y*Px^V zx+lQ1zR~ELtg5Pnrj^jzVLvlkuA|HjHBtYXP1DI#!Fj?HO<$yQBC@NS&CmdKuEX|9 z#SnU=C<20B?2dzDjC5^>tF~N>{woBrnCiKC0N#1Jb~{230TD%6aSj-)uA+lpIj0TE z7>u?=orz(!&d|n?my$@4mtA4TA{lB5g*!kM!oBVY@A5P#fvVBtJt2_jO`hy{m=)NMze z7p#^QAbxsUS(b}6&ILA`nk?I1hyzlsG$y8N2T#{Id|Etv%O?#4C5FWzMsD1BieLM! z@1cvxr}sZ&?HsMvy!7?g*gH7lc=RPcDxwlkP(fG= zGLsKCW;B}CHAIhCGHLO?C5D6y$O3lJHH=0DuC2Lyx52rVXTExyZ~xlYxpwCWTF3rm z#@=jB7b5o;7yRsl5BLv1{tNVjFZuR&zK+fUF0^E0O`$T{CZJ+Y7bGpsB&)TiX>e^z zURddj5z&U$V(q44TBuw=StSCn{{BhLj~Q-qx%sQ6PECU#-AapLf8A^#N^##i6}8?; z&_@)9io#}~B3)Axe23LS5NJAw&N9|jMNFGm+dAmEWb0;FVEgx@`ZT-kUGfA14zj5B zNi^CDhg`Vva$X)hxW^Y?d_mAKJKpF1qZ3AxF}LpA(GC zh|bfx8lz!0JLKlgn?yW+aqlOzUCr~aJjdStjL$y%jEl<+d7e|21*5#+@nc8Vw9KD> zo@btUn)CB>9-p03mL<2!|KfzEf_xP6nGhxZBiQ6iEuMxl+OSufcvmW<|Onq|Yq zdP(q5=#s3f@tc}z^N37cWj@ZK>m(RKYkX6qgQu!jOv{2Y%NZ3VdwYA_x_z5FM_2HR zb3T9XJ@6Ndl;`@vl!I|j;X5+zu$iYA=ajPv-Y7a3xc~T+#j@u3+8tj1?u%TxHf1y^ zDD#3Uz~*9%!I0%5lnugMks?oPj7lP{Ajv!`3xr^wSW|o7U^PsL4BF&EtJ0Cs1uzi0 zfQt&NtT<_SPwSkB3nSP<5G!kBBX(*#>NHjjn`Q_y<;YTzxm^>tdq$rm*(0;!1R5K5uXf!-eKTQnX zHnlB;AU{j*y?=k3%_bq9JNv}W@#z2FG!2t+iMEEE5ypGahNg4)7;s$_VSDepZ446l z;|W1H^W!p4#4i!AqZDoDaa{l{2)@#}1c4-*IG;pIUGOL`>2UoqR*62-AH&{LxNSvj z*88buwh&hVAEdcehS8`P>KQW{F&UM^>jV$Nc`(w@XiYS|qlq{^cqcSHW5vtg`;IIt zsOyacf<#Z($>eptZU(SYns*V>^?uCWMYQhdT93yMG)-%B@?5yiXlde>%O&1NR*Orj zs!5#iL{0OM2MJ@7kzM<_J;U5e+IG%52}^;SkZMdx2vl*Sm7`dEs6g&=Xe|354m zX=E{2P|&-7{)$i`D9XIRIESw{(0K@f=bm|%r=Gpd;lUmt$syy(C~=A$HtRV^l76Nz z13_F~vnP7*h_L{a7G6x^+&rurL9A0DlIJ=8?0mZj56`zf@5+dJG!pS%f+e&jJ&$l| zw2_O>>qyf%#`Ar?_Ra5bx;f|5vlG;4%EPk-2eUaZz49{O`_3EGixvOpKlz{e?2C{2 zjo{mQbO`3n|6ermQ$Kllj%4yDn z2lvVI@9>>(eVZa$M#Y$}=@`$aykay5H?Fg4Ry;mGV=}jV@Y!43|KxLSA7AD9JI~U! z4eOOSMfB(2YJ<@lm2%rdOtgm}{>Uha?2S>FIx({KiG^Ww+HY`8BcnQ^r@xAP>VzZ7 z$5I)T^IJ4TNi{3in5S#RL)mJuxiFrh^Ws^lz~lxmj8aermkL!~79w6#J#oKBiZw=o zRbszI2xu+Sj?M+LvLGmE>W+)mlI3Q}{^0?6nep(^1K$7WLq7WGLt57{na^n2j^G0G z=^nn5#dOv<^14qw!)9rEJEg zajZ8jU);UNgGVR4{_7>zZ`|VA&09Qpa1TeIYHGX>=qzV6nGy}cU3V=GHF!3oMXXZH zCS;jmHXGr(hR;9yjAw6rhwr}r28W|Do5d1qO3JcCDaEMFDf26Q{p+vs`6qX|fBzxn zw8Z0CEF>w&s*J*9#LjX4*zoF$PjTn^5xG+6)-$ssOg_f74OIxd^3|7kwsEySdNb|HI$pXl!UNPgtKlM5~6$Udh3gJ@WC0j=+}> z4*2Mk`>dOmyPy7urn}(w(=Rih91}{<=%_$tp4hc`*P(k+sE#PFk|j_?;us@FOR>|h zQ8CC(qf{U>y(S?g3TcD)4zHw8^3kJ15}{fnlRAwcK8fciDUW>&t>#P&_^w%7{;S26a*N)pfwNMVeiw?ZIU zw-T+8t+!}j-1n%P-w_J-g>zrX_BNF|P0)I(LH}6i1%(4>TsIWPF~%XTx~Eo1G3%e$ ziw7XiiJ5-SwQbYBYD`Agb%_EY+}fTaFZZjUuRY~(wMs(PonqNJM`jFJnbFk^wvc9_ z|Gz$nBgTlq4vmT&Pc?kz-BEqN5BJ=032#y4)($j<)#XC2TPq{_!RoDRX&XnxW6k#G zL6NQnnVhI7NVLr~21`s%Q4vpOi$u>6?LkuuRkA3Gblmk2z9GVX5v+Bc7mLB%fWpQ$hS`|l@^*`307xAX141~gY~#csR5*E9__-@{sisVrE{eE*PF zUVV+};gplpbM}r_j3+ZD<7w(>tc*Ezd5kwf~o zSCE^6h=(9)E5;g(1{E|}Ftj0JvmEEeriLiS;%-1PQntMpl{87D*?PyuwPaQrv&>{{ z)-}s@!)!Ju%S%?>hKCoAsk<68$+1{Ie{_$}AAZ52Sqa@}={P*NLRn;-o?Wn5FE~3p zA^4irYJp3%i4a=}-RKMRw&rYc#@X2!b=L|ynNQg?8=`{MW=&mhh$fPi1=IaKCVO*1 zO{+DRn-$Ay!?NChQdC{b-A502czVKevxd{hy!r0iTt9uxho5}Rs;>C*;REhJeniX+ z&L~z*g;O1`fA4ih^MXJ5<3Hx^XJ4?YHjIlgF?yVGtk)abAXZ*wDIB=d(^Ims<(ZeB z=jNT;99_N2q?~YZcFM!M8;%YRdG)22(JpZI@RZR!V|+ZJ={h0-n`Qjg@BcP0eDx(x zAD@UYS2^Lp84Uvd;+uxH-f;ce0Y_JkP+GHha7YLlIxl(j=n?blSNWaa{5}_<;eYt2 z|10N@A2S-~T%2rp`@J{0GF4oeDGsley!`b$jKZ4JyJr{z$_MhiB6v>>1^at*@?yef zo%8mG_j&h|ce%P+Q=>ROzQe2-Gd&#PX}P#}!a-KZWXSfGS~4-BAKCZRn;mdL=qiGL z_3N70a2be+4XzRqz5i23DISXq<24~mMNOjL_}J?R7z&-CIMm``AVsBra`c*tsJ2r> z(E);x^&@<{`~LfIA+5=jLdB4z#SDOhQW0Y`A=m*(5}+jzpN&%y`inUgkWZrO^#9q6I*YFl0_~2Grl|*h zt5PB&6(qOAdo947)@Z%;h^OsXw;rX0a_5|*b?H2-4ZS$NM>RbjlYlvBtQ2wTDUF8{HIJi ztb!``&o352!n5i-74cN;hGQ+SxfR{@=L0%X?|QNB%1yEbOR!)UI^l{Dd*Ql#RxRHl z$T&QBF?cd5V8P?*LI+x53W<^%&8NKm&O2Og7L4X&E-x?X>Y7PW@ad-?bNACv_}&|D z@OyvoyBr_R`PcvAU$I&)v6;e%@d24tD4Y~DUaW3HvZ)%7;{F^YZks~l{<6$sl;hxF z&aeN*clpM*zRv0CDeu4kE|-^=l=+C|vSP7m`116ewsTy$eVsSH`3Cc&Ip?c0nyw<7 z6`WTKe(;k&=Xn2^*T3;P^T}MY#V;}m&*lFWlW}Xv^F@ek_Ee5-`c38u6IAgI{aQEZ^Z@&LFCnqPY+7-%Xy#ML@RI1^_&wt7N$M>mY$Gy{g zkVPJxKcKEIxPNw^%lcddMOnb-4re;%S7zMUT*Zz8>*Wc!z^WWU(?ION1=iHy3b=SI z=yh)S>Q`Ro;OLO^<+(T=#6V^Z-~G<(JpasdSX*Kh?Ck{}J$lT^*%`;zuW;+BTQt?0 z$#l#&pZW@I*WyEiRw*{zwOEzBBwL}O1n;P-npJ?uC#QV;@m=QgeGac2bChXvvIjW9 z001BWNkl-nwU|1~ndLbqq+`x) zXob>|JQJ0^ZWx3|ve5>N5qP8@x$lasV(=(qu$F|1irBAj7JH;D&+JZd>YrHScZ-C+ z$Qt&r5K+eBwI9SXyTy0JV>1zWYGc4Kd^W8WChZ$pt_MUnM)67)dnB38WGdT=aZ<5j zt(D|GV}*i|ZM6w_Nk+?*0aW5FL+@>_k`lh3UK(rBHcLMA_6a^HRH$BT2hzZ&qD`NlmS$$hHKTX4kix4McpJ3N@IKIad9JJwn=^6k z@S;Nqoj7&`g))f(q7_Z&#V_LlIPO6Y&JDr*HcaV z2F)hQ$2Ryw$27?ZeZtrFKFKn%lIul#&N+0H(4_u&_w=WYRlv34v_q8iryE^Ck;T~2Sg3qWMkFo`J zQZk2JGU9n_Zf|*7+Vsf+>E8xXe~TB@8cHP z*2$KMkQM};3fvvOQ}DxQkPPIIQj0Vpc0^q7jzv>n=M zY+10W8@vjv+chu0`U=m!@GK!Xe)hAU^3?5H{Jp>Zhs5A{bpIaH$%Jow;~RYKwO0t< z^UweHf6k*Xz90mc%`M|`PSZHavUVP&`{qD4Hlx7m!C@yIc?8%H!~m!z%|o|LXG;MZtR2uwGRB z@sEDM3(q{ui_gEnjic+V7nl6#7k|b_AHFM@*xFMyYpSY|xX9@Nv-uI39pROT7kj>J zo+61Ql0%vTLgez|gu2<#G&N1NX0=)nLg47iHReax%)-j6hkWwmfQ9WgD@lC_> z@|5R^=8R@K4m;A=(>)!lE8r&5+6MI%dXuL*E&Ww#H!_@Im5S}$(1o- zd@M-PII#4W*tur^>0{JPaqr?w*3FumFWjJM8s7fp&-vi)`&8>S^BYsH-8`mc#e1K= z&DviuxizOeuzYy;eJ&Q~%&wHYFnNYgAAHEa{dX-Fr;k~=OJ+Ccgn+2K{fM$iJiWSN zG?_`fWm9wOsT=&AKl+;#(=mVb=1(~}zsIpY;L83X&%bz!YqyT6n+3e*-;=l z)$bubxx{Ue*B+(wJh^`MYxJ(5b9Z{SEjV^8fY>5LJz6J^lQpz&9z)nITCy|}CyhwM zP?YUb$94*(AITfmwPKC!AN2BU_V(D#egbfwvp#O+B)zxlOlZ=9q#wv z-TkxY1eawwxFGGOispGv=Up04Z@>H3+rDORq1GQ0*)xot(?iIdoLmJeNQN1+D8aM$+IE0hC2qHPG>-@C20bzio2=i7A!QSVg8K zWv+LRBJ{c?c|W}``f%L5jO?}EUTeG8|E_PW);hU>^t3^VEbnP2T1$wN_mQ$Jhu0~~ zF)>Pj2S8DX=W;Kw6zY%ob9*oUws!kDWYQr2?$Pt{SSDBFqQs2F6(Y+rp`ykJ$kd~V zTe_A9y-IABELJUDYw;A!_m5EB1lKz9Y>()dgl>s<8;Vj<*XoHyZ@U0J$&DQ-6KOg{ zf72%}QD2Dc{*F>xu5JIdyQqXNU^En?f*W^kqsoHM9zJ5N6-ys^?)k6s)XiJyY(iCe zh?1n%wnA(d%OM%sqh$1f7pH^}1IEa>r|0bSuir0rpz*3pW1)6{^82oTw+P?mF75J= zYM=ZEy^K_fqQGd`sHqj3)f&ozE7$kP3M!%M;}vX8Rdnb3rIIF z0)dQ^vooTKJpJr5y!7fz_|Wp+dv9^ET=4BTUgzqq8#J+FS#MZZ8=9iy@c1f+$5+5A z9<3kq==>38qPaQ0$>G(jc>{57g!cWx0MH#~Uj9`~1bxq5A18oNBA zO(fvy%7)r*a0VQjPAO^HlqT!>TESzKc=wsiV2&~-%ov3+tEC|<)OhrKOO|l#E6r%~jE6y*^xpMuG*T4S?=a*-6`WYTRe89nc&a})} zRQI^|(BP|woQ%etot*O7!*^K6$9(kVn=JmX`{bjN>3qWFV#VXf4|)9HK5e~0IS4L_ zb4ln3QC>eU#u!^NF6LC5jwWAoUOnL5_y3Z+ci$tMO=(o*=3&8ja)oz4{)qcuJmmDm zlI4+6Y3TAC-#|N`Gc7XCg6F)0wspkDV>b%d)ZhZgM@JlAxq+QbsddY}mH(58LG6_3b#B7iUnW8>>zhB3Kgzj zmqlD02BD-zCL%&tkXI8_NC9#kS)&F0OEj%6bof>{jxIWe8m=#viTEfDw^4dPS+x~? zh}8y_*~B;PlPS$mjIG!0u>Ph?Diz;}m#WMY`Gj}HPf;)!W4De2HWT7vKVFVc z(({bA=oES+2zI&{qI@4MCz@ zt+nVP-!>eXaD!b)VJb!}uL6;13}umTIk#T)8Y&^y(f7%3;>+E?st+AeNx_-rB4~F$ zrK4q<;DQv>8no#{4q|eIN!NU$Zuaz;@wgng$9UsLh=HmW58t+{shiFA zyehL57-}Pke{V-IDD=}PviE-rS-{woS&hQ@6v?OpMuEx3okKL*( zcb{kIj?9)*D?j(dC{2P&sYjF9K8IHh`SRg??w>qhe*J)p8~Z(b>;UvLqy!+y&P*IS;ZpRY^h1A1zHUWYDlu$ZNLWlqyI|#OT+Cp zKm&%|Ht3ctngrPthay>IF>B1q;brC0rkv6Mng z0q=#=D;%teZty(4c*d`O^K1U!AAX-FPagByTW|2YkAKb6!(*=J0}Lw3*0Zxcqa7&p zygEMT>D8xPPM37Mj`8Kpc6ZMGcg}hJ0MbA$zs()f)fG4CB_BQi9piY#-PgB#l`r$g z>kruNrBacyP;qi`6yr(JFvg4-w&c1Xb-w2JDWX=hLBGKoX|;$kpMRaXN^+UJ(Z*<2pY4?0t^m z5^p@;`j_vDvr?SdZigyd-s6oItHs)#+<6aS@$U|`QS&g3l}B4kYL!+JVojLI#E|H` z$K|_tCswp;{+w!erc`)+^FR{4c_d@F|L{I@67l++AkLPqyDM@xlQhz7`z;|%^xa*q zudnGhTPY>_jyJyYHpk-)|KAV(6Mpba^Tho-9pp=1zIeu~$C2Ov@~3R9XX71*!!`61 zfA*K(W-fv2>4L){bDerp&Rk8O!ElYqlQiq~LNjs{wu1*EbGe}-F=Kh~rTg4Hf1Nx> zKKST8NTN70obR!_f#40V-SwQ`xkq3ol!$R~bvUv=+wyWs93;de`hjF2PE+N_?%1Dq z_`7?iZf44_m_ww@kBQS=#5URYQdbvJ16@9v_N5vf(rWO+Bua{PPnC7`b|lU zDH@VWZN@YbB=Y)S<#Pmq-O9-K9d+PbzMvk48vazp?!k9%%dW~|obMy;!0b`{ByuQ|rRiJ7S zi!y;Ci&++Aj5U>yD~P+eaaDW!ELz-WXJ^b|YR|BggkJ^v5>O%r8cw7KV~iN*IO}`n zRFxFW62$5_p{`E~7i|N|)F0}!_HH#OOGaV|Q)HT|MC?XV${o*rU_xR{@ zTIPnV^S7JQb)G1VLM?g6@zCM}ZN76(s)5cRDpOreoJt~u)3FCq`A8`?Do#pcL*K8i z4p5!Td@sPFHns!!J9p3OI9Rf{cA;K8q+^`vT`wrCGjv_wU~N+j&PyboOQ}Nd;*ipJ zOE+IiiN5P3_&|6csS3f^zH*TcN1lIAkHJ# z-I48=AHe1vdXBua{n{NgSE;4R;<4 zO!1139=}iV2R?uL22U=q(}5 z+>A%8bL{V&bN}H3e*VEf@T(8|wx}2~;V?D<{!gENz}@|QKKbxt-uuO`czEw2fBENs z#dp5-9lW*7QxsJK4BpiQDM%nI_NBF~m}OzgBsyEzbz5Sb`QYcj;XnQ7?=gjuFMat< zlD*>5#=S5a%$wmD@|iflI<)b>(^$5W=d z&r~H5D`MelR;xCbB}SaDn?jT(jjpfrtw7AB8d!+VCMpA#E6;Ww331|VC#9t0bP%p> z<@@Gb=zZ7tZQk{aqv&QzGFO`ss|}}Far2z*I9xpC*~h;_`Fz8s$7n~iRz~ttT5j_- z%ciy6ZW-tK1jW(&jw!^D7&bjQB*c9NjyDH}x`s)Y?AVe$^(?=0=MGml;hc}_NrTGi2Vb`gUH?>)Wm1i46=JO$!7i3MZ~xZbck->}&Y zm>#l;V*U^zo6PiU0$lE8UUNm1l5SKSb-I~kim(~Q+1*cnR zLawqU>mXj0eNqc{)k=&} zdW691^4zS!Xn8!<08AUG$<6g_p68}#A0|%I& z2m%Y4Vlvi?(hPMxr5c&#YJ$`ZWGb?sEB96QX+wz_Yb*|tPZFgFgcBMI)-6c3?>dgt zu~LIfOROc(mEUXkI##ExV(=@vj+|4LO0tz|RF2X!l1*-9{K0o3pJOs9&IF^9aT1`0 zd8$?R!Z=M8ZS|z8mEU_0H3171aW_cy%vg&k^<;2jv?b1`Riy)4pGDW!ac5;5mb6rr z{dT*R0i&gDowP-D*=%hru&u#BAv~cwFATdq+q>@)##dZV z*Sx%W#q~I{bBPiyo$u)T4%_wQzQ>&#w)?jTM^D^%ricWYB`ip6qLnIu+L6sk22-G3 zUy`Lw7cO->t66G8)3h{JBBoUN5+_Yj`?H?H$o1hleHVH6Yj3eR-*WltGk$e^!bcx& z`0$N4I9^_IcGvTbfAKcku4hx5yh@gA+IOca3RroLyWI|3OGVxYM+`OZ6 z&e+bg?a#zcRYk_V$9I+FpAy@RXSb8ZtQ!p0_Z+7K#vd_*XXBth@98!JG0nWZ{)Cs; zU@Gz-QzFGknIq0xI_tT)Jn-`QOLCO^_~^3>KL6fl{M|qN-;@L~W*j2BruKv}ETs0X zmO4TZZ{?UHT`$PaG#~inlSjOG^@Mv5?=i+Jt}iYap9TEJGi-Z?{jLTIj%SyE7Y2vx zJ$@tQX?f;_4oj+)h_356)N-{VB8_%pL?$IP2j^NkO;ryR zndBBhW=hJq&Qg--hz^~Uu`Gi2$2nCM30nCpBLQKiNI*nl(+^lL=a_RA6Nn@Wu#8p7 z!b|NQF4ciez1=^n37vC>STZpNx}jgnj1?K}x+-42>QGkcv&ypLQ`$*)n7q1p(gKhcOw{#2 ze_#1xibiNcxJKvJ@<#eOrATSgImc)`&KO*_s7blIlf@6?G{V6RG8LshDLeDm6EUOXe8xn~vg<61q^hu~vS! zkm#&o$_0}WhDh%+yPYMU^=!6=elW}?Fl0yaR$373jwz4CSP?h7@LJ_ro2r+&Ac6Yd zHIlO)J9hP+uDKd0(EfFbI2t3mwiYY2IQ^~~>;nzmN{WsF*)DQbu-L=u>jXhAryNfW zYIPscMipyqb^2?h2|1@V&lM->wx-S&7+`sjFzFqB%t7KuJ`7Mph&My=M zIjM!)t835t=L^!f(B{?%u5)5+w4nO(^LeiG*)AOBc5iN7v!ebdNcc+ntQ0B9yRKi> zy_&En!n`DxBI8XadV{eW;=U(Z zPd@A59P+c7F}%Xrt_=>1B^C|d`WAqeoFw4TY*)eqxo-w?Sq82#u1Pxyx};*|!vD3_ z;5;}VN#(%t@Pd*9XJ;1cX09)vN(s*xUcLMjUktmQW%Je@hzXk|+nHme>r8{~b-A|I zfywlJth#ODa%G$)X5RwVSfBqmuEer!X~B~&crvi6#Dx>ik4UA6!i2T9W~~L|WN~$! z1t*!Sshe&$^tkFKHqSG}>Zd(LI@dEFCgw5G`whqIBOhISzrQK*khbUxj^dSTE85*|$EL5x%~M5*9bMgM`a$MYsU61D&gnVL^tIeKW#w?!MiL_pL#8YNi?tTt zsot;XEavL-YLmg)p3Zrk?*v8l8IuF#0aGa?zCMTE_gL$(#S%lVc9oLAxFStOef_Rf z*X5Wqq)cZzZ0^K3Ac;Z9Wc<2Gwk*p^qD921WWZV(@Dd76R*}1_N zF(|ms8RM2fBISg!uFU}nmTR!ng3Ki_=&WNM0GIMbnJEoIC1B1=<*atj>q`R=nR|l1 zIit4WD(z*fZZc3xLn_;NESv-qSp=V%DBHKiz^10kGl<8Pk^pfr|;sI%svP z&()w(o%RK-fWC#mVmYY?65=j*f9^935cNrFu2 zSy2NBT{A;C9qSD+sQ*@=ASF@m$p-Q&u(y`#;6*)e*-vLJ*<&$I$f7AF86RCoL1-5_ z6o#S0+M$7*vWC&do>=APmXug)!Gj6fFu2IDE%Wu%w?M|ICAmf2$=}@E$c5PDO(AkG zlTA6Y)>=Z0;ul_#ihk(Y4f1u+D?ezRDWmV#F7_!%@FIiOGR}o53Q;|Tz!(d~_250@ zICC9eF^(^>_AZ@2vUMY2yk-oE@#dN^Sq{_07$=G`+#HX%DHF!RFo-T&w&ELX-G=L% z8IuS4P2$~ezRq)Z!QKcVTXM=JuN2n_A*7pXhgj^|$ROgWqP(_pm!fiKmqphYBM2MI zgm%v02Do>B10`~EeL)8LO-GJJn&xWv;j8PH_~O`h9upyk35&y-Om7V*VE_Oi07*na zRERT$8C$xB9%vUJWae3H0Id^L!Q*-M>KPV;?-%D66^1@PKgT&&y{2PxPti#w*R^yR z<`5aLudrRsl)S-tNdmgQClT@9(rtQ7?GX2KUbt>nPNtG~OyrFYF#TL5WV~lu@4}%1J<1vay{qCG%#3ocwc*8tK zY)*8YSjSomc^0u|?;JK}QVQ6d*%gnshG-3R9o^ta)65vCXj&ppbF+oCov${JGTwYG zmk4scbX|gWNgMjTuXdIb^EAu+swL{7+py{OvS7JWgh?~^loDr~JFv+O=`^`xPV(&L zoaxU7L1e2#ku_erl8tHOD8@)Gk=^!;aXz+%{5Vgdpa++A{28(#6yVT*VW9xS8_GeP*L8MwXZ0LrL zGMg%r-7)lAT;E~C!X+E*28?4$6V7&Y)-v=P4u=Ex_77xXonY(tB$L?P-H{XGxYrO1 zaXTuhv|w~&s|1a)je_NhqZpavOT$bd$0XL0NXgY(h$WG0I44z$PP&$z*fkadCJope z=m;VfUqlhcnn6mt#>u!TJ;h4-+7yemiOza324Clx%ZXHs*jXFtPY?n68tjn3T5r)td4I8=BnD!DowSSQtzKJh8)G>7N`Kx5@b8)nssne``_+VPgwV2Zx?Q@SpO~- zxQ(A|rQ)G6hW<>SsRs6X&g*NcNUYrSp?XRydYbLBaOi{Ux{l*KaWfu8f6$1Cysv_{ z>jAF3zFhfh7*`QW`IAy1)`vdF!1c|{lnSwS-QOGnZZlwV;qvB+XE#qc9=}YC2gdN6 z?5_!jmt4NQCI)zQapdMOH_`Iz;}L6mN-T7p*s!I_*YW+9e09Xz4fnU1Kl{^ndHL`) z;wXHWX%sGywVu-CqqHm}9o2gC7o>SHGZ2l&rMx49Ty^G<29wo<_Z~JILz)BGWL)2g zPNIVjBav`Q*o1V1XL*oAx1qBhW1u${>kZcUR(@77=ahm7*GECc`puTEb9gff1teRx zn_abV>qIlobU5SK_6D((4fI~zvT~=38u&`pSR(}~C0q^E<}_oRt-{V3XB@rpm5*4! zX7HZqEh!pyLyxmv)4ms@ zj;_;El4ICNcWl4e2zR!3xXzI63>G>ke5-jpkdh#Wcqd3f0C05K;&abjIcz0l;xVyz zciC)v!Zb7Hne9e0>Ave3`k~4E_}WE`SpwK)6BA9na)7RE*$(H@)VD@atY}-#(^4K^ zf>?t~P6X*k=@Ra;W#9K~?n;1oI2>fb=sL)md5R=sh{aG+!8pz8!MQCd2d*w7ISZ1Q zV#FoOt{Zs$_1DE#^mt^N0>_&JVGN{3DinL&+GZ~31i6;WxQ&mYe8^91_ zT4(@Pbkt&ujK_(ue)X$-_g{R2&p!K%_uqe?oHM)Kjz50)PdGn6=f^+#5m#4F`HR2! z3*LI`E#CXZFM0p{_qlubF8}(^|Aw=(Gk)-+AM)NW-s3A@`3isYmw!#pnIHf7$9(wf zPx+%i`Xj#co$v7Y@ne4Q18G$I+rRx=-gx5;-h1!Y{QTpeFPoBGke;sVNM2;@LTxZ? z2wa+^NycQHw^hxcqMCV0X1sMG;gcgcQ)vVl+Xbx8j4@T@-BOCD6xR?hV+^y<-zu(d zDan%Y(#Tr`Z46^ws}-3v(j01y1VAN6p^q8qY=J>te3P4`gWu0dR zFm4G*gtNK?KMFT0b1i|Y~eEH60 zkD(o}=d2V7vG(f3YotDx^B!6~Qz)}&Gs;S}Z;0Bhh1(hH#X^)~9Q zXzeoA7H2N~{;qQs)R)gxN>NvXFs<4(J^j`GZ>`0ou6@4-a<_xcQcA1bx%m%RS^>pXbyfWzUM7cX8g9RhE>@do$q z-Q)7|l8cKA4u^S#{y9R1aD>?>Vum@*5`-A>d7rBhf@FV2IG&|riV1Iqn!ye9-5tj1 z$mR8s?PgDoh7>YkD)l~yD0o3Ld@o5x0cB5`B7eMtIMou+Eca*_942+lb0Lg{QVd;Z z$u>&KsDok?(|ll_59~HQ*1&i;5~mqwsX>P$<*tsC%zPZlG4tAkhxF%L27k^wUwxB@ z4UlCqMZKkAMFjmzS5d zQPZ%B%j7&)f7f$cYes6tXXrL~WKuJvH2CDxKag zYs*|8zJSi%4p3p)+&i~aGS+iSKSRwGbj{Z>TS#B{Tuo#Eyjk^G==nHHZ?u6@(aBS_ z18?lfwXKn;r1YjW^0=~nUkB8~M$SVTo3x3|V>sngiZ1ABFn1CZRZwP~Je+_!b&PpV z1MX5|1wBraEDhXrjnIA7tPtYc&upE%wEd?nzk8a@pQ4)@$m(-2<%`$Rkn${2YwL4x zRpu(j*eYz&cqIG22AifV_eq02QEAa8&s0G^PR7hS0n*oPMGO6%5Q12ebF1pR`mdh* zI#F8xJ5h)sLdtDjmFq7pU|v5*m7r^5xPFa};dY&?@1EzmB@=poHLhCEul0Rye_xH8 zR!yn*$Hywz9K%ugs@b$o;jVWWM6&6499JJ~8ADtqageUyoTG^TrbMjWrBcc)Y|5t5 z2tc|kODPm9+G06bLWHZEBV#h8&M_VjT)Y~2c5y*=j>Bc)^0~teuT$m&UFx{Ixd-9M z{^6c6z?6^@X}vif2yt$l6yrLIagD(ax-m#*;v1V?^nb}pZU~h?7T4vPr;mS42!U_?^Edd)YwvLJ*@2%w`YjJ0JmCDBZ?N0# zxV-ocAAb0h!QJDvdwVw4@#N#*b9Hsa`S2PK&bGXK@;M*={&QK}I$xWs9R08*XSg|z zvauP5_enGn5lux9@_65SoG%=vYpyP@o3mSIg>yJhlWah#A{SY@I(zSBA;_7GZI*+} z##R?1mPPA$Jl4PKB1hXKRa>wiI*+MDi6gBjNZh-B&f8zQN9Qeu*d4rbDg#Nz7LHaO=9bHaPWO*yp9U}+N5H71ef zo=5LL;rsvR4|)3ZDHop~IUWnwFA|?VdV$4oyfT<%_~18BIN!dL7-hy3cj_jvaBz@|Ir(Qlvf{;xje*T49fG!NXldzT;l{m(eR zbB9kqeoCGPvh$Q=F;(_RR|HYG{LU4IUIfoYwZ&4R zRDMzt5Mk4IL^JcsXXdYV*z-L$7zQ`6?FZpB8$m!NPg(^ptMyVh?sZuhwB&hO_P)Il zT5FqR76p-kGTK<9L4=@1A+2aR){5H5YF*X$Wqy@5p25{hoH|&Zmajt*Ri0K_7ibdY zwxYVH+MIGEk(yeI zSkf=oZz}oM`hiW?HKQ!KJ|ohRNfBH_sdG7pSd+Q>JgpI%^B6_yGp+d2e}ETjtN~+1 zjUgLsUeO6kB|cm0TQ_8ik!(`^fSGv~@75_f&i*?z;F z*L&vV!SvuZj8W`xr)k3G!13ma!#I*kCPhQY3D-HiGvqLHycwBI!aEq8W8-r}Vsk9w zT$BxS41yMrfR174c9N5B7^g(mzm>L=7ZmS#^v*g zmoHy({UTsp#`(zMI5PBmzV!B+U>(mcKj(NHF>WB|Q8wC~iCMZ8`%NOpnZx*;aU8iB zkED6=PS!<8Cvq;amNb?Hy79%#TGnQ9LJ{HW+_>6$Pm~*+LtJI*s&`z#N}^&cY|nPQ zasD>qW-wVw#ZKja+y-YnR~JXFuCCbnJ3KghgKvKQ+p@@}9?yGR zy_`5+rz%g@FHIkHGc(4BtWD0O2}&3sZZK&}nmY1qm~IT07c*fr#7PhU-)(Si#iPQq z-|Q;V=^4D^;^{R%`Oz=<@|$1d>tFw85Hc5^Uh(AP3tSqwv)@Y-(6}@e2qk7iha=9J zjXfibiOc6l^6a>~f5_vfLX| zAi&FW6AH^v(K!*@wxv|}nksig!Mcne;MwOd`Tn!Nm!!aMIe&Pc!`009{@4GVk~3j6 zy!qPO{OAWi<>x>BHHX82H{W=Nc`E$<_kW1@mc#YLy*m&2^pmIj$N%s>9zT9ej+VaL z^Ze;6{-^)(-*Y@3xpV#yi{-;dpOMnXP%PWwP79F2oB%KKb)|@$%q+=?Glmp37_{Ud z!B5dHe52AgOQ(F%`&I1H+*HK;0l<4FhZZUI<|;j8akX>YRFw*9uv1#lBeH=s?xG~$ zvPpy)=4r;)4(QexWsDpXs_c@RD`qTQ@tjf(m@N*NLt@)oOdc5gId``YiDpYzj5xq~ z9UrR()AW|ed3*tkr=X{ck@WtR&t2cU0dVfkO zP4`~W{F8gDi9~&AaBS}()sL^svQhHf;oD1u1pmkXk4h;b&R3`Hb- zLXhCoIKw<8o<4ob%d3fQd&Xwi^Zxsfxp;Bl?!6879t=Eu?Tr0dV!yZa!x_HYgWu9+ z31o;f;dp}!o_?N~Q=|mwk!A#&3iIIxb0`oJy&cH0U{b~#DK87MshVw$-pj;09IuI` zu<6ct&#??;FItU7F@LFr>{f6ln_|30=i!+vOzo)x9@aT6R@zL);k>Bag zDW&ki`=4O#XOseW_OCG=3XjJNoHL~8*!XjtLSR7dE7$V}HKo&gKCJcY`UJzLTbgVq80( znk8nJu*H++!pnCzlCtN&xNtiRc-C13zWUL4uOmBgaV6qsT%^VLC|M=5iaOeC!&z`eT)=T9#H z%XYgb%}YZA6lu_FnvYc*5D9s5l&s$3nv-Dronlm6g;FJ=imANKkeH52HRy*IIN`T`;Z?+9bV1A{u+QX%%`^n0Xuz;?Xex%7G6^XIOF34W zPo1GT1Er$DtCRy%DN;GB-?ct>eN=5zG5!PryS*9OaugoE75&Mj$wlVe;0z`jQao=$ z@Z<5woRhdj8R0^zcCPLh%Vw5ZIa|L|H%)!r4oY-0&pEd$kOn5&T%~ivx!OUVmS&Q5 z5Tk*z`zM0B7?;MBrU$(O6G}-f_>o{!)+L$WqN^SbQuVBNlEiDG&~;)ElWOqMb=5XDC%o-s&&ips+th}PI#8@*J_TdD58GeZYK?$ z`Wb7av<1CZ=XPl&bG_GUkTOk^G_=+Pw#oc$HVl;xl~N!^M=aG+cg5S)@6+p=XVK2r z3Yd<+c@~;pL4wY1B`3C4GzcNkS7aDy^C;(BrO6$6k|aiA2$;i2A#rvt;D@5_wgORO zT`yabQU9*V!#IxZo{5=KX+b`K@A{Lfn$FdEo?FtY&r6!1UHd$_FG~ZRzq-0QnF!7; zQLknSlD*W7yK-Rl<(!mLwPe^J>!JgBSpdWyP6lVFW%igM#KK%WAw?*n25^0S;PU#w zJSTz~!HSlqH52nx2%{rSfjf6S(^+A+FLZqo9wC)`Qy`9o5G^J3l_e*l1WoX zI`*iMK?$<3n;Hb=TrjC-dZ92^#LkwE@i6n^#T5@89O%3yh0HuBj)x;xuWsrCHWG7X zk=`iDK&mo2wL`P0#bP$C8L6sKqE)rhT*YLoFDxW`jKS1tYKr_#IbG}S((y3jjb*dh zV65SIJd%wxza#`{4k2M}V$*Nw`kpW+=6R&EvalIvT6QH|#U9@|LJl0l#54*T-*-El z=_zRx2_U3_Z<+#o-*dh@r&tL}(i9=htFD0phQaNwsDmH%pFbTs?~r^H>G3 z5`?u)t4NTremSU>0y(UR28oQPDRcSan!|%5`)4cKl=sy z{ho0g`TXI zPd;biY_6|QfrxevNCm0y`=;|9LLj7ga=q<1bJ|$5{!VK}DZ^%Q@k=SR{au3%{k;8N z)K6@6!_#1#o7%&Cq+CbLT09V(c1sz>v z!MZA%la=wo zjjrpK;FD$C>Uo}w`+T~->$&Rn>G^50x#nlfIH1il-v(DUbpBR$Y7N$=mFo;x>&SWO zdUeK$gGHsg$niQj)|1q-I5J4oujNGu#54Ibkw_fMzHuif^&$ z@D{=>!Im=}<23U0@pC%YbH3X$%|}v7jN{1F%d0B0A%mS@}&bi&+#GD2HQc zD8|C`8hRCp&P-FRnZ3tbhc#+85I7vJ8##aIEY5WFz5p{hMN+(hd;lWqwyNXYdoVT7 zm%)50gsk^K7WXBnFh)wUr}uNUQQX>uWc*iea??{VV0$v6#d&`B4*TsEvWQyes$7wC zCKV;7OXst#zR@vcI^VHByTddG{>x8)#_u0}#4rq8UmtjS@f<&FYSlv=o3d&(*Z-xm zSOIpO*vCG7{)C(3b$dUh#Bn^zvp~62wYni6MxlsBW~thTS-5a>nECXx&w2LzIXNX# z3T=TD)FwiLSZN#~28LlP*0Eufg~WGj+Ws7}V5UNe%^IwCn% zZO@iu<*K%^pt1VWHMkf@!L@Zyk6VxV7C_wEEKxd{TY~SC4Ye%mk9D%%XGA(IlNayy zG!l)$*eJV>XF1p-8S<1M$bg$no_bc+L@AtlAnkI%#N6L;+L9QGV$75(sK-xsAsB{Xk1*|&% z8dan$4M1%il-j$KS50|!ez$q#9Eb5(la3mEr=?u4MkWg~yHo;GUJ~0IV7uKmhrzDv znCi7RXRy4;k3tP3eZrX~_*@dJ(wb{Q5ZP&(#L%bf+Sn|mFjZus}qLkKWqac{qCVfurjWsEl>bOuEU#eVTby!=EX#g9S^D0H8L^ZL{_iv9| z*#{TbHyy)X4-_rbKg1VVCe=Elx3`cLeYFW(P8V>!Q4$}nNXBHWGxS)zvxJfx37V9d zCP8s>UpL-)OW$pnroi=8m0TJP!;t9vg6*WV>zosrl%&Xxa>S$o!saR`k{yn&i6g7O zpj>A{9a@wNyG%~-UIf)Y{^%3#oJpptB?aO1WlD;GaBM_cELD{T;lricP*LM%@n3c; zX_mGq*AlZajXxx#L39S4HLJit%8C@454bTj!-g0Gpu94}7#3tHXYE3i&rzL0YfoK< z3@oJtAWoq+gk%mkH=OM|?(BEu2lv_T&+t{AOUft_-xbdL4PSfrOPp;j$KxydDxmDW zXBY-Hn+<)xX(feHuSH)i+eXiIDOa~K+a~L233MIY8e4Eq$G8_MIO8QC#IxV;TRCVR zk4)1MYWJ*KC|e^&0XU1Z9ifcOb8Pqc>gtB)&tJ%ydeLC@ooebyTlW(9=tN$wLo#yl z$G*6@5IaFz=zNuri{;k!uNMzVWYktMgj9pFZ9@_SiP6m=pwcTKjgcu;>%cJKT*u9L zs7)fNH8$$_(gh~Ysi7Dlh{T%m!k2Sf<{LncV%DHeW9x-a=AI;~4Pmaww~Z?c@6lIu zBZWv-SuT}E;}qR>)^dG)*%slPGxIo!tPWy>8RsBwXSYjoDRB%^h01oNTYBrI@BE=4 zmQpY|6IlnkYYC;?kOxi9k*jV2l1L#f99m;aO)_Z7h5<>YZ=Ih3OspErrM)9RtL0${ zHidhSk>Io#TCz+K4QgMVyqt6N&eL}t-Wu-gdtQHdmv4UKtL*xoG>Z(OcM_bJC~{7E zU+gMeyPe?3{<9?G8YHcQs+RD$Q@OL-4Pknmns&I=zP8@)+IU$#hxH$AO4MVtw7L#< zVQDIDUFo;R!a8uijn3&g>jtk{q&a6?O+0h`+@-{>cF=W|%uBUkjjpLQn1Wh*FZEh< z4{s=4KII^nyp)pH9q{^EQ>1bhBa?Za+xO0^a=cnSOKf7>c>*Uvte&IZ=QT3ff;6)P zw`*i@UHaAlIfT%vEhU|H%**e`6NFiHVjE(ZtNn0UDgz3ntkK4`l13YA_KQtviioeJ zt@QWT$^PwRPw)2{2+(_^V`x3M*2%{0<5aS2sEH>?AiiFcPpTvu#3ogkF(*=HI$m@i zXdGJ8ouJCMDKjm}T%Di(-HWq{EbLn9FQrI#eRccLfvL!z$pxGe)g|~-kzglmHKBP{ z*u9~mrCMMX>9({MoHe*C+|%vQlV?b?c+UG;2FNK;=tyy5JYHc+Ca1tWkIeI)VVKwr z8~T36_C40Sw#nF1uv#;7q7zE>=S?|_re`ee!K4&VG9uwI)q!1>azdNb8MP^ zYS$KmNL%G3sDw9q>N^@3OHa}PgDW*~poLvSGMU#nAU1ciT zt0fy}sse)8cG}vM5K?R$D9u#ox+o6A(4<^UPR(&eQDcJ>g@urK_410d{XO1&_g&t8 zJGGLzl&XX*?!4i_y}Nw<>tE*P>WZ7I7qXzSlm%tdcPAV*Rf^D3ssyp88_hD&Um9Rz>V>(k^IAIL<_*>)v@=H6|Tl5{kh%jaVyaw-WNb^Ta6U zC5?Kg&qD(zV+>kN_r?lWv#hBga$(E0>4nzK0_5D0>O?6;j>XW~P9i!Zyz0u%#~3+3 z+rD32n1q7SJCF6g=^tv>e`o8WOR5ckrv?+tI6F=B*8$D?XWm!Gpz?=)?=-+^RR$H1 zcOI8yo-10rKHk)OX064or03Qff_JBz*l9PjZnToD87oPKaaJV7ENr_T=PZ3^=m*bc z*stxYRbP$##Jk#*^N zUA|wB3tiKc!eaF^^J(ysYbEJ+@GD$vN2=#++^z3uYq*!5kuj_S8re%?5*ku2lF+F> zv(B~Ndraxt_pVFzvD)62qHC0Jreh9ZPgMexim^sYCBU&QsMNj5te!zJr*rCL4&rdu zlS`^V$ntEJjsnnCK*5wMA7z*3Fl!A%=b=;At8hX~u@>n1Iv4CoGgK*s-Dcb9Bx{;O z&bjSf8tCb{x8$S-us-?r*0c$wvfbvZ0}B#s(h?}GZ%~`Wl`|nH znuMWs!2Z46Y@c73n4IYF|1-r&p!duj=av5OSf)YaF+0&<# z1ZQ{7<$UXy8Y{=rlq|As*RSy2QBvZ%>ZPtHzbr8FEVZoA#^$O*ASLJ9!9zaz_c~}o zKcQ{%1?LMfXX&!63%_Q}&US5b+5%v>8Er7_JgbkgOT>cXdGQ@Q7L2%6RXugi#V_*PO%rI3;G zy2B8y*hpqma99#*E2*VmD3NLnE%H)b*V7MP)Bs8pXCzxS3T1JVb!$__<|7yno$bLD zawxJk4Vxx8m~yCyZjv>v_kwU%xg+iRR~kUppxN++|0;6aO07)-%btvJOWmw1vZ@G{ z1hQTfSCB?epf$fWw9pojQL+xVD%CeXb9*XNB&GfOo~y_+%O>Qk#rs82edr9{S^8eM zpu1tme!J&vw_~^Q>^F|Hz0gbIG(zsdi&e0-k}ybuvTXG0KvB!lEpS=Q?e%-7fqPZ= zSp96h$uEhrJ3;flm;ii{BfDOE*1@$gr<$j?gW1vmh6QD!&J$Rg)lQSVaZh5{NcU-IVE^$JEaMKNQ5S17bZ^<0~B_8rUyj&Y}}L3`6TtS&unuC9cxv zahpoB3IfqYS?`J7pAr{!23I5*(($?Gz^;=gJ#S4w^qSU9nCs`P_iP=kulHB2?CJeo z2e$woZAWp74Oq!_QdlX=0a;zfeSfFio4eNtX+28BY(^U~aq#0ke(X&jULC~_|naLPAX zo#feYIL;EV`-K~%*18xEQ6^Pq4bybw^CzG2^!XEw5!zVr-eZm2V^SuU+C6Gqdj>Lq zPcvpkmX~eqH0!)Q8P)-+F$P@Cw9$-E7xiVrOBq`#3ppaiBRL(CTt4Mb)*oa%3_KHll6~`fjaJ-$5=;*i8)EbQQy}Fn<5dvveH{U7_y^gH3oMJ_k%pq%o0PVA~HkUofV2 z`rC{(nNpS@s|Cu=q1^L@!g$&U;%i_VRwZX+3}WJN8q~_Q3IDZ}g-5G_m;SfSxz(|! zXeuXjb-nh8x{m-1>$3Xo07RUNEXB$uy&X0cJdjG2qEsybuQ*W0fWo!;3a2hfIcLmt zs@PI)CG{F4mwbv$>w1y%+78=c@WOJ74#8z*&o_!BkiG+{WiLkjg?gfJ}^Y z8;|omH@PLHGf3c5DIO)`Y=^ggby=qf+&XYlkYnAvq}3S%?DzXNcTbS&y0>)BLrK!0 zo8-HPVPMmXRMhc!Bga<#;Gy#rzrfm#B_XlHrX_wl28QZtTIx8oPWmbY1d^U4ect-) zHE~;a_3LY0Ema^%$AthI6|AWD%ONHClryu+sTHjTP%Yqd1N=;tKV77->UfHgQ15jP zvBg?n2%w8r8R}SrT9saYza?O`0bbErop1Wx8at>Wj4`HhbW16C9T>hB$06^LSnkFGvdneqXm~JRJbGR9qX9;k9C*J7Vv8rW49puhAyn$F&#C;t*o{0OtDLuvQz}UNbpnQGKO^GYx%RG>p9o-hgI;DD}pJM z5>tfJZL*Oj3Ts8{uc&O1CIF@-Hf@8f@9VQ3hCy_XQ)aFW8GYw)PAWC26tSgtQhFQe z*t2%YTQQ=gF6)mtAbCaLPlCWDuoJ|6NkG8WgwzSQ-Y)BoDI)vRc}v%APma$y(;1KP zj;=Ound%6p<5#|CX>?Uobj?L8rGS&_#X|Y9bb991_Ia%mp%s+%&p{;jtd;5pP(9>L zYmBhg19@>gv#TJhWbh8f;@63pphQ)9#tS8+{Qq@*Ns{DBvYeU^03tH8Uj6^2MJT)( z7d#RdJP#Kl2&mHKP7mLd0zgs~pHgv(>9NO5*vq`!akk9>V0Ci<7v+6aSVVd^P+56JoH>DLRHO<~h zT||sQGinDU%OMR??7c?1S%5`Ki-WTB)K<&W9?ztEBr~ArYn+1kaX->}z4t0L6}U2t zXbq%*>+yJcMSN`y?JS^l%35y~5u&d*5as@_eOOL~kn}v0@5{L+0eH}q^ie88$~ay- zYLmj&(Bw7N?LMAAMXa}x{&ubL6rAYavsNT;L+jiH+0_tWG)Ja6okn=b}B%ZM;>Ny9IZM81N-qvn9Em=VGBEbo$ zV_h@Sl958hQBKd~01#bw6Ox5G`}NsbJ-ekC`0Kq34pA^(YXw>Ww}!KcUoZzDFF24C z{I;S%vWO*MgfOuw>Nr9%kBq@ywCh`nqK()8ZGL*!E@HH^saL*aue}zlHFbA*BBX>A z0zyjYwc_jfL_JR&rQp78U?Nln&_v{pG3j<+FNoe#GxrTyTd3Vu0k}4ZApki?tn1sf z=?FqzSL*@YooNwj93V=_O-*?rHzyTr!_6R4#VtqtxFzKH8{Ur-_ch|E9YH$g_t%#mABo#q7`M;r}&^ZLnft-!gi-+iR$Tvu*`c+y`y0Fz|m2sXYUiI)O z>3HoZ=kDp@w{S;GKyMvgH_z{zBmr0^R=cyXGLmjLN)#|3*`UmwXpfz^6V|PFgixgt zcVd)ln|gYC^a_l=+JJcUPDKpLMg*cu-($3nO*?FG>u8-k04b$;%h367VFYFUx~6pW z016SusfP?LsAbZbvB)S4^vE@Tv)5o_Ac4{uOifOGeXdX@NjPfJ#}SS97@PcP#vz*x zqqP$z*Pv<#3v~X@a)iNr2$)G|J>cA>Eokq@&RE(LLa2=avGjv2iP2mzaZ6`mR+W%MElrHCs;4-p(mn~PKsYrlAAv2%kl{_?#* zL7YIeqtg-sQX+&!g0-xAb7AY`uq7~pwT-l>T)u#KV#+-vicK6G(wykOTIT>ci#Z*EH=leGRgG4~Q`-7&SSE zRPhT-DjwN@rD9~?8;v|h!9#B&tp`k*ExM-AS%YEFX!mH>8KZb)#wfbCLKrtf?2MEY_$p|%h6FL{K@|u~0&eSqB`?S+;(m+BIbew$OO7CJh$*7h!I-V< zUA4fA2QR|3j6ltp$37Jf&G+xmM4yF_E2oeBsm)Wm?%^`_9m`@etFiB%Dyd3oI3CFR z1qqB}L|(_);Y=Wj=PnZ=<_v^@(<{t{VM1#tTGU(XiI0yDbTlD9$}WDN#tu?K%n4@& zUdQgFiZP>BWCXzVm)3&(ep|ihYse)CKYj${9I@Tr{54D_@Xvq#6MugG<#u`IKiZo3 z#5V3Ihh*|nh%SHGHVOP*uNT0hh%M)gA3uKJ?fo4HgjTC|np)9V8YJBi#E&~m4asd{ zE!m5?WR4xXNDXq%IBS=+6#`BzdO{3XZwWCH9*-AZj|Y}z18H5tZnEB5MY`QqKgefxPOLb_*EuJ|D3f+h*E*}ERlOia%F#73ty)AZn@36s3BcJZ z@)D8K9i?@IB_N#*uTvtu)-s4Hm?QX>@U<5Z7~Fu$5l5}yR;8-7SD*&hx>=e%oTK*JC6hDu@eK#BlTmwRHBm9O9%5 ziGDcy6$XO^v5f+2@r*8ddz}{Y1ss_if{j&FcAMKs@5r^AyM&BcXLcQ5(EF3d0CG^1 zR)$A^7HX@eh3T$|9dq&NaskQ~^THR0<9Q(BvmXqf_lQ8DlA zvQ9X&h)Wt=LiY>v)+8e27^SH~m5x!7M#Zrl014kPrOR~OMWHueBlBLC(<*w<3v<){ zcCHNej}=J8znz1>&hLm*0Y*xTGxrt@g`nw1WjaA!HSeO`rC@#ObuLZ1NMm{gz$C;l zJTpy$$jmOqFo0*DyEd=4i^0Eu-QdqK$AEek9~*E3F(V{kO;fu`36VW;b7A@_pM>K7EYxS44DPtw$VF2#?c=#ICH0(74Xo2J5uO+S=>!fJ} zf|51nyZW7NwbwLFl!02eD zA*G1h4S2i`^v90J>%f|_2RFxYVBdGVz29^k7)KQ8!I~?G82|d`zrADWcrJ`m#MJ+` z-tl<8@O*wD?Fsk$ec&Vpw%bi~tG3_70Dw@rVoMudua{=c;L;9bD{HObApYCyDsD7! z$-~A=fvagsEr1EZ=Dv)&=FTwxH+%LRhS50@ zImnp?NdNB+Vr79zE6-$;QY%I!EhTg?k+6K+D`F5kvV@MNfP>3{{p(L@Z+T&e=IYaH zFfsougz$@*yRonKwJ}haJDLuxdR}#FbI<||!TJn9<{+o>ss5aBR#XoE3~r?I2|1kP z76WGY14Oe0nXFlKUNM4r;MOK!mKh-!py=}a*??DrwPq9$6~QT>#&J!uSy;p+G8!ga z(&^vl`}g*jmuK(#xxWW(M06>>n#W^2S1V>waOuxqfvTt9G}RO&69WKA_3OMuXAb@P zqI~Z9KmG>RwCF2_kw%{9;F4D9bZ8#!eoZRzy)ea|nH?zK=a6;IvcSvci4;P1 zVT6d)ok17}=E2BWA>aRRZIJ4sTb)~#&e}@3pOi<|YqqMfb(GR7g&1Mz(d5Q5K z?)N+1-rj_j*YV@lQO!e`f@a}XNeim}GX@~cz%oP-Sai23BBvxwu%{U5mur{_hwuM{ zNVpa$8A}TY0YU1rvV~3cn-L+24W0o~D@GB?lon>1>k0{TUdrLG?fXM(sFhI4NW;%_ z#nRK`nnr=qe61$our#nWE%@v656M_IO^>H>4%c>~){c*l4{Wz(8hf!z6hLf%Iw7Tu z5C|VXe&Fr>=CAYn@4w^s-+%Y{oO4FZ(#dPPzvF(tgW`hqeuH$ko3KC-;Pdn6PrSds zJ6N{8`t9Qb>$zI54(tK`9OU{s56Qijcx9!nSRAJr7?$wVZgg z2hQ_!TS+@BCE$`~F}2)dx?Qcd5y7tn@SZaje9mdHTZtIiAxi|BCkfe?3aAW(`}3nQgN8V)d!W58J( zj>4cA#dKbC@B$CT8Nj3pGr+lg%s!Rh5Aa|BRa28u^f_w;GIc!BP_>=rx~_P;W!x4y zoGoR~V8qDC209}28n1;6ArKlQZQXlEqwlH9iitnf3TI*R#4k%;%NQs5CrpkwLPQ4? z2x6yXg)rl9e?5`I?mO{-2fV%kOPFi{K4Ha+)tK6NyvTaCVx&o^n|uCYc__iK0ZEB zO2J=${RIG*wywqqOE4tYWxJ=_IQmPm;vKCIgp2tOELpy=_;d*Xi(nU@XWA_eP z%)|E<5E(-$1-%Wuzw8kL{0*1H81eY}6QvZ~f4(`W3IPi2jAe(eTgfDvm+End)bRcN z9abE5p28LzAT+>eKLd?q(~=ac`ysXtvo`Mp2m;oft(|9IB<5x9tcMXSd+*IT4z-A29yl3q+p$C z8&UfJh76DvUeLtEdMZ|#zhDzXDl*@e4If)VX?O2Uol??PuX3neI}!y4AAT_Q!46rm z8MG*g0pHB#LlhWRTz=p(xz>bjPw|OWwOk3bv}gVbnWHvP7|vJ0WGUC_`d)R#^}%nlV(6br~YpR zZ{NqCXjJjCF?{{+f0wxf(LkZ-ihF>MscmR}T_J!2>v)XxCXl^oz&-@9Tr0`t8Iprf zk_#T`C>YR|@iqW50|C1>1F<%a1~RR%#VdAIE@2>+d3J+P@S|q;24YiO;4TP(mFXLW zNXFg`MmQobQ9+K~XGGJ2LDS*S&pre~P6@R&4-V(Z+yd?tuHbE6KQrqmfc-Mehdqd3Pcw%j}kNw2w*TaDa z1>o!P7xv?+GJ%BG>xHTv=b$wkEd-=>LDV+U&j(P>hP*`l;~)RP&!0cB?>pXq{KOxB z{NdJ20B|1FKM&FL)wj#uLPW~!6=xIW8AP0C#qYoWiQ_o%^<`ZJMfYfbZ>jG=oDnEl zWqLrW|x z5wL7Y=SbEdrD0t+tm}rntcWQi)P{Y30eoQH7Ni)F*A?7>{dl1qyRx#$&$jpXcL377 z!GIwqtDX5VB%7CLuxNK~!`9uhkRmeFzzO!{}_Lt8;pD?^(Al&w6 zbG(o-8{-E+yHYkUuzfzCBbWvT$Omx0-yPWR`|i@5WQg0$rA>$D4|3XcY|+|AJBkd3 zg@`?%b(IC(F;aEDK*I3(viB-gw6NuDQk`rYaU?LJu%KJTk|Rp(cr6iUm9!mqLM62Y zQ-4UJYx_TDmUMk5x1Rv1FC)PZVd8*}QT=F)msNK{8HzM%UhJ^9>ORwRjswyBT;*^V zAb(A9GP4(}bWT!j-y!DQ>Lq6&smsYG%r&dYxzdl3qT)^R_XWhz*o67BA$r7UxG*aB zfhCUaOLBkYdL6_dDR=F2{eBDh@BjD@2^7>XGleMaX0@7fNvm`xYu!64Q4GUJbDA4A zUab|^_Ra^?JgyO<4X$jV|vH~IS{X8j!G^*VL^x%jS9fCpJNN5A_NOMz$b6tz5$p+j_bdSVZ)-?$Z_c#3t2 zXhqYTQvs6|Dp~v5)*8+-0wvSoGBH+Vo9uA24cF%H_n;w!DTS{VQ!}U<>39Gj{J4MU z>s_TNeyxCS*7HW+TEi_T*MQmY{=$~$0pP57Qszo>#>$w8Vb_%XxicDK49+}RdfDdd zlC79d_v@(AmCoQIMJ1FoA`#xVMaD$eL(Eb4Ly??VkSwRgNecX$@oP57?#ccqB2T;a4sAmm zhy9e+fLKde86zo5`~CMn0d(AMZ?Jf(YoTuEhU0j6tJT&T_Sc~WQv+yRK{MfgzoU06 zj)9N;?RG;1{Pk@A`uzO#F}SZAx=IP73Mh+2BtUf(1Y zvxGv35c%KcuXR5(t*prpdba{7kw^##W@p_DV9i-v&n%+jJPVFf+Hc?QcVBC!AGY9G zw6j$*P1Lb0*|Pxay5jRw+Qt9)@dL}UVBdESuv+Vr#pV;|DH_w~>w$9YXuaSb?^1Nc zA}^4+u7$ch8XF9+)#Qw#opVMA+Wunm-I&VbIHbL;UNaFn__O`v>+1{0abQu4F#G@G zL-w)9=)Y>3)n#{lV82EQCX<$)`8N*z6*^t6M8*h!3g(lb(g;AmnpA@YKmz39Co($G)>K)}%Kns4V>WuxoZ@P5Bb zwID>NC=4mPHOKgx{QL9IuXAf0_{RL$qPEX{T-7YT)?ZusH87}=7r>P!-~hAM3%JZ_ z3mT+i{n~GP2C%kCTfvuYR@_9b>z5AMIns9gvt#O!7J$9O~C$5+_Kn8iOIh( zRKbBF!-5Ow^Cc-HN}`SK0CJS>QV|h|aPS({rYeVsPTfJW$y)5dx8TdPz+Klt&e?&A zfmqi}0TlCdys}V*tomOFf*=)1*J*U?7%4 z;+g_LP3$ZK^a0_0Gi$JbM;c>vv+StbNL>q`Oe zi36uIu&s6wg6fc~&qoUoVw4VebBbK%Iba1^@_h*ga$dDyIUt6FoEVSC1HH+<(Hc)F zcMZms#30I87ERT*inCQno98Hiwh2ot+Q*DV3y`=IauVOt)`YqG?L$E5zV1Fpy(vqR zMkGyxg!6derOYJ);rZAdn0$VI0y=KXvUsXrp0mh+h=d7~;IRJf9M1XO1CFn+F9ogw zoN7BAcpG3cCiU_4!t?7(#`AUu1xBkP9SuYTy5yTlbljN^D|z_Wme@qEf$SeEP<%L1#Q){M2wS$cchE?J~NVi6Xj;FXjEjzh{XOm#2J}UnB{KJc->~HsS;`#WTh{fi9#pd>5V`Xb`1TGSs zrZu7Ug5!{kl?7d9t7RY@pv$_o{p`6v<^JF08K31;U2SqHkNm5`lSNXm-GDT@MW|K? z-_nuje!=#;w!gPiR>2FVi zG53yNU9^}nq-&!K2X~UrBi8nVko$-+OQA$a`20GsBp~F7OichnuP9ZlSvm=$n{z-r zo7g7>0hPT~fCV%$G3wb7RIm@~00H82t*nf8tTKkachkw~ff&t8cala-c4{F6oOR4i zW=3h%4_0G*M@loe2W&G7BLl~+Ay9N3dGFelC0t)af)-8B#N>rdV?kQA^rETbNp3Ou zNA@nA#Oyvqx{@wI2Xk|9KCjUch+Naa2FU#Pras{FHKr&U^%=Z*_8|o1m_Y%a^6_

89CItzl6eMLx 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 Options: - -b boot-dir Path to bootloader build directory (default: O= or output/) + -b boot-dir Path to bootloader build directory (default: same as -r, or O= or output/) -B Boot-only image (no rootfs, for bootloader testing) -d Download bootloader files from latest-boot release -f Force re-download of bootloader even if cached @@ -48,6 +48,9 @@ Examples: # Standalone with separate boot/rootfs builds: $0 -b x-boot -r output raspberrypi-rpi64 + # Rootfs-only (no separate bootloader build, e.g. EspressoBIN): + $0 -r x-aarch64 marvell-espressobin + # With downloaded rootfs and bootloader: $0 -d -r ~/Downloads/rootfs.squashfs friendlyarm-nanopi-r2s @@ -457,13 +460,20 @@ if [ -n "$STANDALONE" ]; then BOOT_DIR=$(find_build_dir) || die "Could not find boot directory. Use -b option" fi else - if [ -z "$BOOT_DIR" ]; then - BOOT_DIR=$(find_build_dir) || die "Could not find boot directory. Use -b option" - fi - if [ -z "$ROOT_DIR" ]; then ROOT_DIR=$(find_build_dir) || die "Could not find rootfs directory. Set O= or use -r option" fi + + if [ -z "$BOOT_DIR" ]; then + # For boards without a separate bootloader build (e.g. EspressoBIN, + # where U-Boot lives in SPI NOR), -r alone is sufficient: default + # the boot directory to the rootfs directory. + if [ -n "$ROOT_DIR" ]; then + BOOT_DIR="$ROOT_DIR" + else + BOOT_DIR=$(find_build_dir) || die "Could not find boot directory. Use -b option" + fi + fi fi # Set up environment variables, some required by genimage.sh @@ -501,8 +511,8 @@ if [ -n "$STANDALONE" ]; then # Build directory with images/ - copy rootfs and partition images log "Copying artifacts from $ROOT_DIR/images/ to $BINARIES_DIR/" cp "$ROOT_DIR/images/rootfs.squashfs" "$BINARIES_DIR/" - # Copy partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Copy partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/images/$img" ]; then cp "$ROOT_DIR/images/$img" "$BINARIES_DIR/" fi @@ -514,8 +524,8 @@ if [ -n "$STANDALONE" ]; then # Directory directly containing rootfs.squashfs log "Copying rootfs from $ROOT_DIR/rootfs.squashfs" cp "$ROOT_DIR/rootfs.squashfs" "$BINARIES_DIR/" - # Copy partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Copy partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/$img" ]; then cp "$ROOT_DIR/$img" "$BINARIES_DIR/" fi @@ -557,16 +567,16 @@ if [ -n "$DOWNLOAD_BOOT" ]; then ln -sf "$(realpath "$ROOT_DIR")" "$BINARIES_DIR/rootfs.squashfs" elif [ -f "$ROOT_DIR/images/rootfs.squashfs" ]; then ln -sf "$(realpath "$ROOT_DIR/images/rootfs.squashfs")" "$BINARIES_DIR/rootfs.squashfs" - # Link partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Link partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/images/$img" ]; then ln -sf "$(realpath "$ROOT_DIR/images/$img")" "$BINARIES_DIR/$img" fi done elif [ -f "$ROOT_DIR/rootfs.squashfs" ]; then ln -sf "$(realpath "$ROOT_DIR/rootfs.squashfs")" "$BINARIES_DIR/rootfs.squashfs" - # Link partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Link partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/$img" ]; then ln -sf "$(realpath "$ROOT_DIR/$img")" "$BINARIES_DIR/$img" fi From 26a5c3b64455c79e0aa60600c69ace7743c9c0c5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 17 Apr 2026 17:57:51 +0200 Subject: [PATCH 04/11] board/aarch64: fix bpi-r4 dip switch values Inverted values for eMMC and SPI NAND. Also use ABCD instead of Position as column name to match board markings. Signed-off-by: Joachim Wiberg --- board/aarch64/bananapi-bpi-r3/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/board/aarch64/bananapi-bpi-r3/README.md b/board/aarch64/bananapi-bpi-r3/README.md index be92ea284..fd39dcf37 100644 --- a/board/aarch64/bananapi-bpi-r3/README.md +++ b/board/aarch64/bananapi-bpi-r3/README.md @@ -70,11 +70,11 @@ The BPI-R3 has a 4-position DIP switch that controls boot media: DIP switches -| Position | Mode | Description | -|----------|-------------|---------------------------------------| -| 0000 | SD card | Boot from microSD card | -| 0110 | eMMC | Boot from internal eMMC (recommended) | -| 1010 | SPI NAND | Boot from SPI NAND (advanced users) | +| ABCD | Mode | Description | +|------|----------|---------------------------------------| +| 0000 | SD card | Boot from microSD card | +| 0101 | SPI NAND | Boot from SPI NAND (advanced users) | +| 1001 | eMMC | Boot from internal eMMC (recommended) | > [!NOTE] > Switch position is read from left to right: "0" = OFF, "1" = ON. @@ -150,6 +150,7 @@ From the U-Boot prompt: usb start fatload usb 0:1 0x50000000 infix-bpi-r3-emmc.img setexpr blocks ${filesize} / 0x200 +mmc dev 0 mmc write 0x50000000 0x0 ${blocks} ``` From 411321ed4a103406d8f0775cfbb9f7c9185fe4b4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 9 Apr 2026 10:25:48 +0200 Subject: [PATCH 05/11] Add support for developer config snippets Signed-off-by: Joachim Wiberg --- Makefile | 25 ++++++++++++++++++++--- configs/snippets/dev.conf | 1 + configs/snippets/ext4.conf | 3 +++ doc/developers-guide.md | 40 ++++++++++++++++++++++++++++++++----- test/case/repo/defconfig.sh | 5 ++++- 5 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 configs/snippets/dev.conf create mode 100644 configs/snippets/ext4.conf diff --git a/Makefile b/Makefile index f2faa5de8..aaaf3c1ff 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,10 @@ O ?= output # otherwise treat it as relative to ./buildroot. override O := $(if $(filter /%,$O),$O,$(CURDIR)/$O) -config := $(O)/.config -bmake = $(MAKE) -C buildroot O=$(O) $1 +config := $(O)/.config +bmake = $(MAKE) -C buildroot O=$(O) $1 +SNIPPETS_DIR := $(CURDIR)/configs/snippets +MERGE_CONFIG := $(CURDIR)/buildroot/support/kconfig/merge_config.sh all: $(config) buildroot/Makefile @@ -26,6 +28,23 @@ $(config): @echo "'make _defconfig' before building an image." @exit 1 +apply-%: $(SNIPPETS_DIR)/%.conf | $(config) + @KCONFIG_CONFIG=$(config) $(MERGE_CONFIG) -m $(config) $< + @+$(call bmake,olddefconfig) + @echo "Applied snippet: $<" + +list-snippets: + @echo "Available snippets (use 'make apply-'):" + @ls $(SNIPPETS_DIR)/*.conf 2>/dev/null | sed 's|.*/||; s|\.conf$$||; s|^| |' + +dev: | $(config) + @for s in $(SNIPPETS_DIR)/*.conf; do \ + KCONFIG_CONFIG=$(config) $(MERGE_CONFIG) -m $(config) $$s; \ + echo "Applied snippet: $$s"; \ + done + @+$(call bmake,olddefconfig) + @+$(call bmake,all) + %: | buildroot/Makefile @+$(call bmake,$@) @@ -44,4 +63,4 @@ test: buildroot/Makefile: @git submodule update --init -.PHONY: all check coverity dep test cyclonedx +.PHONY: all check coverity dep test cyclonedx list-snippets dev diff --git a/configs/snippets/dev.conf b/configs/snippets/dev.conf new file mode 100644 index 000000000..b744ff27a --- /dev/null +++ b/configs/snippets/dev.conf @@ -0,0 +1 @@ +BR2_TARGET_ENABLE_ROOT_LOGIN=y diff --git a/configs/snippets/ext4.conf b/configs/snippets/ext4.conf new file mode 100644 index 000000000..2b0b2eb40 --- /dev/null +++ b/configs/snippets/ext4.conf @@ -0,0 +1,3 @@ +BR2_TARGET_ROOTFS_EXT2=y +BR2_TARGET_ROOTFS_EXT2_4=y +BR2_TARGET_ROOTFS_EXT2_SIZE="512M" diff --git a/doc/developers-guide.md b/doc/developers-guide.md index d5d5eb721..032fc69d3 100644 --- a/doc/developers-guide.md +++ b/doc/developers-guide.md @@ -7,12 +7,12 @@ account, which is created based on credentials found in the VPD area -- for Qemu devices this is emulated using `qemu_fw_cfg`. For developers this can be quite frustrating to be blocked from logging -in to debug the system. So we recommend enabling the `root` account in -the Buildroot `make menuconfig` system. +in to debug the system. The quickest way to enable root login is to +apply the `dev` configuration snippet: - make menuconfig - -> System configuration - -> [*]Enable root login with password + make apply-dev + +See [Configuration Snippets](#configuration-snippets) for more details. > [!IMPORTANT] > Please see the [Contributing](#contributing) section, below, for @@ -166,6 +166,36 @@ on Buildroot to finalize the target filesystem and generate the images. The final `run` argument is explained below. +### Configuration Snippets + +Infix ships a set of Kconfig fragments in `configs/snippets/` that can +be merged into your active `.config` on demand. This avoids polluting +defconfigs with settings that are only useful during development. + +To see what snippets are available: + + make list-snippets + +To apply a single snippet to the current output directory: + + make apply-dev # enable root login + make apply-ext4 # build an ext4 rootfs (needed for boards + # whose bootloader lacks squashfs support, + # e.g. Marvell ESPRESSObin) + +The `apply-*` targets require an existing `.config` (i.e. you must have +already run a `make _defconfig`). The snippet is merged using +Buildroot's `merge_config.sh`, so it behaves like `make menuconfig`: +unrelated settings are preserved, conflicting ones are overridden. + +To apply **all** snippets at once and then build: + + make dev + +This is the recommended one-shot command for setting up a development +build from a freshly selected defconfig. + + ### YANG Model When making changes to the `confd` and `statd` services, you will often diff --git a/test/case/repo/defconfig.sh b/test/case/repo/defconfig.sh index abd0f16cd..b3acbea16 100755 --- a/test/case/repo/defconfig.sh +++ b/test/case/repo/defconfig.sh @@ -1,4 +1,6 @@ #!/bin/sh +# Verify all config/*_defconfig files, skipping any subdirectories, +# e.g., developer snippets not used in official builds. SCRIPT_PATH="$(dirname "$(readlink -f "$0")")" CONFIGS="$SCRIPT_PATH/../../../configs" @@ -47,6 +49,7 @@ check() done } -check "$CONFIGS"/* || exit 1 +# shellcheck disable=SC2046 +check $(find "$CONFIGS" -maxdepth 1 -type f | LC_ALL=C sort) || exit 1 exit 0 From 7d61be45643745d0bc3ed613d007e47e1fcfad60 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 10 Apr 2026 12:26:30 +0200 Subject: [PATCH 06/11] board/common: add rauc upgrade bundle for ext4 images NOTE: this is intended only for use on devboards like EspressoBin where the onboard bootloader only understands fat and ext2 file systems. To enable, use 'make apply-ext4' Signed-off-by: Joachim Wiberg --- board/common/Config.in | 1 + board/common/image/image-ext4-rauc/Config.in | 27 +++++++++++++++++++ .../common/image/image-ext4-rauc/generate.sh | 27 +++++++++++++++++++ .../image/image-ext4-rauc/image-ext4-rauc.mk | 10 +++++++ configs/snippets/ext4.conf | 1 + 5 files changed, 66 insertions(+) create mode 100644 board/common/image/image-ext4-rauc/Config.in create mode 100755 board/common/image/image-ext4-rauc/generate.sh create mode 100644 board/common/image/image-ext4-rauc/image-ext4-rauc.mk diff --git a/board/common/Config.in b/board/common/Config.in index ed192024e..468a208a3 100644 --- a/board/common/Config.in +++ b/board/common/Config.in @@ -5,6 +5,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-aux/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-qcow/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-gns3a/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-rauc/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-ext4-rauc/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-dl-release/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-readme/Config.in" diff --git a/board/common/image/image-ext4-rauc/Config.in b/board/common/image/image-ext4-rauc/Config.in new file mode 100644 index 000000000..53ad796c4 --- /dev/null +++ b/board/common/image/image-ext4-rauc/Config.in @@ -0,0 +1,27 @@ +config IMAGE_EXT4_RAUC + bool "RAUC upgrade bundle (ext4)" + depends on BR2_TARGET_ROOTFS_EXT2 + select BR2_PACKAGE_HOST_RAUC + help + Create a RAUC upgrade bundle for targets using an ext4 rootfs + image. Intended for development boards whose bootloader does + not support squashfs. + +config IMAGE_EXT4_RAUC_KEY + string "signing key" + depends on IMAGE_EXT4_RAUC + default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key" + help + Path to the private key, in PKCS#8 format, used to sign + the RAUC bundle; or a PKCS#11 URI. + +config IMAGE_EXT4_RAUC_CERT + string "signing certificate" + depends on IMAGE_EXT4_RAUC + default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt" + help + Path to the X509 certificate which will be associated with + the bundle signature. + + NOTE: This cert MUST be included in the trust store of the + system on which this bundle is to be installed. diff --git a/board/common/image/image-ext4-rauc/generate.sh b/board/common/image/image-ext4-rauc/generate.sh new file mode 100755 index 000000000..03cd767c7 --- /dev/null +++ b/board/common/image/image-ext4-rauc/generate.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -e + +ext2="${BINARIES_DIR}/rootfs.ext2" +pkg="${BINARIES_DIR}/${ARTIFACT}-ext4.pkg" + +# RAUC internally uses the file extension to find a suitable install +# handler, hence the name must be .img +cp -f "${ext2}" "${WORKDIR}/rootfs.img" + +cat >"${WORKDIR}/manifest.raucm" < Date: Fri, 10 Apr 2026 12:30:26 +0200 Subject: [PATCH 07/11] patches/ethtool: backport support for --json -T (show time stamping) This is used by the next commit that adds initial support for PTP/gPTP. Signed-off-by: Joachim Wiberg --- ...son-support-for-T-show-time-stamping.patch | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch diff --git a/patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch b/patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch new file mode 100644 index 000000000..8ea2455ba --- /dev/null +++ b/patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch @@ -0,0 +1,309 @@ +From ccd5fe71878cc3eeea2a3137c7b8d5715e3f4253 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Thu, 9 Apr 2026 11:09:51 +0200 +Subject: [PATCH] ethtool: Add --json support for -T (show-time-stamping) +Organization: Wires + +Wire up JSON output for `ethtool -T`: + + - Mark the -T command with .json = true in the command table + - Wrap tsinfo_reply_cb output in open/close_json_object, use + print_string for the header, and handle the PHC index/none + case with proper JSON primitives + - Convert tsinfo_dump_list to open a JSON array (keyed by the + new json_key parameter) and close it on return; update callers + in tsinfo.c and tsconfig.c with appropriate keys + - Convert tsinfo_dump_cb to use print_string(PRINT_ANY) so + each set bit is emitted as a JSON array element in JSON mode + and as an indented text line in plain mode + - Wrap the netlink request in nl_tsinfo with new_json_obj / + delete_json_obj + +Example output on a Marvell 88e6341 (Topaz) switch (lan0): + + $ ethtool --json -T lan0 | jq + [ + { + "ifname": "lan0", + "capabilities": [ + "hardware-transmit", + "hardware-receive", + "software-receive", + "software-system-clock", + "hardware-raw-clock" + ], + "hwtstamp-provider-index": 0, + "hwtstamp-provider-qualifier": "Precise (IEEE 1588 quality)", + "tx-types": [ + "off", + "on" + ], + "rx-filters": [ + "none", + "ptpv2-l4-event", + "ptpv2-l4-sync", + "ptpv2-l4-delay-req", + "ptpv2-l2-event", + "ptpv2-l2-sync", + "ptpv2-l2-delay-req", + "ptpv2-event", + "ptpv2-sync", + "ptpv2-delay-req" + ] + } + ] + +Example output on a Qemu VM with e1000 NIC (e1): + + $ ethtool --json -T e1 | jq + [ + { + "ifname": "e1", + "capabilities": [ + "software-transmit", + "software-receive", + "software-system-clock" + ], + "phc-index": -1, + "tx-types": [], + "rx-filters": [] + } + ] + +Signed-off-by: Joachim Wiberg +--- + ethtool.c | 1 + + netlink/ts.h | 2 +- + netlink/tsconfig.c | 6 +-- + netlink/tsinfo.c | 106 ++++++++++++++++++++++++++++++--------------- + 4 files changed, 75 insertions(+), 40 deletions(-) + +diff --git a/ethtool.c b/ethtool.c +index 9c8a542..f845dae 100644 +--- a/ethtool.c ++++ b/ethtool.c +@@ -5985,6 +5985,7 @@ static const struct option args[] = { + }, + { + .opts = "-T|--show-time-stamping", ++ .json = true, + .func = do_tsinfo, + .nlfunc = nl_tsinfo, + .help = "Show time stamping capabilities", +diff --git a/netlink/ts.h b/netlink/ts.h +index 9442b44..07f140a 100644 +--- a/netlink/ts.h ++++ b/netlink/ts.h +@@ -17,6 +17,6 @@ int tsinfo_qualifier_parser(struct nl_context *nlctx, + void *dest); + int tsinfo_dump_list(struct nl_context *nlctx, const struct nlattr *attr, + const char *label, const char *if_empty, +- unsigned int stringset_id); ++ unsigned int stringset_id, const char *json_key); + + #endif /* ETHTOOL_NETLINK_TS_H__ */ +diff --git a/netlink/tsconfig.c b/netlink/tsconfig.c +index d427c7b..aab9859 100644 +--- a/netlink/tsconfig.c ++++ b/netlink/tsconfig.c +@@ -52,19 +52,19 @@ int tsconfig_reply_cb(const struct nlmsghdr *nlhdr, void *data) + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSCONFIG_TX_TYPES], + "Hardware Transmit Timestamp Mode", " none", +- ETH_SS_TS_TX_TYPES); ++ ETH_SS_TS_TX_TYPES, "tx-types"); + if (ret < 0) + return err_ret; + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSCONFIG_RX_FILTERS], + "Hardware Receive Filter Mode", " none", +- ETH_SS_TS_RX_FILTERS); ++ ETH_SS_TS_RX_FILTERS, "rx-filters"); + if (ret < 0) + return err_ret; + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS], + "Hardware Flags", " none", +- ETH_SS_TS_FLAGS); ++ ETH_SS_TS_FLAGS, "hwtstamp-flags"); + if (ret < 0) + return err_ret; + +diff --git a/netlink/tsinfo.c b/netlink/tsinfo.c +index 187c3ad..da64b50 100644 +--- a/netlink/tsinfo.c ++++ b/netlink/tsinfo.c +@@ -52,6 +52,7 @@ int tsinfo_show_hwprov(const struct nlattr *nest) + return 0; + } + ++ + static int tsinfo_show_stats(const struct nlattr *nest) + { + const struct nlattr *tb[ETHTOOL_A_TS_STAT_MAX + 1] = {}; +@@ -109,39 +110,57 @@ err_close_stats: + static void tsinfo_dump_cb(unsigned int idx, const char *name, bool val, + void *data __maybe_unused) + { ++ char buf[16]; ++ + if (!val) + return; + +- if (name) +- printf("\t%s\n", name); +- else +- printf("\tbit%u\n", idx); ++ if (!name) { ++ snprintf(buf, sizeof(buf), "bit%u", idx); ++ name = buf; ++ } ++ print_string(PRINT_ANY, NULL, "\t%s\n", name); + } + + int tsinfo_dump_list(struct nl_context *nlctx, const struct nlattr *attr, + const char *label, const char *if_empty, +- unsigned int stringset_id) ++ unsigned int stringset_id, const char *json_key) + { + const struct stringset *strings = NULL; +- int ret; ++ bool empty; ++ int ret = 0; + +- printf("%s:", label); +- ret = 0; +- if (!attr || bitset_is_empty(attr, false, &ret)) { +- printf("%s\n", if_empty); +- return ret; +- } +- putchar('\n'); +- if (ret < 0) +- return ret; ++ empty = !attr || bitset_is_empty(attr, false, &ret); + +- if (bitset_is_compact(attr)) { +- ret = netlink_init_ethnl2_socket(nlctx); ++ if (!is_json_context()) { ++ printf("%s:", label); ++ if (empty) { ++ printf("%s\n", if_empty); ++ return ret; ++ } ++ putchar('\n'); ++ if (ret < 0) ++ return ret; ++ } else { + if (ret < 0) + return ret; +- strings = global_stringset(stringset_id, nlctx->ethnl2_socket); ++ open_json_array(json_key, ""); ++ } ++ ++ if (!empty) { ++ if (bitset_is_compact(attr)) { ++ ret = netlink_init_ethnl2_socket(nlctx); ++ if (ret < 0) ++ goto out_close; ++ strings = global_stringset(stringset_id, nlctx->ethnl2_socket); ++ } ++ ret = walk_bitset(attr, strings, tsinfo_dump_cb, NULL); + } +- return walk_bitset(attr, strings, tsinfo_dump_cb, NULL); ++ ++out_close: ++ if (is_json_context()) ++ close_json_array(""); ++ return ret; + } + + int tsinfo_reply_cb(const struct nlmsghdr *nlhdr, void *data) +@@ -163,47 +182,59 @@ int tsinfo_reply_cb(const struct nlmsghdr *nlhdr, void *data) + return err_ret; + + if (silent) +- putchar('\n'); +- printf("Time stamping parameters for %s:\n", nlctx->devname); ++ print_nl(); ++ ++ open_json_object(NULL); ++ print_string(PRINT_ANY, "ifname", ++ "Time stamping parameters for %s:\n", nlctx->devname); + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSINFO_TIMESTAMPING], +- "Capabilities", "", ETH_SS_SOF_TIMESTAMPING); ++ "Capabilities", "", ETH_SS_SOF_TIMESTAMPING, ++ "capabilities"); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + + if (tb[ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER]) { + ret = tsinfo_show_hwprov(tb[ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER]); + if (ret < 0) +- return err_ret; +- ++ goto err_close_dev; + } else if (tb[ETHTOOL_A_TSINFO_PHC_INDEX]) { +- printf("PTP Hardware Clock: "); +- printf("%d\n", +- mnl_attr_get_u32(tb[ETHTOOL_A_TSINFO_PHC_INDEX])); ++ print_uint(PRINT_ANY, "phc-index", ++ "PTP Hardware Clock: %d\n", ++ mnl_attr_get_u32(tb[ETHTOOL_A_TSINFO_PHC_INDEX])); + } else { +- printf("PTP Hardware Clock: "); +- printf("none\n"); ++ if (is_json_context()) ++ print_int(PRINT_JSON, "phc-index", NULL, -1); ++ else ++ printf("PTP Hardware Clock: none\n"); + } + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSINFO_TX_TYPES], + "Hardware Transmit Timestamp Modes", " none", +- ETH_SS_TS_TX_TYPES); ++ ETH_SS_TS_TX_TYPES, "tx-types"); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSINFO_RX_FILTERS], + "Hardware Receive Filter Modes", " none", +- ETH_SS_TS_RX_FILTERS); ++ ETH_SS_TS_RX_FILTERS, "rx-filters"); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + + if (tb[ETHTOOL_A_TSINFO_STATS]) { + ret = tsinfo_show_stats(tb[ETHTOOL_A_TSINFO_STATS]); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + } + ++ if (!silent) ++ print_nl(); ++ close_json_object(); + return MNL_CB_OK; ++ ++err_close_dev: ++ close_json_object(); ++ return err_ret; + } + + int tsinfo_qualifier_parser(struct nl_context *nlctx, +@@ -271,5 +302,8 @@ int nl_tsinfo(struct cmd_context *ctx) + if (ret < 0) + return ret; + +- return nlsock_send_get_request(nlsk, tsinfo_reply_cb); ++ new_json_obj(ctx->json); ++ ret = nlsock_send_get_request(nlsk, tsinfo_reply_cb); ++ delete_json_obj(); ++ return ret; + } +-- +2.43.0 + From a4dd24ed74470c5f2d70717b9e1a3e86e3916112 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 10 Apr 2026 12:25:02 +0200 Subject: [PATCH 08/11] Initial support for IEEE 1588/802.1AS PTP/gPTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remaining work: - phc2sys YANG model (infix-phc2sys.yang, instance-index + servo params) - ts2phc YANG model (GPS/PPS → PHC → ptp4l GM path) - timemaster coordination (Phase 3, after phc2sys YANG is stable) - show ptp network (YANG action or background-polled topology container) - CMLDS (requires upstream linuxptp + 802.1ASdm foundation) - Full 12-bit sdoId, fault log, performance monitoring Backported patches from linuxptp master: - port: fix unicast negotiation recovery after FAULT_DETECTED - udp: fix port-specific ptp/p2p_dst_ipv4 configuration - pmc: avoid race conditions in agent update - phc2sys: wait until pmc agent is subscribed (startup race) - fix MAC driver incorrect SIOCGHWTSTAMP adjustment flags - pmc_agent: longer update interval when not subscribed - phc2sys: don't disable pmc agent with -s/-d/-w options - port_signaling: respect ptp_minor_version in message header - port: refresh link status on faults - uds: copy server socket ownership in pmc clients (non-root pmc) - uds: don't call chmod() on client socket - port: allow mixing wildcard and exact clock identities - Add pidfile support to ptp4l, phc2sys, and timemaster Signed-off-by: Joachim Wiberg --- .../rootfs/etc/finit.d/available/ptp4l@.conf | 3 + .../rootfs/usr/libexec/infix/init.d/00-probe | 54 + configs/aarch64_defconfig | 1 + configs/aarch64_minimal_defconfig | 1 + configs/arm_defconfig | 1 + configs/arm_minimal_defconfig | 1 + configs/riscv64_defconfig | 1 + configs/x86_64_defconfig | 1 + configs/x86_64_minimal_defconfig | 1 + doc/ptp.md | 461 ++ mkdocs.yml | 1 + ...-negotiation-doesn-t-recover-after-F.patch | 114 + ...cific-ptp-p2p_dst_ipv4-configuration.patch | 92 + ...void-race-conditions-in-agent-update.patch | 82 + ...s-Wait-until-pmc-agent-is-subscribed.patch | 45 + ...MAC-driver-is-configured-with-incorr.patch | 51 + ...nger-update-interval-when-not-subscr.patch | 50 + ...disable-pmc-agent-with-s-d-w-options.patch | 40 + ...-ensure-that-signaling-messages-resp.patch | 29 + ...9-port-Refresh-link-status-on-faults.patch | 82 + ...ship-of-server-socket-in-pmc-clients.patch | 58 + ...ds-Don-t-call-chmod-on-client-socket.patch | 56 + ...g-wildcard-identities-and-exact-iden.patch | 82 + ...port-to-ptp4l-phc2sys-and-timemaster.patch | 386 ++ src/bin/show/__init__.py | 18 + src/confd/src/Makefile.am | 1 + src/confd/src/core.c | 9 + src/confd/src/core.h | 3 + src/confd/src/ptp.c | 452 ++ src/confd/yang/confd.inc | 5 +- .../confd/ieee1588-ptp-tt@2023-08-14.yang | 4326 +++++++++++++++++ .../confd/ieee802-dot1as-gptp@2025-12-10.yang | 1301 +++++ src/confd/yang/confd/infix-if-ptp.yang | 102 + .../yang/confd/infix-if-ptp@2026-04-09.yang | 1 + src/confd/yang/confd/infix-interfaces.yang | 6 + ....yang => infix-interfaces@2026-04-09.yang} | 0 src/confd/yang/confd/infix-ptp.yang | 247 + .../yang/confd/infix-ptp@2026-04-07.yang | 1 + src/klish-plugin-infix/xml/infix.xml | 7 + src/statd/python/cli_pretty/cli_pretty.py | 202 + src/statd/python/yanger/__main__.py | 3 + src/statd/python/yanger/ieee1588_ptp.py | 624 +++ .../python/yanger/ietf_interfaces/link.py | 36 +- src/statd/statd.c | 5 +- 44 files changed, 9038 insertions(+), 4 deletions(-) create mode 100644 board/common/rootfs/etc/finit.d/available/ptp4l@.conf create mode 100644 doc/ptp.md create mode 100644 patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch create mode 100644 patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch create mode 100644 patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch create mode 100644 patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch create mode 100644 patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch create mode 100644 patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch create mode 100644 patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch create mode 100644 patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch create mode 100644 patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch create mode 100644 patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch create mode 100644 patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch create mode 100644 patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch create mode 100644 patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch create mode 100644 src/confd/src/ptp.c create mode 100644 src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang create mode 100644 src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang create mode 100644 src/confd/yang/confd/infix-if-ptp.yang create mode 120000 src/confd/yang/confd/infix-if-ptp@2026-04-09.yang rename src/confd/yang/confd/{infix-interfaces@2025-11-06.yang => infix-interfaces@2026-04-09.yang} (100%) create mode 100644 src/confd/yang/confd/infix-ptp.yang create mode 120000 src/confd/yang/confd/infix-ptp@2026-04-07.yang create mode 100644 src/statd/python/yanger/ieee1588_ptp.py diff --git a/board/common/rootfs/etc/finit.d/available/ptp4l@.conf b/board/common/rootfs/etc/finit.d/available/ptp4l@.conf new file mode 100644 index 000000000..caf4d7c2c --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/ptp4l@.conf @@ -0,0 +1,3 @@ +service name:ptp4l :%i log:prio:daemon,tag:ptp4l-%i \ + [2345] ptp4l -f /etc/linuxptp/ptp4l-%i.conf \ + -- PTP instance %i diff --git a/board/common/rootfs/usr/libexec/infix/init.d/00-probe b/board/common/rootfs/usr/libexec/infix/init.d/00-probe index aca2a85d9..e2054cefb 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/00-probe +++ b/board/common/rootfs/usr/libexec/infix/init.d/00-probe @@ -567,6 +567,59 @@ def probe_wifi_radios(out): out["wifi-radios"].append(info) +def probe_ptp_capabilities(out): + """Probe PTP timestamping capabilities per physical interface via ethtool --json -T. + + Only physical interfaces (those with a 'device' sysfs symlink) are probed; + virtual interfaces such as bridges, VLANs, and tun/tap devices are skipped. + Results are stored under out["interfaces"][]["ptp-capabilities"]. + """ + net_base = "/sys/class/net" + if not os.path.exists(net_base): + return + + ifaces = {} + for ifname in sorted(os.listdir(net_base)): + if ifname == "lo": + continue + # Physical interfaces have a 'device' symlink; virtual ones do not. + if not os.path.exists(os.path.join(net_base, ifname, "device")): + continue + + try: + result = subprocess.run( + ["ethtool", "--json", "-T", ifname], + capture_output=True, text=True, timeout=5 + ) + if result.returncode != 0: + continue + data = json.loads(result.stdout)[0] + except Exception: + continue + + caps = { + "capabilities": data.get("capabilities", []), + "tx-types": data.get("tx-types", []), + "rx-filters": data.get("rx-filters", []), + } + + # phc-index is -1 when no PHC is present; omit in that case. + phc = data.get("phc-index", -1) + if phc >= 0: + caps["phc-index"] = phc + + # hwtstamp provider fields are present only on newer kernels/hardware. + if (idx := data.get("hwtstamp-provider-index")) is not None: + caps["hwtstamp-provider-index"] = idx + if (qual := data.get("hwtstamp-provider-qualifier")) is not None: + caps["hwtstamp-provider-qualifier"] = qual + + ifaces[ifname] = {"ptp-capabilities": caps} + + if ifaces: + out.setdefault("interfaces", {}).update(ifaces) + + def main(): out = { "vendor": None, @@ -593,6 +646,7 @@ def main(): return err probe_wifi_radios(out) + probe_ptp_capabilities(out) if not out["factory-password-hash"]: sys.stdout.write("\n\n\033[31mCRITICAL BOOTSTRAP ERROR\n" + diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 74a498ed2..e50982d5f 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -75,6 +75,7 @@ BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPTABLES_NFTABLES=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index 8576151c2..bc197cad3 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -68,6 +68,7 @@ BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_NETCALC=y diff --git a/configs/arm_defconfig b/configs/arm_defconfig index 045ef04cf..1c108b8d4 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -74,6 +74,7 @@ BR2_PACKAGE_FRR=y BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index 279c1f5b6..7ef7fd2ca 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -70,6 +70,7 @@ BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_NETCALC=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index 88ac2a742..30d59e690 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -85,6 +85,7 @@ BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPTABLES_NFTABLES=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 771303313..e9f948dbd 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -74,6 +74,7 @@ BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPTABLES_NFTABLES=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 5eb98272d..c0c982c5f 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -67,6 +67,7 @@ BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_NETCALC=y diff --git a/doc/ptp.md b/doc/ptp.md new file mode 100644 index 000000000..5814ca8e0 --- /dev/null +++ b/doc/ptp.md @@ -0,0 +1,461 @@ +# PTP — Precision Time Protocol + +The Precision Time Protocol (PTP), defined in IEEE 1588-2019, synchronises +clocks across a network to sub-microsecond accuracy. Where NTP (Network Time +Protocol) aims at millisecond accuracy over wide-area networks, PTP is +designed for local-area networks and relies on hardware timestamping in the +network interface to eliminate software-induced jitter. + +PTP works by exchanging timestamped messages between devices. A *grandmaster +clock* — elected by the **Best TimeTransmitter Clock Algorithm (BTCA)** based +on priority, clock class, and accuracy — distributes time to the rest of the +network. Each synchronising device measures the one-way message delay to its +time-transmitter and continuously adjusts its local clock to compensate. + +> [!NOTE] +> The IEEE 1588g-2022 amendment to IEEE 1588-2019 introduced the terms +> *timeTransmitter* and *timeReceiver* as replacements for the former +> *master* and *slave* terminology, and *Best TimeTransmitter Clock +> Algorithm (BTCA)* in place of *BMCA*. This document uses the updated +> terms throughout. You may even see the short forms transmitter and +> receiver here and in online documentation. + +## Clock roles + +Every device in a PTP network takes one of the following roles: + +| Role | Description | +|----------------------------|---------------------------------------------------------------------------------------------| +| **Grandmaster (GM)** | Network-wide time source; elected by BTCA | +| **Time-transmitter** | Sends Sync messages downstream on a port | +| **Time-receiver** | Synchronises to a time-transmitter on a port | +| **Boundary Clock (BC)** | Terminates PTP on each port; acts as time-receiver upstream and time-transmitter downstream | +| **Transparent Clock (TC)** | Passes PTP messages while correcting the residence-time delay accumulated in the device | + +An **Ordinary Clock (OC)** has a single PTP port and is either a +time-transmitter (acting as a grandmaster candidate) or a time-receiver +(a leaf node synchronising to the network). + +## PTP profiles + +A **PTP profile** (as defined in IEEE 1588-2019 §3.1) is a document that +specifies a consistent set of required, permitted, and prohibited PTP +options for a particular application domain — much like a dialect of the +protocol. Examples from the standards world include profiles for power +utilities (IEC/IEEE C37.238), telecom (ITU-T G.8265.1), and +Time-Sensitive Networks. + +Each profile sets a unique value in the `majorSdoId` field of PTP message +headers — a 4-bit identifier that lets devices distinguish traffic belonging +to different profiles on the same link. Profile also determines the network +transport (UDP or Ethernet) and the delay measurement mechanism. + +Currently, two profiles are supported via the `profile` leaf in `default-ds`: + +| `profile` | Standard | majorSdoId | Transport | Delay | +|----------------------|-------------------|:----------:|-----------|----------------| +| `ieee1588` (default) | IEEE 1588-2019 | `0x0` | UDP/IPv4 | `e2e` or `p2p` | +| `ieee802-dot1as` | IEEE 802.1AS-2020 | `0x1` | L2 | `p2p` | + +The **gPTP** (generalized Precision Time Protocol) profile from IEEE 802.1AS-2020 +is used in **TSN** (Time-Sensitive Networking) and **AVB** (Audio/Video Bridging) +applications. Setting `profile ieee802-dot1as` applies all protocol-mandatory +settings automatically — Layer 2 transport, P2P delay measurement, 802.1AS +multicast addressing, path trace, follow-up information, and neighbour propagation +delay thresholds. The user still configures `priority1`, `priority2`, +`domain-number`, `time-receiver-only`, and timer interval leaves. + +The `ieee1588` profile leaves transport and delay mechanism user-configurable +per port. + +## Delay mechanisms + +PTP measures the link delay between neighbours using one of two mechanisms: + +- **End-to-End (E2E)**: Each time-receiver measures the delay to the + grandmaster by sending a `DELAY_REQ` message upstream. Simple to + configure; works with any network topology. +- **Peer-to-Peer (P2P)**: Each port measures its delay to its *immediate + neighbour* independently using `PDELAY_REQ` messages. Enables faster + path-delay updates and is required by the gPTP profile. + +## Data Sets + +IEEE 1588 organises protocol state into named **Data Sets (DS)** — each a +collection of related attributes for one aspect of a PTP instance. You +will encounter these directly in the CLI and in the `show ptp` output: + +| Data Set | CLI node | Contents | +|------------------|----------------|----------------------------------------------------------| +| Default DS | `default-ds` | Instance identity, clock class, priority, domain number | +| Current DS | `current-ds` | Live offset-from-GM, mean path delay, steps-removed | +| Parent DS | `parent-ds` | Grandmaster identity and quality attributes | +| Time Properties DS | `time-properties-ds` | UTC offset, leap-second flags, time source | +| Port DS | `port-ds` | Per-port state, delay mechanism, message intervals | + +## Domains + +A **PTP domain** (0–255) is a logical partition of the network. Devices +only synchronise with others in the same domain. Running multiple +instances on the same device — one per domain, or one per profile — is +fully supported; each instance is independent. + +Each PTP instance is identified on the network by its +`(domain-number, profile)` pair, which must be unique across all instances +on a device. + +> [!NOTE] +> The `show ptp` offset values reflect **PHC** (PTP Hardware Clock) +> synchronisation only. A PHC is the hardware clock exposed by the network +> interface; it tracks the PTP grandmaster but is independent of the Linux +> system clock, which currently is **not** automatically adjusted. + +## Ordinary Clock (time-receiver) + +A typical time-receiver Ordinary Clock, synchronising on interface +`eth0` using the default IEEE 1588 profile: + +

admin@example:/> configure
+admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds time-receiver-only true
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> leave
+
+ +## Ordinary Clock (time-transmitter / grandmaster) + +A grandmaster clock with high priority, domain 0: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds priority1 1
+admin@example:/config/ptp/instance/0/> set default-ds priority2 1
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> leave
+
+ +Lower `priority1` values win in the BTCA. A clock with `priority1 1` will +be preferred over the default `128` in any compliant network. + +## Boundary Clock + +A Boundary Clock terminates PTP on each port and re-originates it. Add one +port per interface: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds instance-type bc
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> end
+admin@example:/config/ptp/instance/0/> edit port 2
+admin@example:/config/ptp/…/0/port/2/> set underlying-interface eth1
+admin@example:/config/ptp/…/0/port/2/> leave
+
+ +> [!TIP] +> PTP port numbers are assigned sorted by `port-index`, so `port-index 1` +> becomes PTP port 1, `port-index 2` becomes PTP port 2, and so on. + +## Transparent Clock + +Transparent Clocks correct timestamps end-to-end without terminating PTP. +Use `instance-type p2p-tc` for a P2P TC (preferred in TSN networks) or +`instance-type e2e-tc` for an E2E TC: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds instance-type p2p-tc
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> end
+admin@example:/config/ptp/instance/0/> edit port 2
+admin@example:/config/ptp/…/0/port/2/> set underlying-interface eth1
+admin@example:/config/ptp/…/0/port/2/> leave
+
+ +> [!NOTE] +> For Transparent Clocks the delay mechanism is determined globally by the +> `instance-type` (`p2p-tc` → P2P, `e2e-tc` → E2E). Per-port +> `delay-mechanism` settings have no effect for TC instances. + +## gPTP / IEEE 802.1AS + +The gPTP profile is used in TSN and AVB applications. Setting +`profile ieee802-dot1as` applies all protocol-mandatory options from +IEEE 802.1AS-2020 automatically — Layer 2 transport, P2P delay +measurement, 802.1AS multicast addressing, and related protocol features. + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds profile ieee802-dot1as
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds time-receiver-only true
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> leave
+
+ +> [!NOTE] +> The `ieee802-dot1as` profile enforces Layer 2 transport and P2P delay +> measurement globally, as required by IEEE 802.1AS-2020. Per-port +> `delay-mechanism` settings have no effect for 802.1AS instances. + +## Multiple Instances + +Multiple PTP instances can run simultaneously, one per domain or profile +combination. Each instance must have a unique `(domain-number, profile)` +pair and an independent set of ports: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds profile ieee1588
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> end
+admin@example:/config/ptp/instance/0/> end
+admin@example:/config/ptp/> edit instance 1
+admin@example:/config/ptp/instance/1/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/1/> set default-ds profile ieee802-dot1as
+admin@example:/config/ptp/instance/1/> edit port 1
+admin@example:/config/ptp/…/1/port/1/> set underlying-interface eth1
+admin@example:/config/ptp/…/1/port/1/> leave
+
+ +## Port states + +Each PTP port progresses through a state machine. The current state is +shown in the `show ptp` port table: + +| State | Meaning | +|------------------------|----------------------------------------------------------------| +| `initializing` | Port is starting up, not yet ready to exchange messages | +| `faulty` | A fault condition has been detected on this port | +| `disabled` | Port is administratively disabled | +| `listening` | Awaiting `ANNOUNCE` messages; BTCA has not yet resolved | +| `pre-time-transmitter` | Transitioning towards time-transmitter state | +| `time-transmitter` | Port is acting as time-transmitter on this link | +| `passive` | Another port on this device is already time-transmitter | +| `uncalibrated` | Receiving sync; local clock not yet locked to time-transmitter | +| `time-receiver` | Port is locked and tracking its time-transmitter | + +A port in `uncalibrated` will typically transition to `time-receiver` +within a few seconds once the clock servo has converged. + +## Monitoring + +> [!TIP] Use the ++question++ key in the CLI +> The `show ptp` command has sub-commands — tap ++question++ after +> `show ptp` to see them, or use ++tab++ to complete. + +### Show all PTP instances + +
admin@example:/> show ptp
+PTP Instance 0                          Ordinary Clock · domain 0
+────────────────────────────────────────────────────────────────────
+  Clock identity          : AA-BB-CC-FF-FE-00-11-22
+  Grandmaster             : DD-EE-FF-FF-FE-33-44-55
+  Priority1/Priority2     : 128 / 128
+  GM Priority1/Priority2  : 1 / 1
+  Clock class             : cc-time-receiver-only
+  GM clock class          : cc-primary-sync
+  Time source             : gnss
+  PTP timescale           : yes
+  UTC offset              : 37 s
+  Time traceable          : yes
+  Freq. traceable         : yes
+  Offset from GM          : -42 ns
+  Mean path delay         : 1250 ns
+  Steps removed           : 1
+
+────────────────────────────────────────────────────────────────────
+Ports
+PORT  INTERFACE          STATE                DELAY  LINK DELAY (ns)
+   1  eth0               time-receiver        E2E                  0
+
+────────────────────────────────────────────────────────────────────
+Message Statistics  (▼ rx  ▲ tx)
+PORT  INTERFACE             SYNC ▼  SYNC ▲  ANN ▼  ANN ▲  PD ▼  PD ▲
+   1  eth0                      42       0     15      0     0     0
+
+
+ +Port state is colour-coded: green for `time-transmitter` and `time-receiver` +(actively synchronising), yellow for transient states (`listening`, +`uncalibrated`, `pre-time-transmitter`), and red for fault states (`faulty`, +`disabled`). The *Message Statistics* section is omitted when no counts are +available. + +### Show a specific instance + +
admin@example:/> show ptp 0
+
+ +## Tuning port intervals + +Adjust announcement, sync, and delay-request intervals per port. Values +are expressed as log₂ of the interval in seconds (e.g. `-3` = 125 ms, +`0` = 1 s, `1` = 2 s): + +
admin@example:/config/ptp/…/0/port/1/> set port-ds log-announce-interval 0
+admin@example:/config/ptp/…/0/port/1/> set port-ds log-sync-interval -3
+admin@example:/config/ptp/…/0/port/1/> set port-ds log-min-delay-req-interval 0
+admin@example:/config/ptp/…/0/port/1/> set announce-receipt-timeout 3
+
+ +`announce-receipt-timeout` is a count of announce intervals, not a duration +in seconds. With `log-announce-interval 0` (1 s) and +`announce-receipt-timeout 3`, a port waits 3 s without receiving an +`ANNOUNCE` before declaring the time-transmitter lost and returning to +`listening`. + +## Message exchange + +PTP distributes time using a small set of messages, all of which carry +hardware timestamps at the network interface: + +| Message | Timestamped | Purpose | +|-------------------------|:-----------:|-----------------------------------------------------| +| `ANNOUNCE` | No | Advertises clock quality for BTCA election | +| `SYNC` | Yes | Carries transmitter timestamp to receivers | +| `FOLLOW_UP` | No | Carries precise `t1` in two-step mode | +| `DELAY_REQ` | Yes | Receiver-initiated E2E delay measurement | +| `DELAY_RESP` | No | Time-transmitter reply to `DELAY_REQ` | +| `PDELAY_REQ` | Yes | Initiates P2P neighbour-delay measurement | +| `PDELAY_RESP` | Yes | Neighbour reply to `PDELAY_REQ` | +| `PDELAY_RESP_FOLLOW_UP` | No | Carries precise `PDELAY_RESP` `t3` in two-step mode | + +In **one-step** mode the timestamp is embedded directly into each `SYNC` +message as it leaves the wire, eliminating the need for `FOLLOW_UP`. +In **two-step** mode the `SYNC` carries a placeholder and the precise +transmit timestamp arrives in a subsequent `FOLLOW_UP`. Hardware +timestamping gives high accuracy in both modes; one-step reduces message +overhead at the cost of more demanding hardware support. + +## Message format + +Every PTP message begins with a common 34-octet header, regardless of type. +The structure below follows the traditional IETF bit-field layout: each row +is four octets wide, bit 7 (MSB) is on the left and bit 0 (LSB) on the +right within each octet. + +``` + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0-3 |trSpec |msgType| rsv | ver | messageLength | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4-7 | domainNumber | minorSdoId | flags | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8-15 | | + + correctionField + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +16-19 | messageTypeSpecific | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +20-27 | | + + clockIdentity + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +28-31 | portNumber | sequenceId | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +32-33 | controlField | logMsgIntvl | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +- **`trSpec`** (`transportSpecific`, bits 7–4 of octet 0): 4-bit profile + identifier. `0x0` = IEEE 1588, `0x1` = gPTP (802.1AS). Set implicitly + by the `profile` configuration leaf. +- **`msgType`** (`messageType`, bits 3–0 of octet 0): `0x0` SYNC · + `0x1` DELAY_REQ · `0x2` PDELAY_REQ · `0x3` PDELAY_RESP · + `0x8` FOLLOW_UP · `0x9` DELAY_RESP · `0xA` PDELAY_RESP_FOLLOW_UP · + `0xB` ANNOUNCE. +- **`rsv`** (reserved, bits 7–4 of octet 1): Set to zero; ignored on + receipt. +- **`ver`** (`versionPTP`, bits 3–0 of octet 1): PTP version; `2` for + IEEE 1588-2008 and IEEE 1588-2019. +- **`messageLength`** (octets 2–3): Total message length in octets, + including the header. +- **`domainNumber`** (octet 4): PTP domain; receivers silently discard + messages that do not match their configured domain. +- **`minorSdoId`** (octet 5): Reserved in IEEE 1588-2008; carries a + profile sub-identifier in IEEE 1588-2019. +- **`flags`** (octets 6–7): Per-message flags — includes the two-step + flag (set when a FOLLOW_UP will follow a SYNC), UTC offset valid, and + leap-second indicators. +- **`correctionField`** (octets 8–15): Accumulated path correction in + nanoseconds × 2¹⁶. Transparent Clocks add their measured residence + time and link delay here as they forward each message, so the final + time-receiver can subtract the total accumulated delay. +- **`messageTypeSpecific`** (octets 16–19): Reserved in IEEE 1588-2008; + carries message-type-specific data in IEEE 1588-2019. +- **`clockIdentity`** (octets 20–27): EUI-64 identity of the sending + clock — the value shown as "Clock identity" in `show ptp`. +- **`portNumber`** (octets 28–29): Port number of the sender within its + clock; together with `clockIdentity` it forms the unique + `sourcePortIdentity`. +- **`sequenceId`** (octets 30–31): Increments with each message; used to + match a DELAY_REQ to its DELAY_RESP. +- **`controlField`** (octet 32): Deprecated in PTPv2; set to fixed + values per message type for backward compatibility with PTPv1. +- **`logMsgIntvl`** (`logMessageInterval`, octet 33): Log₂ of the + expected interval between messages of this type; `0x7F` means not + applicable. + +The `transportSpecific` and `domainNumber` fields are the quickest way to +verify on the wire that a device is using the profile and domain you +configured. + +### Decoding with Wireshark + +Wireshark decodes PTP messages automatically, expanding every header field +and message-type-specific payload in the packet tree. PTP travels over +two UDP ports — 319 for event messages (SYNC, DELAY_REQ, PDELAY_REQ and +their responses) and 320 for general messages (ANNOUNCE, FOLLOW_UP) — as +well as directly over Ethernet (EtherType `0x88F7`) when layer-2 transport +is in use. + +Use the display filter `ptp` to isolate PTP traffic: + +``` +ptp +``` + +To narrow down to a specific domain or profile (exact field names can be +verified in Wireshark via **View → Internals → Supported Protocols**, +filtering for `ptp`): + +``` +ptp.v2.domainnumber == 0 +ptp.v2.transportspecific == 1 +``` + +This makes it straightforward to confirm which grandmaster a port is +tracking, verify that `correctionField` is being updated by a Transparent +Clock, or diagnose why the BTCA is not electing the expected grandmaster. + +## Glossary + +| Abbreviation | Expansion | Notes | +|--------------|--------------------------------------|-------------------------------------------------------------------| +| AVB | Audio/Video Bridging | IEEE 802.1 precursor to TSN; real-time AV over Ethernet | +| IETF | Internet Engineering Task Force | Standards body; defines RFC for layer-3 and up | +| UDP | User Datagram Protocol | IP transport used by PTP; port 319 (event) and 320 (general) | +| EUI-64 | Extended Unique Identifier (64-bit) | IEEE identifier format used as `clockIdentity` in PTP | +| EtherType | Ethernet frame type field | `0x88F7` identifies PTP over layer-2 Ethernet | +| BC | Boundary Clock | Terminates and re-originates PTP on each port | +| BTCA | Best TimeTransmitter Clock Algorithm | Elects the GM; replaces BMCA from IEEE 1588-2008 | +| CMLDS | Common Mean Link Delay Service | IEEE 1588-2019 §16.6; shared delay service for multiple instances | +| DS | Data Set | Named attribute collection in IEEE 1588 (default-ds, port-ds, …) | +| E2E | End-to-End | Delay mechanism: measures path from GM to time-receiver | +| GM | Grandmaster | PTP network-wide time source, elected by BTCA | +| gPTP | generalized Precision Time Protocol | IEEE 802.1AS profile; used in TSN and AVB | +| NTP | Network Time Protocol | Millisecond-accuracy time protocol for wide-area use | +| OC | Ordinary Clock | Single-port PTP clock; time-transmitter or time-receiver | +| P2P | Peer-to-Peer | Delay mechanism: measures delay to immediate neighbour | +| PHC | PTP Hardware Clock | Hardware clock in the NIC used for PTP timestamping | +| PTP | Precision Time Protocol | IEEE 1588 sub-microsecond clock synchronisation protocol | +| SDO | Standards Development Organization | Body that defines a PTP profile; encoded in `sdo-id` | +| TC | Transparent Clock | Forwards PTP messages, correcting for residence-time delay | +| TSN | Time-Sensitive Networking | IEEE 802.1 standard set for deterministic Ethernet | diff --git a/mkdocs.yml b/mkdocs.yml index bc640dc3d..9e92c10d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - Device Discovery: discovery.md - DHCP Server: dhcp.md - NTP Server: ntp.md + - PTP (IEEE 1588/802.1AS): ptp.md - System: - Boot Procedure: boot.md - Configuration: system.md diff --git a/patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch b/patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch new file mode 100644 index 000000000..08c5dae7e --- /dev/null +++ b/patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch @@ -0,0 +1,114 @@ +From d68e6a149340d987ebd19f86a7d792f4aaefdf98 Mon Sep 17 00:00:00 2001 +From: Vincent Cheng +Date: Tue, 17 Sep 2024 15:23:54 -0400 +Subject: [PATCH 01/13] port: Fix unicast negotiation doesn't recover after + FAULT_DETECTED +Organization: Wires + +_Problem_ +After a port link down/up or a tx_timestamp timeout issue, a port acting +as unicast master does not issue ANNC messages after granting unicast +request for ANNC. + +_Analysis_ +When a port FAULT occurs, the port transitions to FAULTY on FAULT_DETECTED +and subsequently port_disable(p) and port_initialize(p) are called on port recovery. + +A port acting as a unicast master, stores clients in p->unicast_service->queue. + +When a port receives a unicast request, unicast_service_add() is called. + +In unicast_service_add(), if the request does not match an entry in +p->unicast_service->queue, FD_UNICAST_SRV_TIMER is started via +unicast_service_rearm_timer(). + +If the unicast request matches an existing p->unicast_service->queue entry +the request is considered an extension and FD_UNICAST_SRV_TIMER must +already be running. + +port_disable() clears FD_UNICAST_SRV_TIMER, ie. stops FD_UNICAST_SRV_TIMER. +However, port_disable() does not clear p->unicast_service->queue. +When the port is restarted, the port retains the previous client data. + +After port recovery, when the client attempts to restart the unicast +service, the request matches an existing entry in p->unicast_service->queue, +and so FD_UNICAST_SRV_TIMER is not started because the port expected +that the FD_UNICAST_SRV_TIMER is already running. + +_Fix_ +This patch clears the unicast client data in port_disable() so +that upon recovery, the initial unicast request will be considered +a new request and trigger the start of the FD_UNICAST_SRV_TIMER. + +v2: +- Add missing sign-off +- Send to develop-request instead of users list + +Signed-off-by: Vincent Cheng +Signed-off-by: Joachim Wiberg +--- + port.c | 1 + + unicast_service.c | 21 +++++++++++++++++++++ + unicast_service.h | 6 ++++++ + 3 files changed, 28 insertions(+) + +diff --git a/port.c b/port.c +index db35a44..282be66 100644 +--- a/port.c ++++ b/port.c +@@ -1985,6 +1985,7 @@ void port_disable(struct port *p) + flush_peer_delay(p); + + p->best = NULL; ++ unicast_service_clear_clients(p); + free_foreign_masters(p); + transport_close(p->trp, &p->fda); + +diff --git a/unicast_service.c b/unicast_service.c +index 687468c..d7a4ecd 100644 +--- a/unicast_service.c ++++ b/unicast_service.c +@@ -571,3 +571,24 @@ int unicast_service_timer(struct port *p) + } + return err; + } ++ ++void unicast_service_clear_clients(struct port *p) ++{ ++ struct unicast_client_address *client, *temp; ++ struct unicast_service_interval *interval; ++ ++ if (!p->unicast_service) { ++ return; ++ } ++ ++ while ((interval = pqueue_extract(p->unicast_service->queue)) != NULL) { ++ ++ LIST_REMOVE(interval, list); ++ ++ LIST_FOREACH_SAFE(client, &interval->clients, list, temp) { ++ LIST_REMOVE(client, list); ++ free(client); ++ } ++ free(interval); ++ } ++} +\ No newline at end of file +diff --git a/unicast_service.h b/unicast_service.h +index f0d6487..8ea1a59 100644 +--- a/unicast_service.h ++++ b/unicast_service.h +@@ -87,4 +87,10 @@ void unicast_service_remove(struct port *p, struct ptp_message *m, + */ + int unicast_service_timer(struct port *p); + ++/** ++ * Clears unicast clients on a given port. ++ * @param p The port in question. ++ */ ++void unicast_service_clear_clients(struct port *p); ++ + #endif +-- +2.43.0 + diff --git a/patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch b/patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch new file mode 100644 index 000000000..bad0848c6 --- /dev/null +++ b/patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch @@ -0,0 +1,92 @@ +From 4ea423b94264e4eeb0c2706fc3485b1fe283cc11 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Wed, 25 Sep 2024 14:37:20 +0200 +Subject: [PATCH 02/13] udp: Fix port-specific ptp/p2p_dst_ipv4 configuration. +Organization: Wires + +If different ports are configured with a different ptp_dst_ipv4 or +p2p_dst_ipv4 address, only the last port in the configuration works +correctly. This is caused by a global variable holding the +destination address for all ports using the udp transport. + +Move the address to the udp structure to avoid the conflict between +different ports, same as when port-specific scope in udp6 was fixed +in commit a48666bee3dd ("udp6: Make mc6_addr transport-local"). + +Fixes: 8a26c94cc88e ("udp+udp6: Make IP addresses configurable.") +Signed-off-by: Miroslav Lichvar +Signed-off-by: Joachim Wiberg +--- + udp.c | 16 ++++++++-------- + 1 file changed, 8 insertions(+), 8 deletions(-) + +diff --git a/udp.c b/udp.c +index 38d0ec4..c9b5f39 100644 +--- a/udp.c ++++ b/udp.c +@@ -44,6 +44,7 @@ struct udp { + struct transport t; + struct address ip; + struct address mac; ++ struct in_addr mcast_addr[2]; + }; + + static int mcast_bind(int fd, int index) +@@ -146,8 +147,6 @@ no_socket: + + enum { MC_PRIMARY, MC_PDELAY }; + +-static struct in_addr mcast_addr[2]; +- + static int udp_open(struct transport *t, struct interface *iface, + struct fdarray *fda, enum timestamp_type ts_type) + { +@@ -165,22 +164,22 @@ static int udp_open(struct transport *t, struct interface *iface, + sk_interface_addr(name, AF_INET, &udp->ip); + + str = config_get_string(t->cfg, name, "ptp_dst_ipv4"); +- if (!inet_aton(str, &mcast_addr[MC_PRIMARY])) { ++ if (!inet_aton(str, &udp->mcast_addr[MC_PRIMARY])) { + pr_err("invalid ptp_dst_ipv4 %s", str); + return -1; + } + + str = config_get_string(t->cfg, name, "p2p_dst_ipv4"); +- if (!inet_aton(str, &mcast_addr[MC_PDELAY])) { ++ if (!inet_aton(str, &udp->mcast_addr[MC_PDELAY])) { + pr_err("invalid p2p_dst_ipv4 %s", str); + return -1; + } + +- efd = open_socket(name, mcast_addr, EVENT_PORT, ttl); ++ efd = open_socket(name, udp->mcast_addr, EVENT_PORT, ttl); + if (efd < 0) + goto no_event; + +- gfd = open_socket(name, mcast_addr, GENERAL_PORT, ttl); ++ gfd = open_socket(name, udp->mcast_addr, GENERAL_PORT, ttl); + if (gfd < 0) + goto no_general; + +@@ -223,6 +222,7 @@ static int udp_send(struct transport *t, struct fdarray *fda, + enum transport_event event, int peer, void *buf, int len, + struct address *addr, struct hw_timestamp *hwts) + { ++ struct udp *udp = container_of(t, struct udp, t); + struct address addr_buf; + unsigned char junk[1600]; + ssize_t cnt; +@@ -243,8 +243,8 @@ static int udp_send(struct transport *t, struct fdarray *fda, + if (!addr) { + memset(&addr_buf, 0, sizeof(addr_buf)); + addr_buf.sin.sin_family = AF_INET; +- addr_buf.sin.sin_addr = peer ? mcast_addr[MC_PDELAY] : +- mcast_addr[MC_PRIMARY]; ++ addr_buf.sin.sin_addr = peer ? udp->mcast_addr[MC_PDELAY] : ++ udp->mcast_addr[MC_PRIMARY]; + addr_buf.len = sizeof(addr_buf.sin); + addr = &addr_buf; + } +-- +2.43.0 + diff --git a/patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch b/patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch new file mode 100644 index 000000000..2245812ec --- /dev/null +++ b/patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch @@ -0,0 +1,82 @@ +From e76bb37019605dea4acd9ccec620a3faec1ba402 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Thu, 17 Oct 2024 15:05:21 +0200 +Subject: [PATCH 03/13] pmc: Avoid race conditions in agent update. +Organization: Wires + +The pmc_agent_update() function updates the subscription to +notifications and also the current UTC offset. It uses a timeout of 0 +to avoid blocking. When the pmc client sends the first request, the +response from ptp4l may not come quickly enough to be received in the +same run_pmc() call. It then sends the other request and checks for the +response. If it is the response to the first request, it will be ignored. +The update works correctly only if both responses are quick enough to be +received in the same call, or are both slow enough that they are +received in the next call of the pmc_agent_update() function. + +The function needs to be called a random number of times in order to +finish one update. If the mismatch between requests and responses +happened consistently, the agent would never reach the up-to-date state +and phc2sys would not enter the main synchronization loop. + +Split the update into two phases, where only one thing is updated at a +time. The function now needs to be called at most 3 times to update both +the subscription and UTC offset, assuming it is not interrupted by +another request outside of the agent's update. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + pmc_agent.c | 24 ++++++++++++++++++------ + 1 file changed, 18 insertions(+), 6 deletions(-) + +diff --git a/pmc_agent.c b/pmc_agent.c +index 86b6ee6..d1a3367 100644 +--- a/pmc_agent.c ++++ b/pmc_agent.c +@@ -37,6 +37,7 @@ struct pmc_agent { + struct pmc *pmc; + uint64_t pmc_last_update; + uint64_t update_interval; ++ int update_phase; + + struct defaultDS dds; + bool dds_valid; +@@ -427,16 +428,27 @@ int pmc_agent_update(struct pmc_agent *node) + ts = tp.tv_sec * NS_PER_SEC + tp.tv_nsec; + + if (ts - node->pmc_last_update >= node->update_interval) { +- if (node->stay_subscribed) { +- renew_subscription(node, 0); +- } +- if (!pmc_agent_query_utc_offset(node, 0)) { ++ switch (node->update_phase) { ++ case 0: ++ if (node->stay_subscribed && ++ renew_subscription(node, 0)) ++ break; ++ node->update_phase++; ++ /* Fall through */ ++ case 1: ++ if (pmc_agent_query_utc_offset(node, 0)) ++ break; ++ node->update_phase++; ++ /* Fall through */ ++ default: + node->pmc_last_update = ts; ++ node->update_phase = 0; ++ break; + } ++ } else { ++ run_pmc(node, 0, -1, &msg); + } + +- run_pmc(node, 0, -1, &msg); +- + return 0; + } + +-- +2.43.0 + diff --git a/patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch b/patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch new file mode 100644 index 000000000..0a25b03d1 --- /dev/null +++ b/patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch @@ -0,0 +1,45 @@ +From 1942fde263f40d318d56acf824626641263facd0 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Thu, 17 Oct 2024 15:05:22 +0200 +Subject: [PATCH 04/13] phc2sys: Wait until pmc agent is subscribed. +Organization: Wires + +When phc2sys is configured with multiple domains, different domains may +have their pmc agent subscribed after different number of calls of the +pmc_agent_update() function depending on how quickly responses from +ptp4l are received. If one domain triggers reconfiguration and the other +domain does not have its agent subscribed yet, it will not have any of +its clocks synchronized until a port changes state and triggers another +reconfiguration of the domain. + +To avoid this problem, wait for each domain to have its agent subscribed +before entering the main synchronization loop. Use a 10ms update +interval to speed up the start of phc2sys. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + phc2sys.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/phc2sys.c b/phc2sys.c +index 6113539..47e896e 100644 +--- a/phc2sys.c ++++ b/phc2sys.c +@@ -962,6 +962,12 @@ static int auto_init_ports(struct domain *domain) + return -1; + } + ++ while (!pmc_agent_is_subscribed(domain->agent)) { ++ usleep(10000); ++ if (pmc_agent_update(domain->agent) < 0) ++ return -1; ++ } ++ + for (i = 1; i <= number_ports; i++) { + err = pmc_agent_query_port_properties(domain->agent, 1000, i, + &state, ×tamping, +-- +2.43.0 + diff --git a/patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch b/patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch new file mode 100644 index 000000000..f4c71b442 --- /dev/null +++ b/patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch @@ -0,0 +1,51 @@ +From 9e1b0df61c4dc9bb1a4aac11acb06e4f1f2c68f7 Mon Sep 17 00:00:00 2001 +From: William Comly +Date: Thu, 31 Oct 2024 15:10:20 -0400 +Subject: [PATCH 05/13] Fix issue where MAC driver is configured with incorrect + adjustment flags sometimes returned by SIOCGHWTSTAMP test. +Organization: Wires + +Once the check for the VLAN bonding flag is complete, clear the ifreq +message to ensure only the intended configuration and flags are set +in the driver. + +Signed-off-by: William Comly +Signed-off-by: Joachim Wiberg +--- + sk.c | 20 +++++++++----------- + 1 file changed, 9 insertions(+), 11 deletions(-) + +diff --git a/sk.c b/sk.c +index aadb237..4860af2 100644 +--- a/sk.c ++++ b/sk.c +@@ -69,17 +69,15 @@ static int hwts_init(int fd, const char *device, int rx_filter, + /* Test if VLAN over bond is supported. */ + cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX; + err = ioctl(fd, SIOCGHWTSTAMP, &ifreq); +- if (err < 0) { +- /* +- * Fall back without flag if user runs new build on old kernel +- * or if driver does not support SIOCGHWTSTAMP ioctl. +- */ +- if (errno == EINVAL || errno == EOPNOTSUPP) { +- init_ifreq(&ifreq, &cfg, device); +- } else { +- pr_err("ioctl SIOCGHWTSTAMP failed: %m"); +- return err; +- } ++ if (err < 0 && errno != EINVAL && errno != EOPNOTSUPP) { ++ pr_err("ioctl SIOCGHWTSTAMP failed: %m"); ++ return err; ++ } ++ ++ init_ifreq(&ifreq, &cfg, device); ++ /* If VLAN over bond supported in kernel, configure flag in driver. */ ++ if (err == 0) { ++ cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX; + } + + switch (sk_hwts_filter_mode) { +-- +2.43.0 + diff --git a/patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch b/patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch new file mode 100644 index 000000000..20d4cac36 --- /dev/null +++ b/patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch @@ -0,0 +1,50 @@ +From 0bb9080c1bf631ae0265475d75860b6acd92ed2c Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Tue, 26 Nov 2024 15:10:32 +0100 +Subject: [PATCH 06/13] pmc_agent: Use longer update interval when not + subscribed. +Organization: Wires + +When phc2sys is started with the -w option, the pmc agent is not +subscribed to events by the pmc_agent_subscribe() function, which also +sets the update interval. The update interval in this case is zero, +which means the pmc agent is trying to update the currentUtcOffset value +on every call of pmc_agent_update(), i.e. on every clock update in +phc2sys. + +Set a default update interval of 60 seconds to reduce the rate of +pmc requests. + +Fixes: e3ca7ea90a9e ("pmc_agent: Make update interval configurable.") +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + pmc_agent.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/pmc_agent.c b/pmc_agent.c +index d1a3367..663adc0 100644 +--- a/pmc_agent.c ++++ b/pmc_agent.c +@@ -33,6 +33,9 @@ + #define UPDATES_PER_SUBSCRIPTION 3 + #define MIN_UPDATE_INTERVAL 10 + ++/* Update interval if the agent not subscribed, just polling the UTC offset */ ++#define DEFAULT_UPDATE_INTERVAL 60 ++ + struct pmc_agent { + struct pmc *pmc; + uint64_t pmc_last_update; +@@ -253,6 +256,7 @@ int init_pmc_node(struct config *cfg, struct pmc_agent *node, const char *uds, + } + node->recv_subscribed = recv_subscribed; + node->recv_context = context; ++ node->update_interval = DEFAULT_UPDATE_INTERVAL * NS_PER_SEC; + + return 0; + } +-- +2.43.0 + diff --git a/patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch b/patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch new file mode 100644 index 000000000..a759b8cde --- /dev/null +++ b/patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch @@ -0,0 +1,40 @@ +From 5a97466a0ed88bae244566c2a5dba85ce72e4f01 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Tue, 26 Nov 2024 15:10:33 +0100 +Subject: [PATCH 07/13] phc2sys: Don't disable pmc agent with -s -d -w options. +Organization: Wires + +When phc2sys is started with -s and -d options to combine a PPS device +and PHC device as a time source, but without an offset specified by +the -O option, the pmc agent is disabled after waiting for ptp4l to have +a port in a synchronized state. This prevents phc2sys from following +changes in the currentUtcOffset value. + +Disable the pmc agent only if no PHC device is specified by the -s +option, i.e. there are no PHC readings to which the UTC offset could be +applied. + +Fixes: 5f1b419c4102 ("phc2sys: Replace magical test with a proper test.") +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + phc2sys.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/phc2sys.c b/phc2sys.c +index 47e896e..5962f6c 100644 +--- a/phc2sys.c ++++ b/phc2sys.c +@@ -1547,7 +1547,7 @@ int main(int argc, char *argv[]) + + if (domains[0].forced_sync_offset || + !phc2sys_using_systemclock(&domains[0]) || +- hardpps_configured(pps_fd)) { ++ (hardpps_configured(pps_fd) && !src_name)) { + pmc_agent_disable(domains[0].agent); + } + } +-- +2.43.0 + diff --git a/patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch b/patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch new file mode 100644 index 000000000..1f526fc8c --- /dev/null +++ b/patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch @@ -0,0 +1,29 @@ +From 9822b22a07e05eb087cedfbb4a21ceecbb8ec74e Mon Sep 17 00:00:00 2001 +From: William Comly +Date: Wed, 19 Feb 2025 10:12:28 -0500 +Subject: [PATCH 08/13] port_signaling.c: ensure that signaling messages + respect ptp_minor_version in message header +Organization: Wires + +Signed-off-by: William Comly +Signed-off-by: Joachim Wiberg +--- + port_signaling.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/port_signaling.c b/port_signaling.c +index ca4202f..cf28756 100644 +--- a/port_signaling.c ++++ b/port_signaling.c +@@ -41,7 +41,7 @@ struct ptp_message *port_signaling_construct(struct port *p, + } + msg->hwts.type = p->timestamping; + msg->header.tsmt = SIGNALING | p->transportSpecific; +- msg->header.ver = PTP_VERSION; ++ msg->header.ver = ptp_hdr_ver; + msg->header.messageLength = sizeof(struct signaling_msg); + msg->header.domainNumber = clock_domain_number(p->clock); + msg->header.sourcePortIdentity = p->portIdentity; +-- +2.43.0 + diff --git a/patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch b/patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch new file mode 100644 index 000000000..5e0ac7e3e --- /dev/null +++ b/patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch @@ -0,0 +1,82 @@ +From b8a3020e806dd6252e7edd434ae3940706a0b516 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Tue, 4 Mar 2025 15:53:37 +0100 +Subject: [PATCH 09/13] port: Refresh link status on faults. +Organization: Wires + +ptp4l gets the ENOBUFS error on the netlink socket when the kernel has +to drop messages due to full socket buffer. If ptp4l has a port in the +faulty state waiting for the link to go up and that event corresponds +to one of the dropped netlink messages, the port will be stuck in the +faulty state until the link goes down and up again. + +To prevent the port from getting stuck, request the current link status +when dispatching the EV_FAULT_DETECTED event. Also, reopen the socket to +get rid of the buffered messages when handling the fault and again when +reinitializing the port. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + port.c | 30 +++++++++++++++++++++++------- + 1 file changed, 23 insertions(+), 7 deletions(-) + +diff --git a/port.c b/port.c +index 282be66..5eea2f2 100644 +--- a/port.c ++++ b/port.c +@@ -1975,6 +1975,20 @@ static int port_cmlds_initialize(struct port *p) + return port_cmlds_renew(p, now.tv_sec); + } + ++static void port_rtnl_initialize(struct port *p) ++{ ++ /* Reopen the socket to get rid of buffered messages */ ++ if (p->fda.fd[FD_RTNL] >= 0) { ++ rtnl_close(p->fda.fd[FD_RTNL]); ++ } ++ p->fda.fd[FD_RTNL] = rtnl_open(); ++ if (p->fda.fd[FD_RTNL] >= 0) { ++ rtnl_link_query(p->fda.fd[FD_RTNL], interface_name(p->iface)); ++ } ++ ++ clock_fda_changed(p->clock); ++} ++ + void port_disable(struct port *p) + { + int i; +@@ -2088,13 +2102,8 @@ int port_initialize(struct port *p) + if (p->bmca == BMCA_NOOP) { + port_set_delay_tmo(p); + } +- if (p->fda.fd[FD_RTNL] == -1) { +- p->fda.fd[FD_RTNL] = rtnl_open(); +- } +- if (p->fda.fd[FD_RTNL] >= 0) { +- const char *ifname = interface_name(p->iface); +- rtnl_link_query(p->fda.fd[FD_RTNL], ifname); +- } ++ ++ port_rtnl_initialize(p); + } + + port_nrate_initialize(p); +@@ -3769,6 +3778,13 @@ int port_state_update(struct port *p, enum fsm_event event, int mdiff) + if (port_link_status_get(p) && clear_fault_asap(&i)) { + pr_notice("%s: clearing fault immediately", p->log_name); + next = p->state_machine(next, EV_FAULT_CLEARED, 0); ++ } else if (event == EV_FAULT_DETECTED) { ++ /* ++ * Reopen the netlink socket and refresh the link ++ * status in case the fault was triggered by a missed ++ * netlink message (ENOBUFS). ++ */ ++ port_rtnl_initialize(p); + } + } + +-- +2.43.0 + diff --git a/patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch b/patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch new file mode 100644 index 000000000..a0dcb5a58 --- /dev/null +++ b/patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch @@ -0,0 +1,58 @@ +From 3dece8ac8b324e702bf54875b53aee1eb49340e0 Mon Sep 17 00:00:00 2001 +From: "Miroslav Lichvar (via linuxptp-devel Mailing List)" + +Date: Thu, 31 Jul 2025 11:35:46 +0200 +Subject: [PATCH 10/13] uds: Copy ownership of server socket in pmc clients. +Organization: Wires + +ptp4l sending a response to a pmc client needs to have permissions to +write to the client's UNIX domain socket. If ptp4l runs under a non-root +user, it cannot write to sockets bound by the pmc client if it did that +as root. + +After binding the client socket, change its owner to the owner of the +server socket, so it can send the client a response. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + uds.c | 15 +++++++++++++++ + 1 file changed, 15 insertions(+) + +diff --git a/uds.c b/uds.c +index 4ddee7b..ce7e92d 100644 +--- a/uds.c ++++ b/uds.c +@@ -60,6 +60,7 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray + const char* file_mode_cfg; + struct sockaddr_un sa; + mode_t file_mode; ++ struct stat st; + int fd, err; + + fd = socket(AF_LOCAL, SOCK_DGRAM, 0); +@@ -97,6 +98,20 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray + uds->address.len = sizeof(sa); + + chmod(name, file_mode); ++ ++ /* ++ * In the client, copy the ownership of the server's socket if it runs ++ * under a non-root user to allow it to send a response to the client ++ * running under root. Avoid following a symlink if the socket is ++ * replaced (e.g. by compromised ptp4l process). ++ */ ++ if (uds_path[0] != '\0') { ++ if (!lstat(uds_path, &st) && (st.st_uid || st.st_gid) && ++ lchown(name, st.st_uid, st.st_gid)) { ++ pr_err("uds: failed to change socket ownership: %m"); ++ } ++ } ++ + fda->fd[FD_EVENT] = -1; + fda->fd[FD_GENERAL] = fd; + return 0; +-- +2.43.0 + diff --git a/patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch b/patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch new file mode 100644 index 000000000..8ec2ab92f --- /dev/null +++ b/patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch @@ -0,0 +1,56 @@ +From 6bfc1de21b64ac17145b119cb636d1c1b3d90a75 Mon Sep 17 00:00:00 2001 +From: "Miroslav Lichvar (via linuxptp-devel Mailing List)" + +Date: Thu, 31 Jul 2025 11:35:47 +0200 +Subject: [PATCH 11/13] uds: Don't call chmod() on client socket. +Organization: Wires + +The pmc clients should not need to modify the permissions of their +socket (following the uds_file_mode setting), they can rely on their +umask. + +Make the chmod() call on the bound socket only in the server. + +This removes a race condition between the bind() and chmod() calls that +could potentially be exploited by ptp4l running under a non-root user. +It could replace the socket with a symlink in order to make the client +running under root to change the mode of a different file. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + uds.c | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/uds.c b/uds.c +index ce7e92d..d74b7a8 100644 +--- a/uds.c ++++ b/uds.c +@@ -97,19 +97,20 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray + uds->address.sun = sa; + uds->address.len = sizeof(sa); + +- chmod(name, file_mode); +- + /* + * In the client, copy the ownership of the server's socket if it runs + * under a non-root user to allow it to send a response to the client + * running under root. Avoid following a symlink if the socket is +- * replaced (e.g. by compromised ptp4l process). ++ * replaced (e.g. by compromised ptp4l process). The server just sets ++ * the permissions on its socket per configuration. + */ + if (uds_path[0] != '\0') { + if (!lstat(uds_path, &st) && (st.st_uid || st.st_gid) && + lchown(name, st.st_uid, st.st_gid)) { + pr_err("uds: failed to change socket ownership: %m"); + } ++ } else { ++ chmod(name, file_mode); + } + + fda->fd[FD_EVENT] = -1; +-- +2.43.0 + diff --git a/patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch b/patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch new file mode 100644 index 000000000..beb608ae9 --- /dev/null +++ b/patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch @@ -0,0 +1,82 @@ +From 5a192e152f4d19ff5899960154f1e14bbd0c6bd7 Mon Sep 17 00:00:00 2001 +From: "Maxime Chevallier (via linuxptp-devel Mailing List)" + +Date: Wed, 4 Feb 2026 10:16:11 +0100 +Subject: [PATCH 12/13] port: Allow mixing wildcard identities and exact + identities in messages +Organization: Wires + +A Port Identity is made of a Clock Identity and a Port Number. Each of +these fields allow wildcards values. + +The current implementation checks if either both these fields contain an +exact value, or if both contains a wildcard. + +Make so that we check for wildcards on each field independently. To +avoid hard to read comparisons, introduce helper functions to compare +each field in PortIdentity. + +Signed-off-by: Maxime Chevallier +Signed-off-by: Joachim Wiberg +--- + port_signaling.c | 6 ++++-- + util.h | 26 ++++++++++++++++++++++++++ + 2 files changed, 30 insertions(+), 2 deletions(-) + +diff --git a/port_signaling.c b/port_signaling.c +index cf28756..b34ebe9 100644 +--- a/port_signaling.c ++++ b/port_signaling.c +@@ -151,8 +151,10 @@ int process_signaling(struct port *p, struct ptp_message *m) + } + + /* Ignore signaling messages not addressed to this port. */ +- if (!pid_eq(&m->signaling.targetPortIdentity, &p->portIdentity) && +- !pid_eq(&m->signaling.targetPortIdentity, &wildcard_pid)) { ++ if ((!pid_cid_eq(&m->signaling.targetPortIdentity, &p->portIdentity) && ++ !pid_cid_eq(&m->signaling.targetPortIdentity, &wildcard_pid)) || ++ (!pid_pn_eq(&m->signaling.targetPortIdentity, &p->portIdentity) && ++ !pid_pn_eq(&m->signaling.targetPortIdentity, &wildcard_pid))) { + return 0; + } + +diff --git a/util.h b/util.h +index b228745..7552353 100644 +--- a/util.h ++++ b/util.h +@@ -158,6 +158,32 @@ static inline int pid_eq(const struct PortIdentity *a, + return memcmp(a, b, sizeof(*a)) == 0; + } + ++/** ++ * Compare two port identities for PortIdentity.clockIdentity equality. ++ * ++ * @param a First port identity. ++ * @param b Second port identity. ++ * @return 1 if identities are equal, 0 otherwise. ++ */ ++static inline int pid_cid_eq(const struct PortIdentity *a, ++ const struct PortIdentity *b) ++{ ++ return cid_eq(&a->clockIdentity, &b->clockIdentity); ++} ++ ++/** ++ * Compare two port identities for PortIdentity.portNumber equality. ++ * ++ * @param a First port identity. ++ * @param b Second port identity. ++ * @return 1 if identities are equal, 0 otherwise. ++ */ ++static inline int pid_pn_eq(const struct PortIdentity *a, ++ const struct PortIdentity *b) ++{ ++ return a->portNumber == b->portNumber; ++} ++ + /** + * Convert a string containing a network address into binary form. + * @param type The network transport type of the address. +-- +2.43.0 + diff --git a/patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch b/patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch new file mode 100644 index 000000000..e78e284a0 --- /dev/null +++ b/patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch @@ -0,0 +1,386 @@ +From 4fdb58ce4e052862f0dc7432d84b19febd5256a5 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Fri, 10 Apr 2026 14:33:56 +0200 +Subject: [PATCH 13/13] Add pidfile support to ptp4l, phc2sys, and timemaster +Organization: Wires + +Add pidfile.c derived from OpenBSD via libite. The pidfile() function +creates a PID file and registers an atexit() handler to clean it up on +normal exit. + +For ptp4l and phc2sys the pidfile path is set via the 'pidfile' global +config option (or -u on the ptp4l command line). For timemaster, +which uses its own config format, the path is given with -u . + +Signed-off-by: Joachim Wiberg +--- + config.c | 1 + + makefile | 12 ++--- + phc2sys.c | 7 +++ + pidfile.c | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ + pidfile.h | 10 ++++ + ptp4l.c | 14 ++++- + timemaster.c | 19 +++++-- + 7 files changed, 194 insertions(+), 11 deletions(-) + create mode 100644 pidfile.c + create mode 100644 pidfile.h + +diff --git a/config.c b/config.c +index d0bc32c..4b46542 100644 +--- a/config.c ++++ b/config.c +@@ -341,6 +341,7 @@ struct config_item config_tab[] = { + PORT_ITEM_INT("power_profile.2011.networkTimeInaccuracy", 0xFFFFFFFF, -1, INT_MAX), + PORT_ITEM_INT("power_profile.2017.totalTimeInaccuracy", 0xFFFFFFFF, -1, INT_MAX), + PORT_ITEM_INT("power_profile.grandmasterID", 0, 0, 0xFFFF), ++ GLOB_ITEM_STR("pidfile", NULL), + GLOB_ITEM_INT("priority1", 128, 0, UINT8_MAX), + GLOB_ITEM_INT("priority2", 128, 0, UINT8_MAX), + GLOB_ITEM_STR("productDescription", ";;"), +diff --git a/makefile b/makefile +index 3c2406b..67622e1 100644 +--- a/makefile ++++ b/makefile +@@ -31,9 +31,9 @@ TS2PHC = ts2phc.o lstab.o nmea.o serial.o sock.o ts2phc_generic_pps_source.o \ + ts2phc_nmea_pps_source.o ts2phc_phc_pps_source.o ts2phc_pps_sink.o ts2phc_pps_source.o + OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o designated_fsm.o \ + e2e_tc.o fault.o $(FILTERS) fsm.o hash.o interface.o monitor.o msg.o phc.o \ +- pmc_common.o port.o port_signaling.o pqueue.o print.o ptp4l.o p2p_tc.o rtnl.o \ +- $(SECURITY) $(SERVOS) sk.o stats.o tc.o $(TRANSP) telecom.o tlv.o tsproc.o \ +- unicast_client.o unicast_fsm.o unicast_service.o util.o version.o ++ pidfile.o pmc_common.o port.o port_signaling.o pqueue.o print.o ptp4l.o \ ++ p2p_tc.o rtnl.o $(SECURITY) $(SERVOS) sk.o stats.o tc.o $(TRANSP) telecom.o \ ++ tlv.o tsproc.o unicast_client.o unicast_fsm.o unicast_service.o util.o version.o + + OBJECTS = $(OBJ) hwstamp_ctl.o nsm.o phc2sys.o phc_ctl.o pmc.o pmc_agent.o \ + pmc_common.o sysoff.o timemaster.o $(TS2PHC) tz2alt.o +@@ -78,14 +78,14 @@ pmc: config.o hash.o interface.o msg.o phc.o pmc.o pmc_common.o print.o \ + $(SECURITY) sk.o tlv.o $(TRANSP) util.o version.o + + phc2sys: clockadj.o clockcheck.o config.o hash.o interface.o msg.o \ +- phc.o phc2sys.o pmc_agent.o pmc_common.o print.o $(SECURITY) $(SERVOS) \ +- sk.o stats.o sysoff.o tlv.o $(TRANSP) util.o version.o ++ phc.o phc2sys.o pidfile.o pmc_agent.o pmc_common.o print.o $(SECURITY) \ ++ $(SERVOS) sk.o stats.o sysoff.o tlv.o $(TRANSP) util.o version.o + + hwstamp_ctl: hwstamp_ctl.o version.o + + phc_ctl: phc_ctl.o phc.o sk.o util.o clockadj.o sysoff.o print.o version.o + +-timemaster: phc.o print.o rtnl.o sk.o timemaster.o util.o version.o ++timemaster: phc.o pidfile.o print.o rtnl.o sk.o timemaster.o util.o version.o + + ts2phc: config.o clockadj.o hash.o interface.o msg.o phc.o pmc_agent.o \ + pmc_common.o print.o $(SECURITY) $(SERVOS) sk.o $(TS2PHC) tlv.o transport.o \ +diff --git a/phc2sys.c b/phc2sys.c +index 5962f6c..b272adf 100644 +--- a/phc2sys.c ++++ b/phc2sys.c +@@ -56,6 +56,7 @@ + #include "sysoff.h" + #include "tlv.h" + #include "uds.h" ++#include "pidfile.h" + #include "util.h" + #include "version.h" + +@@ -1432,6 +1433,12 @@ int main(int argc, char *argv[]) + print_set_syslog(config_get_int(cfg, NULL, "use_syslog")); + print_set_level(config_get_int(cfg, NULL, "logging_level")); + ++ if (config_get_string(cfg, NULL, "pidfile") && ++ pidfile(config_get_string(cfg, NULL, "pidfile"))) { ++ fprintf(stderr, "failed to create pidfile\n"); ++ goto end; ++ } ++ + settings.free_running = config_get_int(cfg, NULL, "free_running"); + settings.servo_type = config_get_int(cfg, NULL, "clock_servo"); + if (settings.free_running || settings.servo_type == CLOCK_SERVO_NTPSHM) { +diff --git a/pidfile.c b/pidfile.c +new file mode 100644 +index 0000000..42121b0 +--- /dev/null ++++ b/pidfile.c +@@ -0,0 +1,142 @@ ++/* Updated by troglobit for libite/finit/uftpd projects 2016/07/04 */ ++/* $OpenBSD: pidfile.c,v 1.11 2015/06/03 02:24:36 millert Exp $ */ ++/* $NetBSD: pidfile.c,v 1.4 2001/02/19 22:43:42 cgd Exp $ */ ++ ++/*- ++ * Copyright (c) 1999 The NetBSD Foundation, Inc. ++ * All rights reserved. ++ * ++ * This code is derived from software contributed to The NetBSD Foundation ++ * by Jason R. Thorpe. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS ++ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED ++ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE ++ * POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "pidfile.h" ++ ++static char *pidfile_path = NULL; ++static pid_t pidfile_pid = 0; ++ ++static void pidfile_cleanup(void); ++ ++static const char *pidfile_rundir = _PATH_VARRUN; ++extern char *__progname; ++ ++/** ++ * Create or update mtime of process PID file. ++ * @param basename Program name, or NULL, may start with '/' ++ * ++ * If @p basename is NULL the implicit @a __progname variable from the ++ * C-library is used. If @p basename starts with '/' it is used as the ++ * absolute path to the PID file. ++ * ++ * @returns POSIX OK(0) on success, non-zero with errno set on error. ++ */ ++int pidfile(const char *basename) ++{ ++ int save_errno; ++ int atexit_already; ++ pid_t pid; ++ FILE *f; ++ ++ if (basename == NULL) ++ basename = __progname; ++ ++ pid = getpid(); ++ atexit_already = 0; ++ ++ if (pidfile_path != NULL) { ++ if (!access(pidfile_path, R_OK) && pid == pidfile_pid) { ++ utimensat(0, pidfile_path, NULL, 0); ++ return 0; ++ } ++ free(pidfile_path); ++ pidfile_path = NULL; ++ atexit_already = 1; ++ } ++ ++ if (basename[0] != '/') { ++ size_t len = strlen(pidfile_rundir); ++ int slash = pidfile_rundir[len > 0 ? len - 1 : 0] != '/'; ++ ++ if (asprintf(&pidfile_path, "%s%s%s.pid", ++ pidfile_rundir, slash ? "/" : "", basename) == -1) ++ return -1; ++ } else { ++ if (asprintf(&pidfile_path, "%s", basename) == -1) ++ return -1; ++ } ++ ++ if ((f = fopen(pidfile_path, "w")) == NULL) { ++ save_errno = errno; ++ free(pidfile_path); ++ pidfile_path = NULL; ++ errno = save_errno; ++ return -1; ++ } ++ ++ if (fprintf(f, "%ld\n", (long)pid) <= 0 || fflush(f) != 0) { ++ save_errno = errno; ++ (void)fclose(f); ++ (void)unlink(pidfile_path); ++ free(pidfile_path); ++ pidfile_path = NULL; ++ errno = save_errno; ++ return -1; ++ } ++ (void)fclose(f); ++ ++ if (atexit_already) ++ return 0; ++ ++ pidfile_pid = pid; ++ if (atexit(pidfile_cleanup) < 0) { ++ save_errno = errno; ++ (void)unlink(pidfile_path); ++ free(pidfile_path); ++ pidfile_path = NULL; ++ pidfile_pid = 0; ++ errno = save_errno; ++ return -1; ++ } ++ ++ return 0; ++} ++ ++static void pidfile_cleanup(void) ++{ ++ if (pidfile_path != NULL && pidfile_pid == getpid()) { ++ (void)unlink(pidfile_path); ++ free(pidfile_path); ++ pidfile_path = NULL; ++ } ++} +diff --git a/pidfile.h b/pidfile.h +new file mode 100644 +index 0000000..7b68c78 +--- /dev/null ++++ b/pidfile.h +@@ -0,0 +1,10 @@ ++/** ++ * @file pidfile.h ++ * @brief PID file support, derived from OpenBSD via libite. ++ */ ++#ifndef HAVE_PIDFILE_H ++#define HAVE_PIDFILE_H ++ ++int pidfile(const char *basename); ++ ++#endif +diff --git a/ptp4l.c b/ptp4l.c +index ac2ef96..12f5e20 100644 +--- a/ptp4l.c ++++ b/ptp4l.c +@@ -34,6 +34,7 @@ + #include "transport.h" + #include "udp6.h" + #include "uds.h" ++#include "pidfile.h" + #include "util.h" + #include "version.h" + +@@ -63,6 +64,7 @@ static void usage(char *progname) + " -l [num] set the logging level to 'num'\n" + " -m print messages to stdout\n" + " -q do not print messages to the syslog\n" ++ " -u [file] write process ID to 'file'\n" + " -v prints the software version and exits\n" + " -h prints this message and exits\n" + "\n", +@@ -90,7 +92,7 @@ int main(int argc, char *argv[]) + /* Process the command line arguments. */ + progname = strrchr(argv[0], '/'); + progname = progname ? 1+progname : argv[0]; +- while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqvh", ++ while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqu:vh", + opts, &index))) { + switch (c) { + case 0: +@@ -163,6 +165,10 @@ int main(int argc, char *argv[]) + case 'q': + config_set_int(cfg, "use_syslog", 0); + break; ++ case 'u': ++ if (config_set_string(cfg, "pidfile", optarg)) ++ goto out; ++ break; + case 'v': + version_show(stdout); + return 0; +@@ -188,6 +194,12 @@ int main(int argc, char *argv[]) + print_set_syslog(config_get_int(cfg, NULL, "use_syslog")); + print_set_level(config_get_int(cfg, NULL, "logging_level")); + ++ if (config_get_string(cfg, NULL, "pidfile") && ++ pidfile(config_get_string(cfg, NULL, "pidfile"))) { ++ fprintf(stderr, "failed to create pidfile\n"); ++ goto out; ++ } ++ + assume_two_step = config_get_int(cfg, NULL, "assume_two_step"); + sk_check_fupsync = config_get_int(cfg, NULL, "check_fup_sync"); + sk_tx_timeout = config_get_int(cfg, NULL, "tx_timestamp_timeout"); +diff --git a/timemaster.c b/timemaster.c +index b367b2f..873083f 100644 +--- a/timemaster.c ++++ b/timemaster.c +@@ -38,6 +38,7 @@ + #include + #include + ++#include "pidfile.h" + #include "print.h" + #include "rtnl.h" + #include "sk.h" +@@ -1531,6 +1532,7 @@ static void usage(char *progname) + "\nusage: %s [options] -f file\n\n" + " -f file specify path to configuration file\n" + " -n only print generated files and commands\n" ++ " -u file write process ID to 'file'\n" + " -l level set logging level (6)\n" + " -m print messages to stdout\n" + " -q do not print messages to syslog\n" +@@ -1543,7 +1545,7 @@ int main(int argc, char **argv) + { + struct timemaster_config *config; + struct script *script; +- char *progname, *config_path = NULL; ++ char *progname, *config_path = NULL, *pid_file = NULL; + int c, ret = 0, log_stdout = 0, log_syslog = 1, dry_run = 0; + + progname = strrchr(argv[0], '/'); +@@ -1553,7 +1555,7 @@ int main(int argc, char **argv) + print_set_verbose(1); + print_set_syslog(0); + +- while (EOF != (c = getopt(argc, argv, "f:nl:mqvh"))) { ++ while (EOF != (c = getopt(argc, argv, "f:nu:l:mqvh"))) { + switch (c) { + case 'f': + config_path = optarg; +@@ -1561,6 +1563,9 @@ int main(int argc, char **argv) + case 'n': + dry_run = 1; + break; ++ case 'u': ++ pid_file = optarg; ++ break; + case 'l': + print_set_level(atoi(optarg)); + break; +@@ -1599,10 +1604,16 @@ int main(int argc, char **argv) + print_set_verbose(log_stdout); + print_set_syslog(log_syslog); + +- if (dry_run) ++ if (dry_run) { + script_print(script); +- else ++ } else { ++ if (pid_file && pidfile(pid_file)) { ++ pr_err("failed to create pidfile %s: %m", pid_file); ++ script_destroy(script); ++ return 1; ++ } + ret = script_run(script); ++ } + + script_destroy(script); + +-- +2.43.0 + diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index c48c365b7..266e642b8 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -711,6 +711,23 @@ def keystore(args: List[str]) -> None: print("Usage: show keystore [symmetric | asymmetric ]") +def ptp(args: List[str]) -> None: + data = get_json("/ieee1588-ptp-tt:ptp") + if not data: + print("PTP: no instances running.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + + # Optional: filter to a specific instance-index + if args and args[0].isdigit(): + cli_pretty(data, "show-ptp", args[0]) + else: + cli_pretty(data, "show-ptp") + + def execute_command(command: str, args: List[str]): command_mapping = { 'bfd': bfd, @@ -725,6 +742,7 @@ def execute_command(command: str, args: List[str]): 'nacm': nacm, 'ntp': ntp, 'ospf': ospf, + 'ptp': ptp, 'rip': rip, 'routes': routes, 'services': services, diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index f457f1ad5..447117994 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -49,6 +49,7 @@ confd_plugin_la_SOURCES = \ keystore.c \ system.c \ ntp.c \ + ptp.c \ syslog.c \ factory-default.c \ routing.c \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 5e4fddaa2..dfd0261d4 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -511,6 +511,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if ((rc = ntp_change(session, config, diff, event, confd))) goto free_diff; + /* ieee1588-ptp-tt */ + if ((rc = ptp_change(session, config, diff, event, confd))) + goto free_diff; + /* infix-services */ if ((rc = services_change(session, config, diff, event, confd))) goto free_diff; @@ -706,6 +710,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) ERROR("Failed to subscribe to infix-meta"); goto err; } + rc = subscribe_model("ieee1588-ptp-tt", &confd, 0); + if (rc) { + ERROR("Failed to subscribe to ieee1588-ptp-tt"); + goto err; + } rc = system_rpc_init(&confd); if (rc) diff --git a/src/confd/src/core.h b/src/confd/src/core.h index f25d91bd4..b56c8bf32 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -271,4 +271,7 @@ int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, const char *path, sr_event_t event, unsigned request_id, void *priv); int ntp_candidate_init(struct confd *confd); +/* ptp.c */ +int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); + #endif /* CONFD_CORE_H_ */ diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c new file mode 100644 index 000000000..4b97575d4 --- /dev/null +++ b/src/confd/src/ptp.c @@ -0,0 +1,452 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include + +#include +#include +#include + +#include "core.h" + +#define XPATH_PTP_ "/ieee1588-ptp-tt:ptp" +#define PTP_CONF_DIR "/etc/linuxptp" + +/* + * Map instance-type string to ptp4l clockType keyword. + * Returns NULL for oc/bc (no explicit clockType needed in [global]). + */ +static const char *instance_type_to_clock_type(const char *type) +{ + if (!type) + return NULL; + if (!strcmp(type, "p2p-tc")) + return "P2P_TC"; + if (!strcmp(type, "e2e-tc")) + return "E2E_TC"; + return NULL; +} + +/* + * Emit all protocol-mandatory [global] settings for the chosen profile. + * Returns true when the profile is ieee802-dot1as (802.1AS/gPTP), which + * the caller uses to suppress per-port delay_mechanism output — identical + * to the guard already used for Transparent Clock instances. + * + * ieee802-dot1as sets the full gPTP option set as required by the standard: + * transportSpecific, network_transport, delay_mechanism, multicast MACs, + * gmCapable, follow_up_info, assume_two_step, path_trace_enabled, + * and the tighter neighborPropDelayThresh. + */ +static bool emit_profile_globals(FILE *fp, const char *profile) +{ + bool dot1as = profile && !strcmp(profile, "ieee802-dot1as"); + + if (dot1as) { + fprintf(fp, "transportSpecific 1\n"); + fprintf(fp, "network_transport L2\n"); + fprintf(fp, "delay_mechanism P2P\n"); + fprintf(fp, "ptp_dst_mac 01:80:C2:00:00:0E\n"); + fprintf(fp, "p2p_dst_mac 01:80:C2:00:00:0E\n"); + fprintf(fp, "gmCapable 1\n"); + fprintf(fp, "follow_up_info 1\n"); + fprintf(fp, "assume_two_step 1\n"); + fprintf(fp, "path_trace_enabled 1\n"); + fprintf(fp, "neighborPropDelayThresh 800\n"); + } else { + fprintf(fp, "transportSpecific 0\n"); + } + return dot1as; +} + +/* + * Return true if ifname has hardware TX timestamping capability according + * to the probed data in system.json (confd->root). If the interface is + * absent from system.json (virtual interface, QEMU tap, etc.) or the + * "hardware-transmit" capability string is missing, returns false. + */ +static bool iface_has_hw_timestamp(json_t *root, const char *ifname) +{ + json_t *caps, *list, *entry; + size_t i; + + if (!root || !ifname) + return false; + + caps = json_object_get(json_object_get( + json_object_get(root, "interfaces"), + ifname), "ptp-capabilities"); + if (!caps) + return false; + + list = json_object_get(caps, "capabilities"); + if (!json_is_array(list)) + return false; + + json_array_foreach(list, i, entry) { + const char *s = json_string_value(entry); + if (s && !strcmp(s, "hardware-transmit")) + return true; + } + return false; +} + +/* + * Scan all ports of inst and determine whether to use hardware or software + * timestamping. Emits a syslog WARNING when falling back to software due + * to a mixed or software-only set of port interfaces. + */ +static const char *instance_time_stamping(struct lyd_node *inst, json_t *root) +{ + struct lyd_node *port; + bool all_hw = true; + bool any_sw = false; + + LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") { + const char *iface = lydx_get_cattr(port, "underlying-interface"); + if (!iface) + continue; + if (iface_has_hw_timestamp(root, iface)) + continue; + all_hw = false; + any_sw = true; + } + + if (any_sw && !all_hw) + WARN("PTP instance has software-only timestamping port(s), " + "falling back to time_stamping software"); + return all_hw ? "hardware" : "software"; +} + +/* + * Write ptp4l config for one PTP instance. + * Config file: /etc/linuxptp/ptp4l-.conf+ (staging) + * + * ptp4l key config layout: + * [global] — instance-wide settings + * [eth0] — per-port interface sections, sorted by port-index + */ +static int write_instance_conf(struct lyd_node *inst, json_t *root) +{ + struct lyd_node *default_ds, *port, *port_ds; + const char *instance_type, *clock_type, *profile; + const char *v; + char path[256]; + uint16_t idx; + bool tc, bc, dot1as; + FILE *fp; + + v = lydx_get_cattr(inst, "instance-index"); + if (!v) + return SR_ERR_INVAL_ARG; + idx = (uint16_t)atoi(v); + + snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); + fp = fopen(path, "w"); + if (!fp) { + ERRNO("Failed creating %s", path); + return SR_ERR_SYS; + } + + fprintf(fp, "# Generated by confd — do not edit\n\n"); + fprintf(fp, "[global]\n"); + + default_ds = lydx_get_child(inst, "default-ds"); + instance_type = lydx_get_cattr(default_ds, "instance-type"); + profile = lydx_get_cattr(default_ds, "infix-ptp:profile"); + + clock_type = instance_type_to_clock_type(instance_type); + tc = (clock_type != NULL); + bc = instance_type && !strcmp(instance_type, "bc"); + + /* Unique UDS socket per instance — required for pmc with multiple instances */ + fprintf(fp, "uds_address /var/run/ptp4l-%u\n", idx); + + /* Timestamping mode: hardware if all ports support it, software otherwise */ + fprintf(fp, "time_stamping %s\n", instance_time_stamping(inst, root)); + + /* Profile — sets transportSpecific and all protocol-mandatory options */ + dot1as = emit_profile_globals(fp, profile); + + /* domainNumber */ + v = lydx_get_cattr(default_ds, "domain-number"); + if (v) + fprintf(fp, "domainNumber %s\n", v); + + /* Transparent Clock clock_type */ + if (tc) + fprintf(fp, "clock_type %s\n", clock_type); + + /* + * Multi-port instances (BC and TC) may span ports on different PHC + * devices when the ports belong to different switch chips (e.g. a + * three-chip board where each mv88e6xxx chip owns its own /dev/ptpN). + * boundary_clock_jbod silences ptp4l's startup PHC-mismatch check and + * lets each port use its own PHC. It is a no-op when all ports share + * the same PHC (single-chip board or software timestamping). + * + * NOTE: for BC on multi-chip hardware, ptp4l only disciplines the PHC + * of the active slave port; the other chips' PHCs will drift unless + * phc2sys(8) -a is also run. That is a separate service (TODO). + */ + if (tc || bc) + fprintf(fp, "boundary_clock_jbod 1\n"); + + /* priority1 / priority2 (not applicable for TC, but harmless) */ + v = lydx_get_cattr(default_ds, "priority1"); + if (v) + fprintf(fp, "priority1 %s\n", v); + v = lydx_get_cattr(default_ds, "priority2"); + if (v) + fprintf(fp, "priority2 %s\n", v); + + /* clientOnly (OC only) — inclusive replacement for slaveOnly in ptp4l 4.x */ + v = lydx_get_cattr(default_ds, "time-receiver-only"); + if (v && !strcmp(v, "true")) + fprintf(fp, "clientOnly 1\n"); + + /* maxStepsRemoved */ + v = lydx_get_cattr(default_ds, "max-steps-removed"); + if (v) + fprintf(fp, "maxStepsRemoved %s\n", v); + + /* + * Transparent Clocks set delay_mechanism globally; ptp4l ignores + * per-port delay_mechanism for TCs. 802.1AS mandates P2P globally + * (already emitted by emit_profile_globals). + */ + if (tc) { + if (!strcmp(clock_type, "P2P_TC")) + fprintf(fp, "delay_mechanism P2P\n"); + else + fprintf(fp, "delay_mechanism E2E\n"); + } + + fprintf(fp, "\n"); + + /* Per-port [interface] sections, sorted by port-index */ + LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") { + const char *iface, *dm; + + iface = lydx_get_cattr(port, "underlying-interface"); + if (!iface) + continue; + + fprintf(fp, "[%s]\n", iface); + + port_ds = lydx_get_child(port, "port-ds"); + if (!port_ds) + goto next_port; + + v = lydx_get_cattr(port_ds, "log-announce-interval"); + if (v) + fprintf(fp, "logAnnounceInterval %s\n", v); + + v = lydx_get_cattr(port_ds, "announce-receipt-timeout"); + if (v) + fprintf(fp, "announceReceiptTimeout %s\n", v); + + v = lydx_get_cattr(port_ds, "log-sync-interval"); + if (v) + fprintf(fp, "logSyncInterval %s\n", v); + + v = lydx_get_cattr(port_ds, "log-min-delay-req-interval"); + if (v) + fprintf(fp, "logMinDelayReqInterval %s\n", v); + + v = lydx_get_cattr(port_ds, "log-min-pdelay-req-interval"); + if (v) + fprintf(fp, "logMinPdelayReqInterval %s\n", v); + + /* + * delay_mechanism per port — only for OC/BC on ieee1588 profile. + * TC and 802.1AS both set it globally; ptp4l ignores per-port + * overrides in those cases. + */ + if (!tc && !dot1as) { + dm = lydx_get_cattr(port_ds, "delay-mechanism"); + if (dm) { + if (!strcmp(dm, "p2p")) + fprintf(fp, "delay_mechanism P2P\n"); + else if (!strcmp(dm, "e2e")) + fprintf(fp, "delay_mechanism E2E\n"); + } + } + + v = lydx_get_cattr(port_ds, "delay-asymmetry"); + if (v && strcmp(v, "0")) + fprintf(fp, "delayAsymmetry %s\n", v); + + /* port-enable */ + v = lydx_get_cattr(port_ds, "port-enable"); + if (v && !strcmp(v, "false")) + fprintf(fp, "masterOnly 0\n"); + + next_port: + fprintf(fp, "\n"); + } + + fclose(fp); + return SR_ERR_OK; +} + +/* + * Remove staging config for one instance. + */ +static void remove_staging(uint16_t idx) +{ + char path[256]; + + snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); + (void)remove(path); +} + +/* + * Activate one instance: rename staging → live, enable finit service. + */ +static int activate_instance(uint16_t idx) +{ + char staging[256], live[256]; + + snprintf(staging, sizeof(staging), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); + snprintf(live, sizeof(live), PTP_CONF_DIR "/ptp4l-%u.conf", idx); + + if (!fexist(staging)) { + (void)remove(live); + return SR_ERR_OK; + } + + (void)rename(live, live); /* no-op, clears stale */ + if (rename(staging, live)) { + ERRNO("Failed renaming %s → %s", staging, live); + return SR_ERR_SYS; + } + + finit_enablef("ptp4l@%u", idx); + return finit_reloadf("ptp4l@%u", idx); +} + +/* + * Deactivate (disable) one instance and remove its live config. + */ +static void deactivate_instance(uint16_t idx) +{ + char live[256]; + + finit_disablef("ptp4l@%u", idx); + + snprintf(live, sizeof(live), PTP_CONF_DIR "/ptp4l-%u.conf", idx); + (void)remove(live); +} + +/* + * Disable any ptp4l@ services in finit enabled/ whose index is not in the + * currently configured set. Called from SR_EV_DONE after enabling active + * instances, to clean up stale services from a previous config. + */ +static void cleanup_stale_instances(struct lyd_node *config) +{ + struct lyd_node *inst; + struct dirent *ent; + DIR *d; + int idx; + + d = opendir(FINIT_RCSD "/enabled"); + if (!d) + return; + + while ((ent = readdir(d))) { + bool found = false; + + if (sscanf(ent->d_name, "ptp4l@%d.conf", &idx) != 1) + continue; + + /* Is this index still configured? */ + LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL), + inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (v && atoi(v) == idx) { + found = true; + break; + } + } + + if (!found) + deactivate_instance((uint16_t)idx); + } + + closedir(d); +} + +static int change(sr_session_ctx_t *session, struct lyd_node *config, + struct lyd_node *diff, sr_event_t event, struct confd *confd) +{ + struct lyd_node *instances, *inst; + int rc = SR_ERR_OK; + + if (diff && !lydx_get_xpathf(diff, XPATH_PTP_)) + return SR_ERR_OK; + + switch (event) { + case SR_EV_ENABLED: + case SR_EV_CHANGE: + break; + + case SR_EV_ABORT: + /* Remove any staging files */ + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + LYX_LIST_FOR_EACH(instances, inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (v) + remove_staging((uint16_t)atoi(v)); + } + return SR_ERR_OK; + + case SR_EV_DONE: + /* Activate all configured instances */ + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + LYX_LIST_FOR_EACH(instances, inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (!v) + continue; + if ((rc = activate_instance((uint16_t)atoi(v)))) + return rc; + } + + /* Disable stale services not in current config */ + cleanup_stale_instances(config); + + if (!instances) + return SR_ERR_OK; + + return SR_ERR_OK; + + default: + return SR_ERR_OK; + } + + /* SR_EV_ENABLED / SR_EV_CHANGE — generate staging configs */ + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + if (!instances) + return SR_ERR_OK; + + if (mkdir(PTP_CONF_DIR, 0755) && errno != EEXIST) { + ERRNO("Failed creating " PTP_CONF_DIR); + return SR_ERR_SYS; + } + + LYX_LIST_FOR_EACH(instances, inst, "instance") { + rc = write_instance_conf(inst, confd->root); + if (rc) + return rc; + } + + return SR_ERR_OK; +} + +int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, + struct lyd_node *diff, sr_event_t event, struct confd *confd) +{ + return change(session, config, diff, event, confd); +} diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 27bc3fe9b..83bea27e4 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -47,10 +47,13 @@ MODULES=( "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" - "infix-interfaces@2025-11-06.yang -e vlan-filtering" + "infix-interfaces@2026-04-09.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" "infix-ntp@2026-03-09.yang" "infix-keystore@2025-12-17.yang" + "ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction" + "ieee802-dot1as-gptp@2025-12-10.yang" + "infix-ptp@2026-04-07.yang" ) diff --git a/src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang b/src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang new file mode 100644 index 000000000..5ccc5ab63 --- /dev/null +++ b/src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang @@ -0,0 +1,4326 @@ +module ieee1588-ptp-tt { + yang-version 1.1; + namespace urn:ieee:std:1588:yang:ieee1588-ptp-tt; + prefix "ptp-tt"; + + import ietf-yang-types { + prefix yang; + } + import ietf-interfaces { + prefix if; + } + + organization "IEEE 1588 Working Group"; + contact + "Web: https://sagroups.ieee.org/1588/ + E-mail: 1588officers@listserv.ieee.org + + Postal: C/O IEEE 1588 Working Group Chair + IEEE Standards Association + 445 Hoes Lane + Piscataway, NJ 08854 + USA"; + description + "This YANG module defines a data model for the configuration + and state of IEEE Std 1588 clocks. IEEE Std 1588 specifies the + Precision Time Protocol (PTP). + + The nodes in this YANG module are designed for compatibility + with ietf-ptp.yang, the YANG data model for IEEE Std 1588-2008, + as specified in IETF RFC 8575. + + NOTE regarding default value: + PTP's concept of 'initialization value' is analogous to YANG's + concept of a 'default value'. According to 8.1.3.4 of + IEEE Std 1588-2019, the initialization value for configuration + is specified in IEEE Std 1588, but that value can be overridden + by a PTP Profile specification, or by the product that + implements PTP. This makes it challenging to repeat the + specification of initialization value using a YANG 'default' + statement, because there is no straightforward mechanism for + a PTP Profile's (or product's) YANG module to import this + module and override its YANG default. Since a YANG management + client can read the default value from the operational + datastore, there is no need to re-specify the default in YANG. + The implementer of PTP refers to the relevant PTP + specifications for the default (not YANG modules). + Therefore, this YANG module avoids use of the YANG 'default' + statement. + + NOTE regarding IEEE Std 1588 classification: + 8.1.2 of IEEE Std 1588-2019 specifies a classification of + each data set member, which corresponds to a leaf in YANG. + The relationship between 1588 classification and + YANG 'config' (i.e., whether the leaf is read-write) is: + - 1588 static: The leaf is 'config false' (read-only). + - 1588 configurable: The leaf is 'config true', which is + the default value for a YANG leaf. + - 1588 dynamic: A judgement is made on a member-by-member + basis. If the member corresponds to the first item of + 8.1.2.1.2 of IEEE Std 1588-2019 (i.e., value from protocol + only, such as log of protocol behavior), the YANG leaf + is 'config false'. Otherwise, the member's value can be + provided by an entity outside PTP (e.g., NETCONF or + RESTCONF client), and therefore the YANG leaf is + 'config true'. + + NOTE regarding terminology (two YANG modules): + To accommodate the need by some organizations to use the + original terminology specified by IEEE Std 1588, and the + need by some other organizations to use the alternative + terminology specified in 4.4 of IEEE Std 1588g-2022, + two YANG modules are provided by IEEE Std 1588e (MIB and + YANG Data Models). For a detailed explanation, see 15.4.2.11 + of IEEE Std 1588e. + This module uses the alternative terminology specified in + 4.4 of IEEE Std 1588g-2022 (timeTransmitter/timeReceiver)."; + + revision 2023-08-14 { + description + "Initial revision."; + reference + "IEEE Std 1588e-2024, IEEE Standard for a Precision Clock + Synchronization Protocol for Networked Measurement and + Control Systems - MIB and YANG Data Models."; + } + + feature fault-log { + description + "Logging of faults detected in the PTP Instance."; + reference + "8.2.6 of IEEE Std 1588-2019"; + } + + feature unicast-negotiation { + description + "Unicast negotiation conducted through use of TLVs."; + reference + "16.1 of IEEE Std 1588-2019"; + } + + feature path-trace { + description + "Use of the PATH_TRACE TLV for tracing the route of + a PTP Announce message through the PTP Network."; + reference + "16.2 of IEEE Std 1588-2019"; + } + + feature alternate-timescale { + description + "The transmission of an ALTERNATE_TIME_OFFSET_INDICATOR TLV + entity from the Grandmaster PTP Instance may indicate the + offset of an alternate timescale from the timescale in + use in the domain."; + reference + "16.3 of IEEE Std 1588-2019"; + } + + feature holdover-upgrade { + description + "A holdover-upgradable PTP Instance can potentially + become the Grandmaster PTP Instance in the event the + previous Grandmaster PTP Instance is disconnected + or its characteristics degrade."; + reference + "16.4 of IEEE Std 1588-2019"; + } + + feature cmlds { + description + "The Common Mean Link Delay Service (CMLDS) is an optional + service that enables any PTP Port that would normally obtain + the value of a link's and + using the peer-to-peer method to instead obtain these + values from this optional service. The CMLDS service is + available to all PTP Instances communicating with a specific + transport mechanism, over the physical link between two PTP + Nodes."; + reference + "16.6 of IEEE Std 1588-2019"; + } + + feature timestamp-correction { + description + "Correction of timestamps using configurable management data."; + reference + "16.7 of IEEE Std 1588-2019"; + } + + feature asymmetry-correction { + description + "Calculation of the on a Direct PTP Link + between two PTP Instances connected using an applicable + bidirectional medium."; + reference + "16.8 of IEEE Std 1588-2019"; + } + + feature time-receiver-monitoring { + description + "Mechanism for monitoring timing information in a PTP Port + in the timereceiver state. The time-receiver-monitoring feature + specifies TLVs that the TimeReceiver PTP Instance transmits + with this information, typically in a Signaling message."; + reference + "16.11 of IEEE Std 1588-2019"; + } + + feature enhanced-metrics { + description + "Mechanism for propagating estimates of various + inaccuracy components affecting the overall expected + PTP Instance Time accuracy. The metrics will be updated + and available for utilization at the various points along + the PTP timing chain: from the Grandmaster Instance, up to + a leaf PTP Instance in the synchronization tree. Each + PTP Instance along the timing path updates the + relevant metrics based on its contribution to the expected + degradation in PTP Instance Time accuracy due to various + induced timing error components."; + reference + "16.12 of IEEE Std 1588-2019"; + } + + feature grandmaster-cluster { + description + "Mechanism for faster selection of the Grandmaster PTP Instance + from the set of PTP Instances for which this option is both + implemented and enabled."; + reference + "17.2 of IEEE Std 1588-2019"; + } + + feature alternate-time-transmitter { + description + "Mechanism for PTP Ports on a PTP Communication Path that + are not currently the time-transmitter port of that + PTP Communicatio Path to exchange PTP timing information with + other PTP Ports on the same PTP Communication Path, and for + each of the other PTP Ports to acquire knowledge of the + characteristics of the transmission path between itself and + each alternate timeTransmitter PTP Port."; + reference + "17.3 of IEEE Std 1588-2019"; + } + + feature unicast-discovery { + description + "Mechanism for PTP to be used over a network that does not + provide multicast. A PTP Instance is configured with the + addresses of PTP Ports of other PTP Instances with which + it should attempt to establish unicast communication. + The PTP Instance may request that these PTP Ports transmit + unicast Announce, Sync, and Delay_Resp messages to it."; + reference + "17.4 of IEEE Std 1588-2019"; + } + + feature acceptable-time-transmitter { + description + "Mechanism that allows PTP Ports in the time-receiver state + to be configured to refuse to synchronize to PTP Instances not + on the acceptable timeTransmitter list."; + reference + "17.5 of IEEE Std 1588-2019"; + } + + feature external-port-config { + description + "External port configuration allows an external entity + (such as YANG-based remote management) to disable the + IEEE Std 1588 state machines that control each port's + state, including the BTCA. Each port's state is + then configured by the external entity."; + reference + "17.6 of IEEE Std 1588-2019"; + } + + feature performance-monitoring { + description + "Collection of performance monitoring logs that can be + read using management."; + reference + "Annex J of IEEE Std 1588-2019"; + } + + feature l1-sync { + description + "Layer 1-based synchronization performance + enhancement."; + reference + "Annex L of IEEE Std 1588-2019"; + } + + identity network-protocol { + description + "Enumeration for the protocol used by a PTP Instance to + transport PTP messages. + YANG identity is used so that a PTP Profile's YANG augment + can assign values, using numeric range F000 to FFFD hex."; + reference + "7.4.1 of IEEE Std 1588-2019"; + } + identity udp-ipv4 { + base network-protocol; + description + "UDP on IPv4. Numeric value is 0001 hex."; + } + identity udp-ipv6 { + base network-protocol; + description + "UDP on IPv6. Numeric value is 0002 hex."; + } + identity ieee802-3 { + base network-protocol; + description + "IEEE Std 802.3 (Ethernet). Numeric value is 0003 hex."; + } + identity devicenet { + base network-protocol; + description + "DeviceNet. Numeric value is 0004 hex."; + } + identity controlnet { + base network-protocol; + description + "ControlNet. Numeric value is 0005 hex."; + } + identity profinet { + base network-protocol; + description + "PROFINET. Numeric value is 0006 hex."; + } + identity otn { + base network-protocol; + description + "Optical Transport Network (OTN). Numeric value + is 0007 hex."; + } + identity unknown { + base network-protocol; + description + "Unknown. Numeric value is FFFE hex."; + } + + identity clock-class { + description + "Enumeration that denotes the traceability, synchronization + state and expected performance of the time or frequency + distributed by the Grandmaster PTP Instance. + IEEE Std 1588 does not specify a name for each clock-class, + but the names below are intended to be as intuitive as possible. + YANG identity is used so that a PTP Profile's YANG augment + can assign values using a numeric range designated for use by + alternate PTP Profiles."; + reference + "7.6.2.5 of IEEE Std 1588-2019"; + } + identity cc-primary-sync { + base clock-class; + description + "A PTP Instance that is synchronized to a primary + reference time source. The timescale distributed shall be PTP. + Numeric value is 6 decimal."; + } + identity cc-primary-sync-lost { + base clock-class; + description + "A PTP Instance that has previously been designated + as clockClass 6, but that has lost the ability to + synchronize to a primary reference time source and is in + holdover mode and within holdover specifications. Or a PTP + Instance designated with clockClass 7 based on the Holdover + Upgrade option. The timescale distributed shall be PTP. + Numeric value is 7 decimal."; + } + identity cc-application-specific-sync { + base clock-class; + description + "A PTP Instance that is synchronized to an + application-specific source of time. The timescale + distributed shall be ARB. + Numeric value is 13 decimal."; + } + identity cc-application-specific-sync-lost { + base clock-class; + description + "A PTP Instance that has previously been designated as + clockClass 13, but that has lost the ability to synchronize + to an application-specific source of time and is in + holdover mode and within holdover specifications. Or a PTP + Instance designated with clockClass 14 based on the Holdover + Upgrade option. The timescale distributed shall be ARB. + Numeric value is 14 decimal."; + } + identity cc-primary-sync-alternative-a { + base clock-class; + description + "Degradation alternative A for a PTP Instance of + clockClass 7 that is not within holdover specification + or that is based on the specifications of the Holdover + Upgrade option. + Numeric value is 52 decimal."; + } + identity cc-application-specific-alternative-a { + base clock-class; + description + "Degradation alternative A for a PTP Instance of + clockClass 14 that is not within holdover specification or + that is based on the specifications of the Holdover Upgrade + option. + Numeric value is 58 decimal."; + } + identity cc-primary-sync-alternative-b { + base clock-class; + description + "Degradation alternative B for a PTP Instance of + clockClass 7 that is not within holdover specification + or that is based on the specifications of the Holdover + Upgrade option. + Numeric value is 187 decimal."; + } + identity cc-application-specific-alternative-b { + base clock-class; + description + "Degradation alternative B for a PTP Instance of + clockClass 14 that is not within holdover specification or + that is based on the specifications of the Holdover Upgrade + option. + Numeric value is 193 decimal."; + } + identity cc-default { + base clock-class; + description + "Default clockClass, used if none of the other + clockClass definitions apply. + Numeric value is 248 decimal."; + } + identity cc-time-receiver-only { + base clock-class; + description + "A PTP Instance that is timeReceiver only. + Numeric value is 255 decimal."; + } + + identity clock-accuracy { + description + "Enumeration that indicates the expected accuracy of a + PTP Instance when it is the Grandmaster PTP Instance, + or in the event it becomes the Grandmaster PTP Instance. + The value shall be conservatively estimated by the PTP + Instance to a precision consistent with the value of the + selected clock-accuracy and of the next lower enumerated + value, for example, for clockAccuracy = 23 hex, between + 250 ns and 1000 ns. + IEEE Std 1588 does not specify a name for each clock-accuracy, + but the names below are intended to be as intuitive as possible. + YANG identity is used so that a PTP Profile's YANG augment + can assign values, using numeric range 80 to FD hex."; + reference + "7.6.2.6 of IEEE Std 1588-2019"; + } + identity ca-time-accurate-to-1000-fs { + base clock-accuracy; + description + "The time is accurate to within 1 ps (1000 fs). + Numeric value is 17 hex."; + } + identity ca-time-accurate-to-2500-fs { + base clock-accuracy; + description + "The time is accurate to within 2.5 ps (2500 fs). + Numeric value is 18 hex."; + } + identity ca-time-accurate-to-10-ps { + base clock-accuracy; + description + "The time is accurate to within 10 ps. + Numeric value is 19 hex."; + } + identity ca-time-accurate-to-25ps { + base clock-accuracy; + description + "The time is accurate to within 25 ps. + Numeric value is 1A hex."; + } + identity ca-time-accurate-to-100-ps { + base clock-accuracy; + description + "The time is accurate to within 100 ps. + Numeric value is 1B hex."; + } + identity ca-time-accurate-to-250-ps { + base clock-accuracy; + description + "The time is accurate to within 250 ps. + Numeric value is 1C hex."; + } + identity ca-time-accurate-to-1000-ps { + base clock-accuracy; + description + "The time is accurate to within 1ns (1000 ps). + Numeric value is 1D hex."; + } + identity ca-time-accurate-to-2500-ps { + base clock-accuracy; + description + "The time is accurate to within 2.5 ns (2500 ps). + Numeric value is 1E hex."; + } + identity ca-time-accurate-to-10-ns { + base clock-accuracy; + description + "The time is accurate to within 10 ns. + Numeric value is 1F hex."; + } + identity ca-time-accurate-to-25-ns { + base clock-accuracy; + description + "The time is accurate to within 25 ns. + Numeric value is 20 hex."; + } + identity ca-time-accurate-to-100-ns { + base clock-accuracy; + description + "The time is accurate to within 100 ns. + Numeric value is 21 hex."; + } + identity ca-time-accurate-to-250-ns { + base clock-accuracy; + description + "The time is accurate to within 250 ns. + Numeric value is 22 hex."; + } + identity ca-time-accurate-to-1000-ns { + base clock-accuracy; + description + "The time is accurate to within 1 us (1000 ns). + Numeric value is 23 hex."; + } + identity ca-time-accurate-to-2500-ns { + base clock-accuracy; + description + "The time is accurate to within 2.5 us (2500 ns). + Numeric value is 24 hex."; + } + identity ca-time-accurate-to-10-us { + base clock-accuracy; + description + "The time is accurate to within 10 us. + Numeric value is 25 hex."; + } + identity ca-time-accurate-to-25-us { + base clock-accuracy; + description + "The time is accurate to within 25 us. + Numeric value is 26 hex."; + } + identity ca-time-accurate-to-100-us { + base clock-accuracy; + description + "The time is accurate to within 100 us. + Numeric value is 27 hex."; + } + identity ca-time-accurate-to-250-us { + base clock-accuracy; + description + "The time is accurate to within 250 us. + Numeric value is 28 hex."; + } + identity ca-time-accurate-to-1000-us { + base clock-accuracy; + description + "The time is accurate to within 1 ms (1000 us). + Numeric value is 29 hex."; + } + identity ca-time-accurate-to-2500-us { + base clock-accuracy; + description + "The time is accurate to within 2.5 ms (2500 us). + Numeric value is 2A hex."; + } + identity ca-time-accurate-to-10-ms { + base clock-accuracy; + description + "The time is accurate to within 10 ms. + Numeric value is 2B hex."; + } + identity ca-time-accurate-to-25-ms { + base clock-accuracy; + description + "The time is accurate to within 25 ms. + Numeric value is 2Chex."; + } + identity ca-time-accurate-to-100-ms { + base clock-accuracy; + description + "The time is accurate to within 100 ms. + Numeric value is 2D hex."; + } + identity ca-time-accurate-to-250-ms { + base clock-accuracy; + description + "The time is accurate to within 250 ms. + Numeric value is 2E hex."; + } + identity ca-time-accurate-to-1-s { + base clock-accuracy; + description + "The time is accurate to within 1 s. + Numeric value is 2F hex."; + } + identity ca-time-accurate-to-10-s { + base clock-accuracy; + description + "The time is accurate to within 10 s. + Numeric value is 30 hex."; + } + identity ca-time-accurate-to-gt-10-s { + base clock-accuracy; + description + "The time accuracy exceeds 10 s. + Numeric value is 31 hex."; + } + + identity time-source { + description + "Enumeration for the source of time used by the Grandmaster + PTP Instance. + YANG identity is used so that a PTP Profile's YANG augment + can assign values, using numeric range F0 to FE hex."; + reference + "7.6.2.8 of IEEE Std 1588-2019"; + } + identity atomic-clock { + base time-source; + description + "Any PTP Instance that is based on an atomic resonance + for frequency, or a PTP Instance directly connected + to a device that is based on an atomic resonance for + frequency. Numeric value is 10 hex."; + } + identity gnss { + base time-source; + description + "Any PTP Instance synchronized to a satellite system that + distributes time and frequency. Numeric value is 20 hex."; + } + identity terrestrial-radio { + base time-source; + description + "Any PTP Instance synchronized via any of the radio + distribution systems that distribute time and frequency. + Numeric value is 30 hex."; + } + identity serial-time-code { + base time-source; + description + "Any PTP Instance synchronized via any of the serial + time code distribution systems that distribute time + and frequency, for example, IRIG-B. + Numeric value is 39 hex."; + } + identity ptp { + base time-source; + description + "Any PTP Instance synchronized to a PTP-based source + of time external to the domain. Numeric value is 40 hex."; + } + identity ntp { + base time-source; + description + "Any PTP Instance synchronized via NTP or Simple Network + Time Protocol (SNTP) servers that distribute time and + frequency. Numeric value is 50 hex."; + } + identity hand-set { + base time-source; + description + "Used for any PTP Instance whose time has been set by + means of a human interface based on observation of a + source of time to within the claimed clock accuracy. + Numeric value is 60 hex."; + } + identity other { + base time-source; + description + "Other source of time and/or frequency not covered by + other values. Numeric value is 90 hex."; + } + identity internal-oscillator { + base time-source; + description + "Any PTP Instance whose frequency is not based on atomic + resonance, and whose time is based on a free-running + oscillator with epoch determined in an arbitrary or + unknown manner. Numeric value is A0 hex."; + } + + typedef time-interval { + type int64; + description + "Time interval, expressed in nanoseconds, multiplied by 2^16. + Positive or negative time intervals outside the maximum range + of this data type shall be encoded as the largest positive and + negative values of the data type, respectively."; + reference + "5.3.2 of IEEE Std 1588-2019"; + } + + typedef clock-identity { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){7}"; + } + description + "Identifies unique entities within a PTP Network, + e.g. a PTP Instance or an entity of a common service. + The identity is an 8-octet array, constructed according + to specifications in IEEE Std 1588, using an + organization identifier from the IEEE Registration + Authority. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + reference + "5.3.4 of IEEE Std 1588-2019 + 7.5.2.2 of IEEE Std 1588-2019"; + } + + typedef relative-difference { + type int64; + description + "Relative difference expressed as a dimensionless + fraction and multiplied by 2^62, with any + remaining fractional part truncated."; + reference + "5.3.11 of IEEE Std 1588-2019"; + } + + typedef instance-type { + type enumeration { + enum oc { + value 0; + description + "Ordinary Clock"; + } + enum bc { + value 1; + description + "Boundary Clock"; + } + enum p2p-tc { + value 2; + description + "Peer-to-peer Transparent Clock"; + } + enum e2e-tc { + value 3; + description + "End-to-end Transparent Clock"; + } + } + description + "Enumeration for the type of PTP Instance. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "8.2.1.5.5 of IEEE Std 1588-2019"; + } + + typedef fault-severity { + type enumeration { + enum emergency { + value 0; + description + "Emergency: system is unusable"; + } + enum alert { + value 1; + description + "Alert: immediate action needed"; + } + enum critical { + value 2; + description + "Critical: critical conditions"; + } + enum error { + value 3; + description + "Error: error conditions"; + } + enum warning { + value 4; + description + "Warning: warning conditions"; + } + enum notice { + value 5; + description + "Notice: normal but significant condition"; + } + enum informational { + value 6; + description + "Informational: informational messages"; + } + enum debug { + value 7; + description + "Debug: debug-level messages"; + } + } + description + "Enumeration for the severity of a fault record. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "8.2.6.3 of IEEE Std 1588-2019"; + } + + typedef port-state { + type enumeration { + enum initializing { + value 1; + description + "The PTP Port is initializing its data sets, hardware, and + communication facilities. The PTP Port shall not place any + PTP messages on its communication path."; + } + enum faulty { + value 2; + description + "The fault state of the protocol. Except for PTP management + messages that are a required response to a PTP message + received from the applicable management mechanism, + a PTP Port in this state shall not transmit any PTP related + messages. In a Boundary Clock, no activity on a faulty + PTP Port shall affect the other PTP Ports of the + PTP Instance. If fault activity on a PTP Port in this state + cannot be confined to the faulty PTP Port, then all + PTP Ports shall be in the faulty state."; + } + enum disabled { + value 3; + description + "The PTP Port is disabled. Except for PTP management + messages that are a required response to a PTP message + received from the applicable management mechanism, + a PTP Port in this state shall not transmit any PTP related + messages. In a Boundary Clock, no activity at the PTP Port + shall be allowed to affect the activity at any other + PTP Port of the Boundary Clock. A PTP Port in this state + shall discard all received PTP messages except for PTP + management messages."; + } + enum listening { + value 4; + description + "The PTP Port is waiting for the announce-receipt-timeout + to expire or to receive an Announce message from a + TimeTransmitter PTP Instance. The purpose of this state + is to allow orderly addition of PTP Instances to a domain + (i.e. to know if this PTP Port is truly a port of the + Grandmaster PTP Instance prior to taking that role)."; + } + enum pre-time-transmitter { + value 5; + description + "This port state provides an additional mechanism to + support more orderly reconfiguration of PTP Networks when + PTP Instances are added or deleted, PTP Instance + characteristics change, or connection topology changes. + In this state, a PTP Port behaves as it would if it were in + the time-transmitter state except that it does not place + certain classes of PTP messages on the PTP Communication + Path associated with the PTP Port."; + } + enum time-transmitter { + value 6; + description + "The PTP Port is the source of time on the + PTP Communication Path."; + } + enum passive { + value 7; + description + "The PTP Port is not the source of time on the + PTP Communication Path nor does it synchronize to a + TimeTransmitter Clock (receive time). The PTP Port can + potentially change to time-receiver when PTP Instances are + added or deleted, PTP Instance characteristics change, or + connection topology changes."; + } + enum uncalibrated { + value 8; + description + "The PTP Port is anticipating a change to the time-receiver + state, but it has not yet satisfied all requirements + (implementation or PTP Profile) necessary to ensure + complete synchronization. For example, an implementation + might require a minimum number of PTP Sync messages + in order to completely synchronize its servo algorithm."; + } + enum time-receiver { + value 9; + description + "The PTP Port synchronizes to the PTP Port on the + PTP Communication Path that is in the time-transmitter + state (i.e. receives time)."; + } + } + description + "Enumeration for the state of the protocol engine associated + with the PTP Port. Values for this enumeration are specified + by the IEEE 1588 standard exclusively."; + reference + "8.2.15.3.1 of IEEE Std 1588-2019 + 9.2.5 of IEEE Std 1588-2019"; + } + + typedef delay-mechanism { + type enumeration { + enum e2e { + value 1; + description + "The PTP Port is configured to use the delay + request-response mechanism."; + } + enum p2p { + value 2; + description + "The PTP Port is configured to use the peer-to-peer + delay mechanism."; + } + enum no-mechanism { + value 254; + description + "The PTP Port does not implement the delay mechanism. + This value shall not be used except when the applicable + PTP Profile specifies either: + 1) that the PTP Instance only supports frequency + transfer (syntonization) and that neither path delay + mechanism is to be used or + 2) that the PTP Instance participates in time transfer, + but the system accuracy requirements are such that, + for a segment of the system path, delays can be neglected + allowing PTP Instances in that portion of the PTP Network + to use the no-mechanism value."; + } + enum common-p2p { + value 3; + description + "The PTP Port is configured to use the Common Mean Link + Delay Service option."; + } + enum special { + value 4; + description + "Special Ports do not use either delay mechanism."; + } + } + description + "Enumeration for the path delay measuring mechanism. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "8.2.15.4.4 of IEEE Std 1588-2019"; + } + + typedef l1sync-state { + type enumeration { + enum disabled { + value 1; + description + "L1Sync is not enabled on this PTP Port, + or the event L1SYNC_RESET has occurred."; + } + enum idle { + value 2; + description + "L1Sync is enabled on this PTP Port. The PTP Port + sends messages with the L1_SYNC TLV. Initialization + occurs in this state."; + } + enum link-alive { + value 3; + description + "The PTP Port sends messages with the L1_SYNC TLV. + The PTP Port is receiving valid L1_SYNC TLV + from a peer PTP Port."; + } + enum config-match { + value 4; + description + "The PTP Port sends messages with the L1_SYNC TLV. + The PTP Port has a compatible configuration profile + when compared with its peer PTP Port configuration + profile received in the L1_SYNC TLV."; + } + enum l1-sync-up { + value 5; + description + "The PTP Port sends messages with the L1_SYNC TLV. + The relationship required by configuration is currently + in place. Synchronization enhancements are performed."; + } + } + description + "Enumeration for states of an L1Sync state machine associated + with an L1Sync port. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "L.5.3.5 of IEEE Std 1588-2019 + L.7.2 of IEEE Std 1588-2019"; + } + + grouping timestamp { + description + "The IEEE Std 1588 Timestamp type represents a + positive time with respect to the epoch + of PTP Instance Time. + This type is represented in YANG as a grouping, + with leafs seconds-field and nanoseconds-field."; + reference + "5.3.3 of IEEE Std 1588-2019 + 8.2.6.3 of IEEE Std 1588-2019"; + + leaf seconds-field { + type uint64 { + range "0..281474976710655"; + } + description + "The seconds-field member is the integer portion + of the timestamp in units of seconds. Since the + IEEE 1588 type is UInteger48, only 48 bits + are represented in YANG."; + } + + leaf nanoseconds-field { + type uint32; + description + "The nanoseconds-field member is the fractional + portion of the timestamp in units of nanoseconds."; + } + } + grouping port-identity { + description + "The IEEE Std 1588 PortIdentity type identifies a + PTP Port or Link Port."; + reference + "5.3.5 of IEEE Std 1588-2019"; + + leaf clock-identity { + type clock-identity; + description + "IEEE Std 1588 clockIdentity."; + } + + leaf port-number { + type uint16; + description + "IEEE Std 1588 portNumber. + If portNumber is unavailable, the value 0 can + be used, or this leaf can be omitted from the + operational datastore."; + reference + "7.5.2.3 of IEEE Std 1588-2019"; + } + } + + grouping port-address { + description + "The IEEE Std 1588 PortAddress type represents the + protocol address of a PTP Port."; + reference + "5.3.6 of IEEE Std 1588-2019"; + + leaf network-protocol { + type identityref { + base network-protocol; + } + description + "Protocol used by a PTP Instance to transport + PTP messages."; + } + + leaf address-length { + type uint16; + description + "Number of octets in address-field."; + } + + leaf address-field { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2})*"; + } + description + "The protocol address of a PTP Port in the format + defined by the mapping annex of the protocol as + identified by the network-protocol leaf. + The most significant octet of the address-field + is mapped into the octet of the address-field + member with index 0. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + } + } + + grouping clock-quality { + description + "Quality of a PTP Instance, which contains IEEE Std 1588 + clockClass, clockAccuracy and offsetScaledLogVariance. + PTP Instances with better quality are more likely to + become the Grandmaster PTP Instance."; + reference + "5.3.7 of IEEE Std 1588-2019 + 8.2.1.3.1 of IEEE Std 1588-2019"; + + leaf clock-class { + type identityref { + base clock-class; + } + description + "The clockClass denotes the traceability of the time + or frequency distributed by the clock."; + reference + "7.6.2.5 of IEEE Std 1588-2019 + 8.2.1.3.1.2 of IEEE Std 1588-2019"; + } + + leaf clock-accuracy { + type identityref { + base clock-accuracy; + } + description + "The clockAccuracy indicates the accuracy of the clock + (Local Clock of the PTP Instance)."; + reference + "7.6.2.6 of IEEE Std 1588-2019 + 8.2.1.3.1.3 of IEEE Std 1588-2019"; + } + + leaf offset-scaled-log-variance { + type uint16; + description + "The offsetScaledLogVariance indicates the stability of the + clock (Local Clock of the PTP Instance). It provides an + estimate of the variations of the clock from a linear timescale + when it is not synchronized to another clock using the + protocol."; + reference + "7.6.2.7 of IEEE Std 1588-2019"; + } + } + + grouping fault-record { + description + "Record of a fault in the PTP Instance. + + NOTE - IEEE Std 1588 specifies a member + faultRecordLength for this type, which is needed + for PTP Management Messages, but is not needed for + YANG management."; + reference + "5.3.10 of IEEE Std 1588-2019"; + + container time { + description + "Time the fault occurred as indicated by the Timestamping + Clock of the PTP Instance. A value of all 1's for the + fields in the timestamp shall indicate that the occurrence + time is not available."; + uses timestamp; + } + + leaf severity { + type fault-severity; + description + "Severity of the fault."; + } + + leaf name { + type string; + description + "Name for the fault, unique within the implementation."; + } + + leaf value { + type string; + description + "Any value that may be associated with the fault that is + necessary for fault diagnosis."; + } + + leaf description { + type string; + description + "Any supplementary description of the fault."; + } + } + + grouping communication-capabilities { + description + "Multicast/unicast capabilities for a port + and message type. + These attributes report the values that are transmitted + by this PTP Instance to other PTP Instance(s) in the + network to indicate the multicast/unicast capabilities + for a port and message type. Therefore, the context is + protocol communication, and not YANG configuration."; + reference + "5.3.12 of IEEE Std 1588-2019 + 8.2.25 of IEEE Std 1588-2019 + 16.9.2 of IEEE Std 1588-2019"; + + leaf multicast-capable { + type boolean; + description + "True if the PTP Port is capable of transmitting + PTP messages using multicast communication, + otherwise it shall be false."; + } + + leaf unicast-capable { + type boolean; + description + "True if the PTP Port is capable of transmitting + PTP messages using unicast communication, + otherwise it shall be false."; + } + + leaf unicast-negotiation-capable { + type boolean; + description + "True if the PTP Port is capable negotiating unicast + communication using the unicast negotiation feature, + and unicast-negotiation-port-ds/enable is true, + otherwise the value of shall be false."; + } + + leaf unicast-negotiation-required { + type boolean; + description + "True if the value of unicast-negotiation-capable is true + and the use of the unicast negotiation feature is + required by the implementation, otherwise the value + shall be false."; + } + } + + grouping ptp-instance-performance-parameters { + description + "PTP Instance Performance Monitoring Parameters, + related to the PTP Port or Link Port in the + time-receiver state."; + reference + "Table J.1 of IEEE Std 1588-2019"; + + leaf average-time-transmitter-time-receiver-delay { + type time-interval; + description + "Average of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf minimum-time-transmitter-time-receiver-delay { + type time-interval; + description + "Minimum of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf maximum-time-transmitter-time-receiver-delay { + type time-interval; + description + "Maximum of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf stddev-time-transmitter-time-receiver-delay { + type time-interval; + description + "StdDev of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf average-time-receiver-time-transmitter-delay { + type time-interval; + description + "Average of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf minimum-time-receiver-time-transmitter-delay { + type time-interval; + description + "Minimum of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf maximum-time-receiver-time-transmitter-delay { + type time-interval; + description + "Maximum of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf stddev-time-receiver-time-transmitter-delay { + type time-interval; + description + "StdDev of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf average-mean-path-delay { + type time-interval; + description + "Average of the this interval."; + } + leaf minimum-mean-path-delay { + type time-interval; + description + "Minimum of the for this interval."; + } + leaf maximum-mean-path-delay { + type time-interval; + description + "Maximum of the for this interval."; + } + leaf stddev-mean-path-delay { + type time-interval; + description + "StdDev of the for this interval."; + } + leaf average-offset-from-time-transmitter { + type time-interval; + description + "Average of the for this + interval."; + } + leaf minimum-offset-from-time-transmitter { + type time-interval; + description + "Minimum of the for this + interval."; + } + leaf maximum-offset-from-time-transmitter { + type time-interval; + description + "Maximum of the for this + interval."; + } + leaf stddev-offset-from-time-transmitter { + type time-interval; + description + "StdDev of the for this + interval."; + } + } + + grouping ptp-port-performance-parameters-peer-delay { + description + "PTP Port Performance Monitoring Parameters, + related to the PTP Port or Link Port using the + peer-to-peer delay mechanism."; + reference + "Table J.2 of IEEE Std 1588-2019"; + + leaf average-mean-link-delay { + type time-interval; + description + "Average of the for this interval."; + } + leaf min-mean-link-delay { + type time-interval; + description + "Minimum of the for this interval."; + } + leaf max-mean-link-delay { + type time-interval; + description + "Maximum of the for this interval."; + } + leaf stddev-mean-link-delay { + type time-interval; + description + "StdDev of the for this interval."; + } + } + + grouping additional-performance-parameters { + description + "Additional Performance Monitoring Parameters, + intended to complement ptp-instance-performance-parameters."; + reference + "Table J.3 of IEEE Std 1588-2019"; + + leaf announce-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Announce + messages that have been transmitted for this + interval."; + } + leaf announce-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Announce + messages from the current GM that have been + received for this interval."; + } + leaf announce-foreign-rx { + type yang:zero-based-counter32; + description + "Counter indicating the total number of Announce + messages from the foreign TimeTransmitters that have been + received for this interval."; + } + leaf sync-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Sync + messages that have been transmitted for this + interval."; + } + leaf sync-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Sync + messages that have been received for this + interval."; + } + leaf follow-up-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Follow_Up + messages that have been transmitted for this + interval."; + } + leaf follow-up-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Follow_Up + messages that have been received for this + interval."; + } + leaf delay-req-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Req + messages that have been transmitted for this + interval."; + } + leaf delay-req-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Req + messages that have been received for this + interval."; + } + leaf delay-resp-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Resp + messages that have been transmitted for this + interval."; + } + leaf delay-resp-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Resp + messages that have been received for this + interval."; + } + leaf pdelay-req-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Req + messages that have been transmitted for this + interval."; + } + leaf pdelay-req-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Req + messages that have been received for this + interval."; + } + leaf pdelay-resp-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Resp + messages that have been transmitted for this + interval."; + } + leaf pdelay-resp-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Resp + messages that have been received for this + interval."; + } + leaf pdelay-resp-follow-up-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of + Pdelay_Resp_Follow_Up messages that have + been transmitted for this interval."; + } + leaf pdelay-resp-follow-up-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of + Pdelay_Resp_Follow_Up messages that have + been transmitted for this interval."; + } + } + + grouping clock-performance-monitoring-data-record { + description + "The IEEE Std 1588 ClockPerformanceMonitoringDataRecord + type is used for PTP Instance performance monitoring + statistics."; + reference + "Table J.4.1 of IEEE Std 1588-2019"; + + leaf index { + type uint16; + description + "Index to each record in the list (0-99)."; + } + + leaf measurement-valid { + type boolean; + description + "The measurement-valid flag shall indicate the data + can be correctly interpreted. Validity is + implementation specific and may be defined in + a PTP Profile. If for some periods the data is not + valid for part of the data collection interval + (e.g. the clock is not locked), a specific + implementation can report the statistics only for + valid data and with measurement-valid true. + + This flag applies to all parameters for a + given measurement period, including PTP Port + and Link Port related."; + } + + leaf period-complete { + type boolean; + description + "The period-complete flag shall indicate that + measurements were performed during the entire + period (15-minute or 24-hour). For example, + if the PTP Instance is disabled for five minutes + of a 15-minute period, period-complete is false. + The period-complete flag is not related to the + validity of measurements that were performed. + + This flag applies to all parameters for a + given measurement period, including PTP Port + and Link Port related."; + } + + leaf pm-time { + type yang:timestamp; + description + "Time of the beginning of the measurement record. + This leaf's type is YANG timestamp, which is based + on system time (also known as local time). System + time is an unsigned integer in units of + 10 milliseconds, using an epoch defined by the + implementation (typically time of boot-up)."; + reference + "IETF RFC 6991"; + } + + uses ptp-instance-performance-parameters; + } + + grouping port-performance-monitoring-peer-delay-data-record { + description + "The IEEE Std 1588 PortPerformanceMonitoringPeerDelayDataRecord + type is used for the PTP Port related performance monitoring + statistics for the peer-to-peer delay measurement mechanism."; + reference + "Table J.4.1 of IEEE Std 1588-2019"; + + leaf index { + type uint16; + description + "Index to each record in the list (0-99)."; + } + + leaf pm-time { + type yang:timestamp; + description + "Time of the beginning of the measurement record. + This leaf's type is YANG timestamp, which is based + on system time (also known as local time). System + time is an unsigned integer in units of + 10 milliseconds, using an epoch defined by the + implementation (typically time of boot-up)."; + reference + "RFC 6991"; + } + + uses ptp-port-performance-parameters-peer-delay; + } + + grouping port-performance-monitoring-data-record { + description + "The IEEE Std 1588 PortPerformanceMonitoringDataRecord + type is used for additional PTP Port related performance + monitoring statistics."; + reference + "Table J.4.1 of IEEE Std 1588-2019"; + + leaf index { + type uint16; + description + "Index to each record in the list (0-99)."; + } + + leaf pm-time { + type yang:timestamp; + description + "Time of the beginning of the measurement record. + This leaf's type is YANG timestamp, which is based + on system time (also known as local time). System + time is an unsigned integer in units of + 10 milliseconds, using an epoch defined by the + implementation (typically time of boot-up)."; + reference + "RFC 6991"; + } + + uses additional-performance-parameters; + } + + container ptp { + description + "Contains all YANG nodes for the PTP data sets. + This hierarchy can be augmented with YANG nodes + for a specific vendor or PTP Profile."; + + container instances { + description + "YANG container that is used to get all PTP Instances. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list instance { + + key "instance-index"; + + description + "List of one or more PTP Instances in the product (PTP Node). + Each PTP Instance represents a distinct instance of PTP + implementation (i.e. distinct Ordinary Clock, Boundary Clock, + or Transparent Clock), maintaining a distinct time. + PTP Instances may be created or deleted dynamically in + implementations that support dynamic create/delete."; + reference + "8.1.4.2 of IEEE Std 1588-2019"; + + leaf instance-index { + type uint32; + description + "The instance list is indexed using a number that is + unique per PTP Instance within the PTP Node, applicable + to the management context only (i.e. not used in PTP + messages). The domain-number of the PTP Instance is not + used as the key to instance-list, since it is possible + for a PTP Node to contain multiple PTP Instances using + the same domain-number."; + reference + "8.1.4.2 of IEEE Std 1588-2019"; + } + + container default-ds { + description + "The default data set of the PTP Instance."; + reference + "8.2.1 of IEEE Std 1588-2019"; + + leaf two-step-flag { + type boolean; + config false; + status deprecated; + description + "When set to true, the PTP Instance is two-step, + otherwise the PTP Instance is one-step. + This data set member is no longer used. However, + the twoStepFlag of the PTP common header is used. + One step or two step egress behavior is allowed to + be specified per PTP Port, or per PTP Instance. + Management of the one/two step egress behavior of + a PTP Port is not provided by this standard, but + can be specified as extensions to the data sets by a + PTP Profile or a product specification."; + reference + "8.2.1.2.1 of IEEE Std 1588-2019"; + } + + leaf clock-identity { + type clock-identity; + config false; + description + "The IEEE Std 1588 clockIdentity of the PTP Instance."; + reference + "8.2.1.2.2 of IEEE Std 1588-2019"; + } + + leaf number-ports { + type uint16; + config false; + description + "The number of PTP Ports on the PTP Instance. + For an Ordinary Clock, the value shall be one."; + reference + "8.2.1.2.3 of IEEE Std 1588-2019"; + } + + container clock-quality { + description + "The IEEE Std 1588 clockQuality of the PTP Instance. + PTP Instances with better quality are more likely to + become the Grandmaster PTP Instance."; + reference + "8.2.1.3.1 of IEEE Std 1588-2019"; + uses clock-quality; + } + + leaf priority1 { + type uint8; + description + "The IEEE Std 1588 priority1 of the PTP Instance. + Since priority1 is one of the first comparisons + performed by the Best TimeTransmitter Clock Algorithm + (BTCA), this leaf's configuration can be used to + explicitly select a Grandmaster PTP Instance. + Lower values take precedence. + The value of priority1 shall be configurable to any + value in the range 0 to 255, unless restricted by + limits established by the applicable PTP Profile."; + reference + "7.6.2.3 of IEEE Std 1588-2019 + 8.2.1.4.1 of IEEE Std 1588-2019"; + } + + leaf priority2 { + type uint8; + description + "The IEEE Std 1588 priority2 of the PTP Instance. + The priority2 member is compared by the + Best TimeTransmitter Clock Algorithm (BTCA) after + priority1 and clockQuality. + Lower values take precedence. + The value of priority2 shall be configurable to any + value in the range 0 to 255, unless restricted by + limits established by the applicable PTP Profile."; + reference + "7.6.2.4 of IEEE Std 1588-2019 + 8.2.1.4.2 of IEEE Std 1588-2019"; + } + + leaf domain-number { + type uint8; + description + "The IEEE Std 1588 domainNumber of the PTP Instance. + A domain consists of one or more PTP Instances + communicating with each other as defined by the + protocol. A domain shall define the scope of PTP message + communication, state, operations, data sets, and + timescale. Therefore, each domain represents a distinct + time. + Within a PTP Network, a domain is identified by two + data set members: domainNumber and sdoId. + The domainNumber is the primary mechanism for end users + and system integrators to isolate the operation of a + PTP Instance from PTP messages used in other domains. + The value of the domainNumber shall be configurable + to values permitted in IEEE Std 1588, unless the + allowed values are further restricted by the applicable + PTP Profile."; + reference + "7.1 of IEEE Std 1588-2019 + 8.2.1.4.3 of IEEE Std 1588-2019"; + } + + leaf time-receiver-only { + type boolean; + description + "The value of time-receiver-only shall be true if the + PTP Instance is a time-receiver-only PTP Instance + (false for not time-receiver-only). + The time-receiver-only member can be true for + Ordinary Clocks only. + When time-receiver-only is true, the PTP Instance + implements special behavior in the context of the state + machines that determine port-state."; + reference + "8.2.1.4.4 of IEEE Std 1588-2019 + 9.2.2.1 of IEEE Std 1588-2019"; + } + + leaf sdo-id { + type uint16 { + range "0..4095"; + } + description + "The IEEE Std 1588 sdoId of the PTP Instance. + A domain consists of one or more PTP Instances + communicating with each other as defined by the + protocol. A domain shall define the scope of PTP message + communication, state, operations, data sets, and + timescale. Therefore, each domain represents a distinct + time. + Within a PTP Network, a domain is identified by two + data set members: domainNumber and sdoId. + The sdoId of a domain is a 12-bit integer in the + closed range 0 to 4095. + The sdoId member is the primary mechanism for providing + isolation of PTP Instances operating a PTP Profile + specified by a Standards Development Organization (SDO), + from other PTP Instances operating a PTP Profile + specified by a different SDO."; + reference + "7.1 of IEEE Std 1588-2019 + 8.2.1.4.5 of IEEE Std 1588-2019 + 16.5 of IEEE Std 1588-2019"; + } + + container current-time { + description + "For management read, this member shall return the + current value of the PTP Instance Time. + When management write is supported, this member + shall set the PTP Instance Time. + Time originates in the Grandmaster PTP Instance and + is distributed by PTP to other PTP Instances in + the domain. + NOTE 1 - The time in the Grandmaster PTP Instance + is normally determined by interacting with a primary + reference, e.g., GPS, by means outside the scope of + this standard. + NOTE 2 - When this member is used to set time in a + PTP Instance other than the Grandmaster PTP Instance, + the PTP Node can return a management error. + NOTE 3 - If the time is set in a PTP Instance other + than the Grandmaster PTP Instance, it will be + overwritten by the operation of the protocol and will + therefore exist only as a transient."; + reference + "8.2.1.5.1 of IEEE Std 1588-2019"; + uses timestamp; + } + + leaf instance-enable { + type boolean; + description + "Indicates if the PTP Instance is enabled for + PTP operation. + When management write is supported: + - Write of the value true shall cause the PTP Instance + to initialize, only if the value was previously false. + - Write of the value false shall immediately disable + operation of the PTP Instance (i.e. analogous to power + off). + If this leaf is not supported, the PTP Instance shall be + specified-by-design to be enabled (true)."; + reference + "8.2.1.5.2 of IEEE Std 1588-2019"; + } + + leaf external-port-config-enable { + if-feature external-port-config; + type boolean; + description + "This value determines whether the external port + configuration option is in the disabled state (false) + or enabled state (true). + When this value is false, each PTP Port's state + is determined by PTP state machines, including + the Best TimeTransmitter Clock Algorithm (BTCA). + When this value is true, each PTP Port's state + is configured externally, and PTP state machines + are effectively disabled. External configuration + of PTP Port state can be accomplished using the + desiredState member of the port (i.e., + ../ports/port[]/external-port-config-port-ds/ + desired-state)."; + reference + "8.2.1.5.3 of IEEE Std 1588-2019 + 17.6 of IEEE Std 1588-2019"; + } + + leaf max-steps-removed { + type uint8 { + range "2..255"; + } + description + "If the value of stepsRemoved of an Announce message + is greater than or equal to the value of this + max-steps-removed leaf, the Announce message is not + considered in the operation of the + Best TimeTransmitter Clock Algorithm (BTCA). + The value shall be in the closed range 2 to 255. + If the leaf is not supported, the value used shall + be 255."; + reference + "8.2.1.5.4 of IEEE Std 1588-2019 + 9.3.2.5 of IEEE Std 1588-2019"; + } + + leaf instance-type { + type instance-type; + description + "The type of PTP Instance. + This leaf is read-only unless support for write is + explicitly specified by the applicable PTP Profile or + product specification."; + reference + "8.2.1.5.5 of IEEE Std 1588-2019"; + } + } + + container current-ds { + description + "Provides current data from operation + of the protocol."; + reference + "8.2.2 of IEEE Std 1588-2019"; + + leaf steps-removed { + type uint16; + config false; + description + "The number of PTP Communication Paths traversed + between this PTP Instance and the Grandmaster + PTP Instance."; + reference + "8.2.2.2 of IEEE Std 1588-2019"; + } + + leaf offset-from-time-transmitter { + type time-interval; + config false; + description + "The current value of the time difference between + a TimeTransmitter PTP Instance and a + TimeReceiver PTP Instance as computed by the + TimeReceiver PTP Instance. + NOTE - When a PTP Profile requires a Boundary + Clock to transfer offset information internally + from TimeReceiver PTP Port to TimeTransmitter + PTP Port(s), this value effectively returns the offset + from the Grandmaster PTP Instance."; + reference + "8.2.2.3 of IEEE Std 1588-2019"; + } + + leaf mean-delay { + type time-interval; + config false; + description + "The current value of the mean propagation time between + a TimeTransmitter PTP Instance and a + TimeReceiver PTP Instance as computed by the + TimeReceiver PTP Instance. + If the PTP Instance has no PTP Port in time-receiver or + uncalibrated state, this returns zero. + Otherwise, the TimeReceiver PTP Port returns this value + depending on its delay-mechanism: + e2e: mean propagation time over the + PTP Communication Path, i.e. + p2p or common-p2p: mean propagation time over the + PTP Link, i.e. + disabled or special: zero"; + reference + "7.4.2 of IEEE Std 1588-2019 + 8.2.2.4 of IEEE Std 1588-2019"; + } + + leaf mean-path-delay { + type time-interval; + config false; + status deprecated; + description + "In IEEE Std 1588-2008, currentDS.meanDelay was called + currentDS.meanPathDelay. While the specification of + this member is retained in the current standard, the + member is renamed to currentDS.meanDelay. This change + is consistent with other changes that ensure clarity + and consistency of naming, where + - 'path' is associated with the + request-response mechanism + - 'link' is associated with the + peer-to-peer delay mechanism"; + reference + "8.2.2.4 of IEEE Std 1588-2008"; + } + + leaf synchronization-uncertain { + type boolean; + config false; + description + "This boolean is true when synchronization is + uncertain (e.g., not within specification) + in either the Parent PTP Port or this + PTP Instance. The value is copied from a + received Announce message to transmitted Announce + message, such that it reflects uncertain + synchronization from this PTP Instance to the + Grandmaster. Performance metrics for determining + uncertainty are specified by the applicable + PTP Profile."; + reference + "8.2.2.5 of IEEE Std 1588-2019"; + } + } + + container parent-ds { + description + "Provides data learned from the parent of this + PTP Instance (i.e. time-transmitter port on the other + side of the path/link)."; + reference + "8.2.3 of IEEE Std 1588-2019"; + + container parent-port-identity { + config false; + description + "The IEEE Std 1588 portIdentity of the PTP Port on the + TimeTransmitter PTP Instance that issues the Sync + messages used in synchronizing this PTP Instance."; + reference + "8.2.3.2 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf parent-stats { + type boolean; + config false; + description + "When set to true, the values of + parent-ds/observed-parent-offset-scaled-log-variance + and + parent-ds/observed-parent-clock-phase-change-rate + have been measured and are valid."; + reference + "8.2.3.3 of IEEE Std 1588-2019"; + } + + leaf observed-parent-offset-scaled-log-variance { + type uint16; + config false; + description + "Estimate of the variance of the phase offset of the + Local PTP Clock of the Parent PTP Instance as measured + with respect to the Local PTP Clock in the + TimeReceiver PTP Instance. This measurement is + optional, but if not made, the value of + parent-ds/parent-stats shall be false."; + reference + "7.6.3.3 of IEEE Std 1588-2019 + 7.6.3.5 of IEEE Std 1588-2019 + 8.2.3.4 of IEEE Std 1588-2019"; + } + + leaf observed-parent-clock-phase-change-rate { + type int32; + config false; + description + "Estimate of the phase change rate of the + Local PTP Clock of the Parent PTP Instance as measured + by the TimeReceiver PTP Instance using its + Local PTP Clock. + If the estimate exceeds the capacity of its data type, + this value shall be set to 7FFF FFFF (base 16) or + 8000 0000 (base 16), as appropriate. A positive sign + indicates that the phase change rate in the + Parent PTP Instance is greater than that in the + TimeReceiver PTP Instance. The measurement of this + value is optional, but if not measured, the value of + parent-ds/parent-stats shall be false."; + reference + "7.6.4.4 of IEEE Std 1588-2019 + 8.2.3.5 of IEEE Std 1588-2019"; + } + + leaf grandmaster-identity { + type clock-identity; + config false; + description + "The IEEE Std 1588 clockIdentity of the Grandmaster PTP + Instance."; + reference + "8.2.3.6 of IEEE Std 1588-2019"; + } + + container grandmaster-clock-quality { + config false; + description + "The IEEE Std 1588 clockQuality of the Grandmaster PTP + Instance."; + reference + "8.2.3.7 of IEEE Std 1588-2019"; + uses clock-quality; + } + + leaf grandmaster-priority1 { + type uint8; + config false; + description + "The IEEE Std 1588 priority1 of the Grandmaster PTP + Instance."; + reference + "8.2.3.8 of IEEE Std 1588-2019"; + } + + leaf grandmaster-priority2 { + type uint8; + config false; + description + "The IEEE Std 1588 priority2 of the Grandmaster PTP + Instance."; + reference + "8.2.3.9 of IEEE Std 1588-2019"; + } + + container protocol-address { + description + "The protocol address of the PTP Port + that issues the Sync messages used in synchronizing + this PTP Instance."; + reference + "8.2.3.10 of IEEE Std 1588-2019"; + uses port-address; + } + + leaf synchronization-uncertain { + type boolean; + config false; + description + "This boolean is true when synchronization is + uncertain in the Parent PTP Port."; + reference + "8.2.3.11 of IEEE Std 1588-2019"; + } + } + + container time-properties-ds { + description + "Provides data learned from the current + Grandmaster PTP Instance."; + reference + "8.2.4 of IEEE Std 1588-2019"; + + leaf current-utc-offset { + when "../current-utc-offset-valid='true'"; + type int16; + description + "Specified as in IERS Bulletin C, this provides + the offset from UTC (TAI - UTC). The offset is in + units of seconds."; + reference + "7.2.4 of IEEE Std 1588-2019 + 8.2.4.2 of IEEE Std 1588-2019"; + } + + leaf current-utc-offset-valid { + type boolean; + description + "The value of current-utc-offset-valid shall be true + if the values of current-utc-offset, leap59, and leap61 + are known to be correct, otherwise it shall be false. + NOTE - The constraint for leap59 and leap61 did not + exist in IEEE Std 1588-2008, and for compatibility, + corresponding when statements were not included below."; + reference + "8.2.4.3 of IEEE Std 1588-2019"; + } + + leaf leap59 { + type boolean; + description + "If the timescale is PTP, a true value for leap59 + shall indicate that the last minute of the + current UTC day contains 59 seconds. + If the timescale is not PTP, the value shall be + false."; + reference + "8.2.4.4 of IEEE Std 1588-2019"; + } + + leaf leap61 { + type boolean; + description + "If the timescale is PTP, a true value for leap61 + shall indicate that the last minute of the + current UTC day contains 61 seconds. + If the timescale is not PTP, the value shall be + false."; + reference + "8.2.4.5 of IEEE Std 1588-2019"; + } + + leaf time-traceable { + type boolean; + description + "The value of time-traceable shall be true if the + timescale is traceable to a primary reference; + otherwise, the value shall be false. + The uncertainty specifications appropriate to the + evaluation of whether traceability to a primary + reference is achieved should be defined in the + applicable PTP Profile. In the absence of such a + definition the value of time-traceable is + implementation specific."; + reference + "8.2.4.6 of IEEE Std 1588-2019"; + } + + leaf frequency-traceable { + type boolean; + description + "The value of time-traceable shall be true if the + frequency determining the timescale is traceable + to a primary reference; otherwise, the value shall + be false. + The uncertainty specifications appropriate to the + evaluation of whether traceability to a primary + reference is achieved should be defined in the + applicable PTP Profile. In the absence of such a + definition the value of frequency-traceable is + implementation specific."; + reference + "8.2.4.7 of IEEE Std 1588-2019"; + } + + leaf ptp-timescale { + type boolean; + description + "If ptp-timescale is true, the timescale of + the Grandmaster PTP Instance is PTP, which is + the elapsed time since the PTP epoch measured + using the second defined by International Atomic + Time (TAI). + If ptp-timescale is false, the timescale of + the Grandmaster PTP Instance is ARB, which is + the elapsed time since an arbitrary epoch."; + reference + "7.2.1 of IEEE Std 1588-2019 + 8.2.4.8 of IEEE Std 1588-2019"; + } + + leaf time-source { + type identityref { + base time-source; + } + description + "The source of time used by the Grandmaster + PTP Instance."; + reference + "7.6.2.8 of IEEE Std 1588-2019 + 8.2.4.9 of IEEE Std 1588-2019"; + } + } + + container description-ds { + description + "Provides descriptive information for the PTP Instance."; + reference + "8.2.5 of IEEE Std 1588-2019"; + + leaf manufacturer-identity { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){2}"; + } + config false; + description + "3-octet OUI or CID owned by the manufacturer of the + PTP Instance, assigned by the IEEE Registration + Authority. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + reference + "8.2.5.2 of IEEE Std 1588-2019"; + } + + leaf product-description { + type string { + length "2..64"; + } + config false; + description + "The product-description string shall indicate, in order: + - The name of the manufacturer of the PTP Instance, + manufacturerName, followed by a semicolon (;) + - The model number of the PTP Instance, modelNumber, + followed by a semicolon (;) + - A unique identifier of this PTP Instance, + instanceIdentifier, such as the MAC address or + the serial number. + The content and meaning of the manufacturerName, + modelNumber, and the instanceIdentifier strings are + determined by the manufacturer of the PTP Instance."; + reference + "8.2.5.3 of IEEE Std 1588-2019"; + } + + leaf product-revision { + type string { + length "2..32"; + } + config false; + description + "Indicate the revisions for PTP Instance's + hardware (HW), firmware (FW), and software (SW). + This information shall be semicolon (;) separated + text fields in the order HW;FW;SW. Non-applicable + revisions shall be indicated by a text fields of + zero length."; + reference + "8.2.5.4 of IEEE Std 1588-2019"; + } + + leaf user-description { + type string { + length "0..128"; + } + description + "Configurable description of the product's PTP Instance. + The user-description string should indicate, in order: + - A user-defined name of the PTP Instance, + e.g., Sensor-1, followed by a semicolon (;) + - A user-defined physical location of the PTP Instance, + e.g., Rack-2 Shelf-3."; + reference + "8.2.5.5 of IEEE Std 1588-2019"; + } + } + + container fault-log-ds { + if-feature fault-log; + config false; + description + "Represents an optional mechanism for logging of faults + that occur in the PTP Instance. If one member of + fault-log-ds is supported, all members shall be + supported."; + reference + "8.2.6 of IEEE Std 1588-2019"; + + leaf number-of-fault-records { + type uint16; + config false; + description + "The number of fault records available in + fault-record-list."; + reference + "8.2.6.2 of IEEE Std 1588-2019"; + } + + list fault-record-list { + config false; + description + "List of fault records, number-of-fault-records + in length. + The maximum length of fault-record-list is + implementation-specific. The fault-record-list + is maintained by the PTP Instance until + fault-log-ds.reset is used."; + reference + "8.2.6.3 of IEEE Std 1588-2019"; + + uses fault-record; + } + + action reset { + description + "This action causes the contents of fault-record-list + to be cleared, and number-of-fault-records to be set + to zero."; + reference + "8.2.6.4 of IEEE Std 1588-2019"; + } + } + + // The nonvolatileStorageDS in 8.2.7 of IEEE Std 1588-2019 + // is not applicable for YANG, since protocols like NETCONF + // and RESTCONF specify analogous features for configuration + // storage. + + container path-trace-ds { + if-feature path-trace; + description + "Provides data for the optional path + trace mechanism."; + reference + "16.2 of IEEE Std 1588-2019"; + + leaf-list list { + type clock-identity; + config false; + description + "List of IEEE Std 1588 clock identity values + (type ClockIdentity), in the order provided in the + PATH_TRACE TLV."; + reference + "16.2.2.2.1 of IEEE Std 1588-2019"; + } + + leaf enable { + type boolean; + description + "Allows for enable/disable of the path trace mechanism + using management. If path-trace-ds.enable is true, + the path trace mechanism shall be operational. + If path-trace-ds.enable is false, the path trace + mechanism shall be inactive."; + reference + "16.2.2.3.1 of IEEE Std 1588-2019"; + } + } + + container alternate-timescale-ds { + if-feature alternate-timescale; + description + "Provides data for the optional alternate + timescale offsets mechanism."; + reference + "16.3 of IEEE Std 1588-2019"; + + leaf max-key { + type uint8; + config false; + description + "The value of max-key shall indicate the value of + the largest key-field in the list."; + reference + "16.3.4.3.1 of IEEE Std 1588-2019"; + } + + list list { + key "key-field"; + description + "List of alternate timescales in the PTP Instance. + Elements in the list can be created or deleted, if + those operations are supported by management. + + If management write is supported for items + current-offset, jump-seconds, and time-of-next-jump, + the value for all three items shall be provided + within a single write operation, and the update of + all three items shall be atomic. If any of the three + values fails to update, a management error shall be + returned."; + reference + "16.3.4.4.1 of IEEE Std 1588-2019"; + + leaf key-field { + type uint8; + description + "Unique identifier of each element in the list."; + } + + leaf enable { + type boolean; + description + "If enable is true, the + ALTERNATE_TIME_OFFSET_INDICATOR TLV + for this alternate timescale shall be attached + to Announce messages. If enable is false, the TLV + shall not be attached."; + } + + leaf current-offset { + type int32; + description + "Offset of the alternate time, in seconds, from + PTP Instance Time in the Grandmaster PTP Instance."; + } + + leaf jump-seconds { + type int32; + description + "Size of the next discontinuity, in seconds, in the + alternate timescale. A value of zero indicates that + no discontinuity is expected. A positive value + indicates that the discontinuity will cause the + current-offset of the alternate timescale to + increase."; + } + + leaf time-of-next-jump { + type uint64; + description + "Value of the seconds-field of the transmitting PTP + Instance Time at the time that the next discontinuity + will occur. The discontinuity occurs at the start of + the second indicated by the value of time-of-next-jump. + Only 48-bits are valid (the upper 16-bits are always + zero)."; + } + + leaf display-name { + type string { + length "0..10"; + } + description + "Textual description of the alternate timescale."; + } + } + } + + container holdover-upgrade-ds { + if-feature holdover-upgrade; + description + "Provides data for the optional holdover + upgrade mechanism."; + reference + "16.4 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "Used to enable (true) or disable (false) the + holdover upgrade mechanism."; + } + } + + container grandmaster-cluster-ds { + if-feature grandmaster-cluster; + description + "Provides data for the optional grandmaster + cluster mechanism."; + reference + "17.2.3 of IEEE Std 1588-2019"; + + leaf max-table-size { + type uint8; + config false; + description + "Maximum number of elements permitted + in the port-address list. + + NOTE - The actualTableSize of IEEE Std 1588 is not + applicable for YANG, since YANG mechanisms can be used + to control the number of elements in port-address."; + } + + leaf log-query-interval { + type int8; + description + "Logarithm to the base 2 of the mean interval in + seconds between unicast Announce messages from + cluster members."; + } + + list port-address { + key "index"; + description + "List of port addresses, one for each member of the + grandmaster cluster."; + + leaf index { + type uint16; + description + "Index to a port address in the list, typically + sequential from 0 to N-1, where N is the number of + port addresses."; + } + + uses port-address; + } + } + + container acceptable-time-transmitter-ds { + if-feature acceptable-time-transmitter; + description + "Provides data for the optional acceptable + timeTransmitter table mechanism."; + reference + "17.5.3 of IEEE Std 1588-2019"; + + leaf max-table-size { + type uint16; + config false; + description + "Maximum number of elements permitted + in the list. + + NOTE - The actualTableSize of IEEE Std 1588 is not + applicable for YANG, since YANG mechanisms can be used + to control the number of elements in list."; + reference + "17.5.3.3.1 of IEEE Std 1588-2019"; + } + + list list { + key "index"; + description + "List of acceptable timeTransmitters in the + PTP Instance. Elements in the list can be created or + deleted, if those operations are supported by + management. + + If management write is supported for items + acceptable-clock-identity, acceptable-port-number, + and alternate-priority1, the value for all three items + shall be provided within a single write operation, + and the update of all three items shall be atomic. + If any of the three values fails to update, a management + error shall be returned."; + reference + "17.5.3.4.2 of IEEE Std 1588-2019"; + + leaf index { + type uint8; + description + "Unique index to each element in the list, typically + sequential from 0 to N-1, where N is the number of + elements."; + } + + container acceptable-port-identity { + description + "The IEEE Std 1588 portIdentity of the + acceptable timeTransmitter."; + uses port-identity; + } + + leaf alternate-priority1 { + type uint8; + description + "The IEEE Std 1588 priority1 used as an alternate + for the acceptable timeTransmitter."; + } + } + } + + container performance-monitoring-ds { + if-feature performance-monitoring; + description + "Provides data for the optional performance + monitoring mechanism, scoped to the PTP Instance."; + reference + "8.2.13 of IEEE Std 1588-2019 + J.5.1 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "Permits management control over the collection of + performance monitoring data, including + performance-monitoring-ds (PTP Instance), + ports/port[]/performance-monitoring-port-ds + (PTP Port of PTP Instance), and + common-services/cmlds/ports/port[]/ + performance-monitoring-port-ds (CMLDS Link Port + associated with enabled PTP Port)."; + reference + "J.5.1.1 of IEEE Std 1588-2019"; + } + + list record-list { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + PTP Instance. The list is organized as follows: + - 97 15-minute measurement records, the current record + at index 0, followed by the most recent 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for performance-monitoring-ds/ + record-list[6] returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still 97 and 98. + + If a specific parameter + (e.g. max-time-transmitter-time-receiver-delay) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + one in all bits, except the most significant. This + represents the largest positive value of + time-interval, indicating a value outside the + maximum range."; + reference + "J.5.1.2 of IEEE Std 1588-2019"; + + uses clock-performance-monitoring-data-record; + } + } + + container enhanced-metrics-ds { + if-feature enhanced-metrics; + description + "Provides data for the optional enhanced + synchronization accuracy metrics mechanism."; + reference + "16.12 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "If the Enhanced Synchronization Accuracy Metrics feature + is implemented, the value true shall indicate that + the feature is enabled on the PTP Instance, and the + value false shall indicate that the option is disabled + on the PTP Instance."; + reference + "8.2.14.2 of IEEE Std 1588-2019"; + } + } + + container ports { + description + "YANG container that is used to get all PTP Ports + in the PTP Instance. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list port { + key "port-index"; + description + "List of data for each PTP Port in the PTP Instance. + While the PTP Instance is disabled, it is possible to + have zero PTP Ports (i.e., ports not yet created). + While the PTP Instance is enabled, an Ordinary Clock + will have one PTP Port, and a Boundary Clock or + Transparent Clock will have more than one PTP Port."; + reference + "8.1.4.2 of IEEE Std 1588-2019"; + + leaf port-index { + type uint16; + description + "The port list is indexed using a number that is + unique per PTP Port within the PTP Instance, + applicable to the management context only + (i.e., not used in PTP messages)."; + } + + leaf underlying-interface { + type if:interface-ref; + description + "Reference to the configured underlying IETF YANG + interface that is used by this PTP Port for + transport of PTP messages. Among other data, + physical identifiers for the interface + (e.g., MAC address) can be obtained using this + reference."; + reference + "RFC 8343"; + } + + container port-ds { + description + "Primary data set for the PTP Port."; + reference + "8.2.15 of IEEE Std 1588-2019"; + + container port-identity { + config false; + description + "The IEEE Std 1588 portIdentity of this PTP Port."; + reference + "8.2.15.2.1 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf port-state { + type port-state; + config false; + description + "Current state of the protocol engine associated + with this PTP Port."; + reference + "8.2.15.3.1 of IEEE Std 1588-2019"; + } + + leaf log-min-delay-req-interval { + type int8; + description + "Logarithm to the base 2 of the IEEE Std 1588 + minDelayReqInterval, the minimum permitted + mean time interval between successive Delay_Req + messages sent by a TimeReceiver PTP Instance."; + reference + "7.7.2.4 of IEEE Std 1588-2019 + 8.2.15.3.2 of IEEE Std 1588-2019"; + } + + leaf mean-link-delay { + type time-interval; + config false; + description + "If the value of the delay-mechanism leaf is p2p + this value shall be an estimate of the current + one-way propagation delay on the PTP Link attached + to this PTP Port, computed using the peer-to-peer + delay mechanism. + If the value of the delay-mechanism leaf is + common-p2p, this value shall be equal to the value of + ptp/common-services/cmlds/ports/port[]/port-ds/ + mean-link-delay. + If the value of the delay-mechanism leaf is e2e, + disabled, or special, this value shall be zero."; + reference + "8.2.15.3.3 of IEEE Std 1588-2019"; + } + + leaf peer-mean-path-delay { + type time-interval; + config false; + status deprecated; + description + "In IEEE Std 1588-2008, this data set member was + called portDS.peerMeanPathDelay. While the + specification of this member is retained in the + current standard, the member is renamed to + portDS.meanLinkDelay (i.e., ../mean-link-delay). + This change is consistent with other changes that + ensure clarity and consistency of naming, where + - 'path' is associated with the + request-response mechanism + - 'link' is associated with the + peer-to-peer delay mechanism"; + reference + "8.2.5.3.3 of IEEE Std 1588-2008"; + } + + leaf log-announce-interval { + type int8; + description + "Logarithm to the base 2 of the mean IEEE Std 1588 + announceInterval, the time interval between + successive Announce messages sent by a PTP Port."; + reference + "7.7.2.2 of IEEE Std 1588-2019 + 8.2.15.4.1 of IEEE Std 1588-2019"; + } + + leaf announce-receipt-timeout { + type uint8; + description + "The integral multiple of IEEE Std 1588 + announceInterval that must pass without receipt of + an Announce message before the occurrence of the + event ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES. The range + shall be 2 to 255 subject to further restrictions of + the applicable PTP Profile. While 2 is permissible, + normally the value should be at least 3."; + reference + "7.7.3.1 of IEEE Std 1588-2019 + 8.2.15.4.2 of IEEE Std 1588-2019"; + } + + leaf log-sync-interval { + type int8; + description + "Logarithm to the base 2 of the mean IEEE Std 1588 + syncInterval, the time interval between successive + Sync messages, when transmitted as multicast + messages. The rates for unicast transmissions are + negotiated separately on a per PTP Port basis and + are not constrained by this leaf."; + reference + "7.7.2.3 of IEEE Std 1588-2019 + 8.2.15.4.3 of IEEE Std 1588-2019"; + } + + leaf delay-mechanism { + type delay-mechanism; + description + "The path delay measuring mechanism used by the PTP + Port in computing (propagation delay)."; + reference + "8.2.15.4.4 of IEEE Std 1588-2019"; + } + + leaf log-min-pdelay-req-interval { + type int8; + description + "Logarithm to the base 2 of the IEEE Std 1588 + minPdelayReqInterval, the minimum permitted + mean time interval between successive Pdelay_Req + messages sent over a PTP Link."; + reference + "7.7.2.5 of IEEE Std 1588-2019 + 8.2.15.4.5 of IEEE Std 1588-2019"; + } + + leaf version-number { + type uint8; + description + "The PTP major version in use on the PTP Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "8.2.15.4.6 of IEEE Std 1588-2019"; + } + + leaf minor-version-number { + type uint8; + description + "The PTP minor version in use on the PTP Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "8.2.15.4.7 of IEEE Std 1588-2019"; + } + + leaf delay-asymmetry { + type time-interval; + description + "The value of IEEE Std 1588 + applicable to the PTP Port, which is the + difference in transmission time in one direction + as compared to the opposite direction."; + reference + "7.4.2 of IEEE Std 1588-2019 + 8.2.15.4.8 of IEEE Std 1588-2019"; + } + + leaf port-enable { + type boolean; + description + "Indicates if the PTP Port is enabled for + PTP operation. + When management write is supported: + - Write of the value true causes the + DESIGNATED_ENABLED event to occur, even if the + value was previously true. + - Write of the value false causes the + DESIGNATED_DISABLED event to occur, even if the + value was previously false. + If this leaf is not supported, the PTP Port shall be + specified-by-design to be enabled (true)."; + reference + "8.2.15.5.1 of IEEE Std 1588-2019"; + } + + leaf time-transmitter-only { + type boolean; + description + "If the value of time-transmitter-only is true, + the PTP Port shall be in the IEEE Std 1588 + timeTransmitterOnly mode. + If the value is false, the PTP Port shall not be + in the timeTransmitterOnly mode. + When time-transmitter-only is true, the PTP Port + can never enter the time-receiver port-state."; + reference + "8.2.15.5.2 of IEEE Std 1588-2019 + 9.2.2.2 of IEEE Std 1588-2019"; + } + } + + container timestamp-correction-port-ds { + if-feature timestamp-correction; + description + "Provides access to the configurable correction of + timestamps provided to the PTP protocol."; + reference + "8.2.16 of IEEE Std 1588-2019 + 16.7 of IEEE Std 1588-2019"; + + leaf egress-latency { + type time-interval; + description + "Interval between the + provided for a PTP message and the time at which + the message timestamp point of the PTP message + crosses the reference plane."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.2 of IEEE Std 1588-2019"; + } + + leaf ingress-latency { + type time-interval; + description + "Interval between the time the message timestamp + point of an ingress PTP message crosses the + reference plane and the + provided for the PTP message."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.3 of IEEE Std 1588-2019"; + } + } + + container asymmetry-correction-port-ds { + if-feature asymmetry-correction; + description + "Provides access to asymmetry correction parameters + that are used to compute the value of + delayAsymmetry>."; + reference + "8.2.17 of IEEE Std 1588-2019 + 16.8 of IEEE Std 1588-2019"; + + leaf constant-asymmetry { + type time-interval; + description + "Constant asymmetry used to fine adjust the + dynamically calculated value of , + when the mechanism to calculate + or certain media is enabled."; + reference + "8.2.17.2 of IEEE Std 1588-2019"; + } + + leaf scaled-delay-coefficient { + type relative-difference; + description + "This is the ."; + reference + "8.2.17.3 of IEEE Std 1588-2019"; + } + + leaf enable { + type boolean; + description + "When this value is true, the mechanism to calculate + for certain media is enabled on + this PTP Port. When this value is false, this + mechanism is disabled on this PTP Port."; + reference + "8.2.17.4 of IEEE Std 1588-2019"; + } + } + + container description-port-ds { + description + "Provides descriptive information for the PTP Port."; + reference + "8.2.18 of IEEE Std 1588-2019"; + + leaf profile-identifier { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){5}"; + } + config false; + description + "When profile-identifier is supported, its value + shall identify the PTP Profile implemented by the + PTP Port, using the value assigned by the + organization that created the PTP Profile. + The profile identifier is six octets that identify + the PTP Profile's organization, profile within the + organization, and version. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + reference + "8.2.18.2 of IEEE Std 1588-2019 + 20.3.3 of IEEE Std 1588-2019"; + } + + container protocol-address { + config false; + description + "Protocol address which is used as the source address + by the network transport protocol for this + PTP Port."; + reference + "8.2.18.3 of IEEE Std 1588-2019"; + uses port-address; + } + } + + container unicast-negotiation-port-ds { + if-feature unicast-negotiation; + description + "Provides management access to the optional unicast + negotiation mechanism."; + reference + "16.1 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "When enable is false, the unicast negotiation + mechanism is disabled on this PTP Port. + When enable is true, the unicast negotiation + mechanism is enabled on this PTP Port."; + reference + "8.2.19.2 of IEEE Std 1588-2019"; + } + } + + container alternate-time-transmitter-port-ds { + if-feature alternate-time-transmitter; + description + "Provides management access to the optional alternate + timeTransmitter mechanism."; + reference + "17.3.3 of IEEE Std 1588-2019"; + + leaf number-of-alt-time-transmitters { + type uint8; + description + "Limits the number of PTP Ports that can + simultaneously transmit messages with the + alternate timeTransmitter flag set to TRUE."; + reference + "17.3.3.2.1 of IEEE Std 1588-2019"; + } + + leaf tx-alt-multicast-sync { + type boolean; + description + "Controls Sync transmission. If true and the + PTP Port is currently transmitting multicast + Announce messages with alternateTimeTransmitterFlag + TRUE, the PTP Port shall also transmit multicast + Sync and, if a two-step PTP Instance, + Follow_Up messages. Otherwise do not transmit + these messages."; + reference + "17.3.3.2.2 of IEEE Std 1588-2019"; + } + + leaf log-alt-multicast-sync-interval { + type int8; + description + "Logarithm to the base 2 of the mean interval + in seconds between Sync messages transmitted + under the terms of this alternate timeTransmitter + mechanism."; + reference + "17.3.3.2.3 of IEEE Std 1588-2019"; + } + } + + container unicast-discovery-port-ds { + if-feature unicast-discovery; + description + "Provides management access to the optional unicast + discovery mechanism."; + reference + "17.4.3 of IEEE Std 1588-2019"; + + leaf max-table-size { + type uint16; + config false; + description + "Maximum number of elements permitted + in the port-address list. + + NOTE - The actualTableSize of IEEE Std 1588 is not + applicable for YANG, since YANG mechanisms can be + used to control the number of elements in + port-address."; + } + + leaf log-query-interval { + type int8; + description + "Logarithm to the base 2 of the mean interval in + seconds between requests from a PTP Instance for + a unicast Announce message."; + } + + list port-address { + key "index"; + description + "List of port addresses for unicast discovery."; + + leaf index { + type uint16; + description + "Index to a port address in the list, typically + sequential from 0 to N-1, where N is the number of + port addresses."; + } + + uses port-address; + } + } + + container acceptable-time-transmitter-port-ds { + if-feature acceptable-time-transmitter; + description + "Provides management access to the optional + acceptable timeTransmitter mechanism."; + reference + "17.5.4 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "When enable is false, the acceptable + timeTransmitter table option is not used on this + PTP Port, and the normal operation of the protocol + is in effect. + When enable is true, the acceptable timeTransmitter + table option is used on this PTP Port as specified + in the standard."; + reference + "17.5.4.2.1 of IEEE Std 1588-2019"; + } + } + + container l1-sync-basic-port-ds { + if-feature l1-sync; + description + "Provides data for operation of the optional layer-1 + based synchronization performance enhancement feature. + This data is required when the feature is supported."; + reference + "8.2.23 of IEEE Std 1588-2019 + L.5 of IEEE Std 1588-2019"; + + leaf enabled { + type boolean; + description + "Specifies whether the L1Sync option is enabled + on the PTP Port. If enabled is true, then the + L1Sync message exchange is supported and enabled."; + reference + "L.4.1 of IEEE Std 1588-2019"; + } + + leaf tx-coherent-is-required { + type boolean; + description + "Specifies whether the L1Sync port is required + to be a transmit coherent port."; + reference + "L.4.2 of IEEE Std 1588-2019"; + } + + leaf rx-coherent-is-required { + type boolean; + description + "Specifies whether the L1Sync port is required + to be a receive coherent port."; + reference + "L.4.3 of IEEE Std 1588-2019"; + } + + leaf congruent-is-required { + type boolean; + description + "Specifies whether the L1Sync port is required + to be a congruent port."; + reference + "L.4.4 of IEEE Std 1588-2019"; + } + + leaf opt-params-enabled { + type boolean; + description + "Specifies whether the L1Sync port transmitting + the L1_SYNC TLV extends this TLV with optional + parameters."; + reference + "L.4.5 of IEEE Std 1588-2019"; + } + + leaf log-l1sync-interval { + type int8; + description + "Logarithm to the base 2 of the mean IEEE Std 1588 + L1SyncInterval, the time interval between successive + periodic messages sent by the L1Sync port and + carrying the L1_SYNC TLV."; + reference + "L.4.6 of IEEE Std 1588-2019"; + } + + leaf l1sync-receipt-timeout { + type uint8; + description + "The intergral number of elapsed IEEE Std 1588 + L1SyncIntervals that must pass without receipt + of the L1_SYNC TLV before the L1_SYNC TLV + reception timeout occurs."; + reference + "L.4.7 of IEEE Std 1588-2019"; + } + + leaf link-alive { + type boolean; + config false; + description + "True when a L1_SYNC TLV is received at the PTP Port + and L1Sync is enaled on the PTP Port. False when the + L1_SYNC TLV reception timeout occurs."; + reference + "L.5.3.1 of IEEE Std 1588-2019"; + } + + leaf is-tx-coherent { + type boolean; + config false; + description + "True when the L1Sync port is a transmit coherent + port."; + reference + "L.5.3.2 of IEEE Std 1588-2019"; + } + + leaf is-rx-coherent { + type boolean; + config false; + description + "True when the L1Sync port is a receive coherent + port."; + reference + "L.5.3.3 of IEEE Std 1588-2019"; + } + + leaf is-congruent { + type boolean; + config false; + description + "True when the L1Sync port is a congruent port."; + reference + "L.5.3.4 of IEEE Std 1588-2019"; + } + + leaf l1sync-state { + type l1sync-state; + config false; + description + "Current state of the L1Sync state machine associated + with this L1Sync port."; + reference + "L.5.3.5 of IEEE Std 1588-2019"; + } + + leaf peer-tx-coherent-is-required { + type boolean; + config false; + description + "Specifies whether this L1Sync port is required + to be a transmit coherent port by a peer, + as indicated in the value of the TCR field of the + most recently received L1_SYNC TLV."; + reference + "L.5.3.6 of IEEE Std 1588-2019"; + } + + leaf peer-rx-coherent-is-required { + type boolean; + config false; + description + "Specifies whether this L1Sync port is required + to be a receive coherent port by a peer, + as indicated in the value of the RCR field of the + most recently received L1_SYNC TLV."; + reference + "L.5.3.7 of IEEE Std 1588-2019"; + } + + leaf peer-congruent-is-required { + type boolean; + config false; + description + "Specifies whether this L1Sync port is required + is required to be a congruent port by a peer, + as indicated in the value of the CR field of the + most recently received L1_SYNC TLV."; + reference + "L.5.3.8 of IEEE Std 1588-2019"; + } + + leaf peer-is-tx-coherent { + type boolean; + config false; + description + "True when the peer L1Sync port is a + transmit coherent port + (as received in the L1_SYNC TLV)."; + reference + "L.5.3.9 of IEEE Std 1588-2019"; + } + + leaf peer-is-rx-coherent { + type boolean; + config false; + description + "True when the peer L1Sync port is a + receive coherent port + (as received in the L1_SYNC TLV)."; + reference + "L.5.3.10 of IEEE Std 1588-2019"; + } + + leaf peer-is-congruent { + type boolean; + config false; + description + "True when the peer L1Sync port is a + congruent port + (as received in the L1_SYNC TLV)."; + reference + "L.5.3.11 of IEEE Std 1588-2019"; + } + } + + container l1-sync-opt-params-port-ds { + if-feature l1-sync; + description + "Provides data for operation of the optional layer-1 + based synchronization performance enhancement feature. + This data is optional when the feature is supported."; + reference + "8.2.24 of IEEE Std 1588-2019 + L.8.4 of IEEE Std 1588-2019"; + + leaf timestamps-corrected-tx { + type boolean; + description + "When true, the L1Sync port shall correct the + transmitted egress timestamps with the known value + of the phase offset, as indicated in the Link + Reference Model."; + reference + "L.8.4.2.1 of IEEE Std 1588-2019"; + } + + leaf phase-offset-tx-valid { + type boolean; + config false; + description + "True if and only if the values of the transmission + phase offset parameters (phase-offset-tx + and phase-offset-tx-timestamp) are valid."; + reference + "L.8.4.3.1 of IEEE Std 1588-2019"; + } + + leaf phase-offset-tx { + type time-interval; + config false; + description + "Transmission phase offset, which is the + time difference between the significant instant + with which the passage of the message timestamp + point through the reference plane is aligned, + and the time represented by the captured + timestamp of this passage of the message."; + reference + "L.8.4.3.3 of IEEE Std 1588-2019"; + } + + container phase-offset-tx-timestamp { + config false; + description + "Transmission phase offset timestamp + for the associated transmission phase offset."; + reference + "L.8.4.3.4 of IEEE Std 1588-2019"; + + uses timestamp; + } + + leaf frequency-offset-tx-valid { + type boolean; + config false; + description + "True if and only if the values of the transmission + frequency offset parameters (frequency-offset-tx + and frequency-offset-tx-timestamp) are valid."; + reference + "L.8.4.3.2 of IEEE Std 1588-2019"; + } + + leaf frequency-offset-tx { + type time-interval; + config false; + description + "Transmission frequency offset, multiplied + by one second. Transmission frequency offset + is the known rate of change of the transmission + phase offset."; + reference + "L.8.4.3.5 of IEEE Std 1588-2019"; + } + + container frequency-offset-tx-timestamp { + config false; + description + "Transmission frequency offset timestamp + for the associated transmission frequency + offset."; + reference + "L.8.4.3.6 of IEEE Std 1588-2019"; + + uses timestamp; + } + } + + container communication-cap-port-ds { + config false; + description + "Provides data for multicast/unicast communication + capabilities."; + reference + "8.2.25 of IEEE Std 1588-2019"; + + container sync { + description + "Communication capabilities of the PTP Port with + respect to sending Sync messages."; + + uses communication-capabilities; + } + + container delay-resp { + description + "Communication capabilities of the PTP Port with + respect to sending Delay_Resp messages."; + + uses communication-capabilities; + } + } + + container performance-monitoring-port-ds { + if-feature performance-monitoring; + description + "Provides data for the optional performance + monitoring mechanism, scoped to each PTP Port."; + reference + "8.2.26 of IEEE Std 1588-2019 + J.5.2 of IEEE Std 1588-2019"; + + list record-list-peer-delay { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + PTP Port that is using the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/ + record-list-peer-delay[6] returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. min-mean-link-delay) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + one in all bits, except the most significant. This + represents the largest positive value of + time-interval, indicating a value outside the + maximum range."; + reference + "J.5.2.1 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-peer-delay-data-record; + } + + list record-list { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + PTP Port, not specific to the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/record-list[6] + returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. sync-tx) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + with the value zero, indicating that nothing was + counted. + + Each counter in the record shall be initialized to + zero at the start of a new 15-minute and + 24-hour interval."; + reference + "J.5.2.2 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-data-record; + } + } + + container common-services-port-ds { + description + "Provides management access to the common services, + scoped to each PTP Port."; + reference + "16.6.5 of IEEE Std 1588-2019"; + + leaf cmlds-link-port-port-number { + if-feature cmlds; + type uint16; + config false; + description + "Common services operate on all PTP Instances + of the PTP Node. When a common service has + port-specific behavior, it specifies a Link Port, + which represents the physical port that the service + uses to transport PTP messages. In the context of + such a common service, the PTP Port represents a + logical port. + The Common Mean Link Delay Service (CMLDS) is + port-specific, and this leaf provides the + mapping of the PTP Port of this PTP Instance + to the corresponding Link Port in CMLDS. The + Link Port is identified using an IEEE Std 1588 + portNumber. The corresponding Link Port's + portNumber is located in the hierarchy at + /ptp/common-services/cmlds/ports/port[]/port-ds/ + port-identity/port-number."; + reference + "16.6.5.1.1.1 of IEEE Std 1588-2019"; + } + } + + container external-port-config-port-ds { + if-feature external-port-config; + description + "Provides management access to the external + configuration option, scoped to each PTP Port."; + reference + "17.6.3 of IEEE Std 1588-2019"; + + leaf desired-state { + type port-state; + description + "When the value of + default-ds/external-port-config-enable is true, + this desired-state is used to externally configure + the PTP Port's state (i.e., ../../port-ds/port-state) + to a desired value."; + reference + "17.6.3.2 of IEEE Std 1588-2019"; + } + } + + container time-receiver-monitoring-port-ds { + if-feature time-receiver-monitoring; + description + "Provides management access to the optional + TimeReceiver Event Monitor service, scoped to each + PTP Port."; + reference + "16.11.6 of IEEE Std 1588-2019"; + + leaf enable { + type bits { + bit time-receiver-rx-sync-timing-data { + position 0; + description + "True activates generation of the + TIME_RECEIVER_RX_SYNC_TIMING_DATA TLV."; + } + bit time-receiver-rx-sync-computed-data { + position 1; + description + "True activates generation of the + TIME_RECEIVER_RX_SYNC_COMPUTED_DATA TLV."; + } + bit time-receiver-tx-event-timestamps { + position 2; + description + "True activates generation of the + TIME_RECEIVER_TX_EVENT_TIMESTAMPS_DATA TLV."; + } + } + description + "Each bit (boolean flag) indicates whether + the data for a corresponding timeReceiver event + monitoring TLV is computed, and whether the data + is transmitted by the timeReceiver."; + reference + "16.11.6.2 of IEEE Std 1588-2019"; + } + + leaf events-per-rx-sync-timing-tlv { + type uint8; + description + "Indicates the number of events to report per + TIME_RECEIVER_RX_SYNC_TIMING_DATA TLV."; + reference + "16.11.6.3 of IEEE Std 1588-2019"; + } + + leaf events-per-rx-sync-computed-tlv { + type uint8; + description + "Indicates the number of events to report per + TIME_RECEIVER_RX_SYNC_COMPUTED_DATA TLV."; + reference + "16.11.6.4 of IEEE Std 1588-2019"; + } + + leaf events-per-tx-timestamps-tlv { + type uint8; + description + "Indicates the number of events to report per + TIME_RECEIVER_TX_EVENT_TIMESTAMPS_DATA TLV."; + reference + "16.11.6.5 of IEEE Std 1588-2019"; + } + + leaf tx-event-type { + type uint8; + description + "Indicates the event message type selected for + the egress event monitoring. The four low-order + bits are defined to correspond to the + IEEE Std 1588 messageType field."; + reference + "16.11.6.6 of IEEE Std 1588-2019"; + } + + leaf rx-sync-timing-tlv-message-m { + type uint8; + description + "The value M, where M indicates that every Mth + event message is selected for monitoring in the + TIME_RECEIVER_RX_SYNC_TIMING_DATA TLV. + For example, if the value of M is 4, every fourth + event message is selected for monitoring in + the TLV."; + reference + "16.11.6.7 of IEEE Std 1588-2019"; + } + + leaf rx-sync-computed-tlv-message-m { + type uint8; + description + "The value M, where M indicates that every Mth + event message is selected for monitoring in the + TIME_RECEIVER_RX_SYNC_COMPUTED_DATA TLV. + For example, if the value of M is 4, every fourth + event message is selected for monitoring in + the TLV."; + reference + "16.11.6.8 of IEEE Std 1588-2019"; + } + + leaf tx-timestamps-tlv-message-m { + type uint8; + description + "The value M, where M indicates that every Mth + event message is selected for monitoring in the + TIME_RECEIVER_TX_EVENT_TIMESTAMPS_DATA TLV. + For example, if the value of M is 4, every fourth + event message is selected for monitoring in + the TLV."; + reference + "16.11.6.9 of IEEE Std 1588-2019"; + } + } + } + } + } + } + + container transparent-clock-default-ds { + status deprecated; + description + "This default data set was specified in + IEEE Std 1588-2008, and under some interpretations, + it applied to all domains, which in turn means that it + represents multiple Transparent Clocks. + In IEEE Std 1588-2019, this data set is specified as + applying to the PTP Node (all domains), but the data set is + deprecated. For new designs, the standard recommends that + Transparent Clocks use the PTP Instance data sets + (i.e., /ptp/instances/instance[]), such that each + Transparent Clock supports a single PTP Instance and + domain."; + reference + "8.3.1 of IEEE Std 1588-2019"; + + leaf clock-identity { + type clock-identity; + config false; + status deprecated; + description + "The clockIdentity of the local clock."; + reference + "8.3.2.2.1 of IEEE Std 1588-2019"; + } + + leaf number-ports { + type uint16; + config false; + status deprecated; + description + "The number of PTP Ports of the device."; + reference + "8.3.2.2.2 of IEEE Std 1588-2019"; + } + + leaf delay-mechanism { + type delay-mechanism; + status deprecated; + description + "The propagation delay measuring mechanism (e2e or p2p)."; + reference + "8.3.2.3.1 of IEEE Std 1588-2019"; + } + + leaf primary-domain { + type uint8; + status deprecated; + description + "The domainNumber of the primary syntonization domain."; + reference + "8.3.2.3.2 of IEEE Std 1588-2019"; + } + } + + container transparent-clock-ports { + status deprecated; + description + "YANG container that is used to get all ports of the + IEEE Std 1588 transparentClockPortDS. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list port { + key "port-index"; + status deprecated; + description + "This list of Transparent Clock port data sets was specified + in IEEE Std 1588-2008, and under some interpretations, + it applied to all domains, which in turn means that it + represents multiple Transparent Clocks. + In IEEE Std 1588-2019, this list is specified as + applying to the PTP Node (all domains), but the list is + deprecated. For new designs, the standard recommends that + Transparent Clocks use the PTP Instance data sets + (i.e., /ptp/instances/instance[]), such that each + Transparent Clock supports a single PTP Instance + and domain."; + reference + "8.3.1 of IEEE Std 1588-2019"; + + leaf port-index { + type uint16; + description + "The port list is indexed using a number that is + unique per port within the Transparent Clock, + applicable to the management context only + (i.e., not used in PTP messages)."; + } + + leaf underlying-interface { + type if:interface-ref; + description + "Reference to the configured underlying IETF YANG + interface that is used by this port for + transport of PTP messages. Among other data, + physical identifiers for the interface + (e.g. MAC address) can be obtained using this + reference."; + reference + "RFC 8343"; + } + + container port-ds { + description + "IEEE Std 1588 transparentClockPortDS."; + reference + "8.3.3 of IEEE Std 1588-2019"; + + container port-identity { + config false; + status deprecated; + description + "The IEEE Std 1588 portIdentity of this port."; + reference + "8.3.3.2.1 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf log-min-pdelay-req-interval { + type int8; + status deprecated; + description + "The logarithm to the base 2 of the + minPdelayReqInterval (minimum permitted mean time + interval between successive Pdelay_Req messages)."; + reference + "8.3.3.3.1 of IEEE Std 1588-2019"; + } + + leaf faulty-flag { + type boolean; + status deprecated; + description + "Shall be true if the port is faulty and false + if the port is operating normally."; + reference + "8.3.3.3.2 of IEEE Std 1588-2019"; + } + + leaf peer-mean-path-delay { + type time-interval; + config false; + status deprecated; + description + "An estimate of the current one-way propagation delay + on the link when the delayMechanism is P2P; otherwise, + it is zero."; + reference + "8.3.3.3.3 of IEEE Std 1588-2019"; + } + } + } + } + + container common-services { + description + "Provides management access to the common services. + Common services operate on all PTP Instances + of the PTP Node."; + + container cmlds { + if-feature cmlds; + description + "The Common Mean Link Delay Service (CMLDS) is an + optional service that enables any PTP Port that would + normally obtain the value of a link's + and using the peer-to-peer method + to instead obtain these values from this optional service. + The CMLDS service is available to all PTP Instances + communicating with a specific transport mechanism, + e.g. using Annex F, over the physical link between two PTP + Nodes. + + In this option, the term Link Port refers to the mechanism + enabling communication with a specific transport mechanism, + e.g. using Annex F, over the physical link between two PTP + Nodes. + + The Common Mean Link Delay Service is designed to run + independently from any PTP Instances communicating + over a Link Port. The service provides information on the + as well as the as the + measured in the timescale used by the service. The service + runs on every Link Port where the CMLDS is present. + Information required by a PTP Port is requested from and + delivered by the service running on the associated + Link Port."; + reference + "16.6.4 of IEEE Std 1588-2019"; + + container default-ds { + description + "The default data set of CMLDS."; + reference + "16.6.4.1 of IEEE Std 1588-2019"; + + leaf clock-identity { + type clock-identity; + config false; + description + "The IEEE Std 1588 clockIdentity used by CMLDS."; + reference + "16.6.4.1.2.1 of IEEE Std 1588-2019"; + } + + leaf number-link-ports { + type uint16; + config false; + description + "The number of Link Ports of CMLDS."; + reference + "16.6.4.1.2.2 of IEEE Std 1588-2019"; + } + } + + container ports { + description + "YANG container that is used to get all Link Ports + of CMLDS. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list port { + key "port-index"; + description + "List of data for each Link Port of CMLDS. + The list is structured as leafs for each member + of the IEEE Std 1588 cmldsLinkPortDS (primary + Link Port data set), followed by containers for + each optional Link Port data set. Members of data set + cmldsLinkPortDS.commonMeanLinkDelayInformation + are listed directly under the list, in order + to keep the YANG naming hierarchy as short as + possible."; + reference + "16.6.4.2 of IEEE Std 1588-2019"; + + leaf port-index { + type uint16; + description + "The port list is indexed using a number that is + unique per Link Port within the CMLDS, applicable + to the management context only (i.e. not used in PTP + messages)."; + } + + leaf underlying-interface { + type if:interface-ref; + description + "Reference to the configured underlying IETF YANG + interface that is used by this Link Port for + transport of PTP messages. Among other data, + physical identifiers for the interface + (e.g. MAC address) can be obtained using this + reference."; + reference + "RFC 8343"; + } + + container link-port-ds { + description + "The IEEE Std 1588 cmldsLinkPortDS of this Link Port."; + reference + "16.6.4.2 of IEEE Std 1588-2019"; + + container port-identity { + config false; + description + "The IEEE Std 1588 portIdentity of this Link Port."; + reference + "16.6.4.2.2.1 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf domain-number { + type uint8; + config false; + description + "The IEEE Std 1588 domainNumber used by this + Link Port. This domain number is not configurable, + since its value is determined by the transport + mechanism of the Link Port."; + reference + "16.6.4.2.2.2 of IEEE Std 1588-2019"; + } + + leaf service-measurement-valid { + type boolean; + config false; + description + "This boolean is initialized to false, and will + be false whenever the required PTP messages for + CMLDS are not received on the Link Port. When + the required PTP messages for CMLDS are received, + this boolean is true. + This value is obtained from the + CommonMeanLinkDelayInformation structure returned + by CMLDS."; + reference + "16.6.3.2 of IEEE Std 1588-2019"; + } + + leaf mean-link-delay { + type time-interval; + config false; + description + "Estimate of the current one-way propagation delay + on the PTP Link, i.e., , attached + to this Link Port, computed using the peer-to-peer + delay mechanism. + This value is obtained from the + CommonMeanLinkDelayInformation structure returned + by CMLDS."; + reference + "16.6.3.2 of IEEE Std 1588-2019"; + } + + leaf scaled-neighbor-rate-ratio { + type int32; + config false; + description + "Ratio of the rate of this PTP Node's clock to + the clock of its neighbor attached + to this Link Port, i.e., , + scaled as specified in the standard. + This value is obtained from the + CommonMeanLinkDelayInformation structure returned + by CMLDS."; + reference + "16.6.3.2 of IEEE Std 1588-2019"; + } + + leaf log-min-pdelay-req-interval { + type int8; + description + "Logarithm to the base 2 of the IEEE Std 1588 + minPdelayReqInterval, the minimum permitted + mean time interval between successive Pdelay_Req + messages sent by CMLDS."; + reference + "16.6.4.2.4.1 of IEEE Std 1588-2019"; + } + + leaf version-number { + type uint8; + description + "The PTP major version in use on the Link Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "16.6.4.2.4.2 of IEEE Std 1588-2019"; + } + + leaf minor-version-number { + type uint8; + description + "The PTP minor version in use on the Link Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "16.6.4.2.4.3 of IEEE Std 1588-2019"; + } + + leaf delay-asymmetry { + type time-interval; + description + "The value of IEEE Std 1588 + applicable to the Link Port, which is the + difference in transmission time in one direction + as compared to the opposite direction."; + reference + "7.4.2 of IEEE Std 1588-2019 + 16.6.4.2.4.4 of IEEE Std 1588-2019"; + } + } + + container timestamp-correction-port-ds { + if-feature timestamp-correction; + description + "Provides access to the configurable correction of + timestamps provided to the PTP protocol."; + reference + "16.6.4.3 of IEEE Std 1588-2019"; + + leaf egress-latency { + type time-interval; + description + "Interval between the + provided for a PTP message and the time at which + the message timestamp point of the PTP message + crosses the reference plane."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.2 of IEEE Std 1588-2019"; + } + + leaf ingress-latency { + type time-interval; + description + "Interval between the time the message timestamp + point of an ingress PTP message crosses the + reference plane and the + provided for the PTP message."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.3 of IEEE Std 1588-2019"; + } + } + + container asymmetry-correction-port-ds { + if-feature asymmetry-correction; + description + "Provides access to asymmetry correction parameters + that are used to compute the value of + ."; + reference + "16.6.4.4 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "When this value is true, the mechanism to calculate + for certain media is enabled on + this PTP Port. When this value is false, this + mechanism is disabled on this PTP Port."; + reference + "8.2.17.4 of IEEE Std 1588-2019"; + } + + leaf constant-asymmetry { + type time-interval; + description + "Constant asymmetry used to fine adjust the + dynamically calculated value of , + when the mechanism to calculate + or certain media is enabled."; + reference + "8.2.17.2 of IEEE Std 1588-2019"; + } + + leaf scaled-delay-coefficient { + type relative-difference; + description + "This is the ."; + reference + "8.2.17.3 of IEEE Std 1588-2019"; + } + } + + container performance-monitoring-port-ds { + if-feature performance-monitoring; + description + "Provides data for the optional performance + monitoring mechanism, scoped to each Link Port."; + reference + "16.6.4.5 of IEEE Std 1588-2019"; + + list record-list-peer-delay { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + Link Port that is using the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/ + record-list-peer-delay[6] returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. min-mean-link-delay) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + one in all bits, except the most significant. This + represents the largest positive value of + time-interval, indicating a value outside the + maximum range."; + reference + "J.5.2.1 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-peer-delay-data-record; + } + + list record-list { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + Link Port, not specific to the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/record-list[6] + returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. sync-tx) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + with the value zero, indicating that nothing was + counted. + + Each counter in the record shall be initialized to + zero at the start of a new 15-minute and + 24-hour interval."; + reference + "J.5.2.2 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-data-record; + } + } + } + } + } + } + } +} diff --git a/src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang b/src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang new file mode 100644 index 000000000..af2e94cc9 --- /dev/null +++ b/src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang @@ -0,0 +1,1301 @@ +module ieee802-dot1as-gptp { + yang-version "1.1"; + namespace urn:ieee:std:802.1AS:yang:ieee802-dot1as-gptp; + prefix dot1as-gptp; + + import ietf-yang-types { + prefix yang; + } + import ieee1588-ptp-tt { + prefix ptp-tt; + } + + organization + "IEEE 802.1 Working Group"; + contact + "WG-URL: http://ieee802.org/1/ + WG-EMail: stds-802-1-l@ieee.org + + Contact: IEEE 802.1 Working Group Chair + Postal: C/O IEEE 802.1 Working Group + IEEE Standards Association + 445 Hoes Lane + Piscataway, NJ 08854 + USA + + E-mail: stds-802-1-chairs@ieee.org"; + description + "Management objects that control timing and synchronization for + time sensitive applications, as specified in Clause 14 of + IEEE Std 802.1AS-2025. + + Copyright (C) IEEE (2025). This version of this YANG module is + part of IEEE Std 802.1AS-2025; see the standard itself for full + legal notices."; + + revision 2025-12-10 { + description + "Published as part of IEEE Std 802.1AS-2025."; + reference + "IEEE Std 802.1AS - Timing and Synchronization for + Time-Sensitive Applications: IEEE Std 802.1AS-2025. + IEEE Std 1588 - IEEE Standard for a Precision Clock + Synchronization Protocol for Networked Measurement and + Control Systems: IEEE Std 1588-2019, IEEE Std 1588g-2022, + IEEE Std 1588e-2024."; + } + typedef scaled-ns { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){11}"; + } + description + "The IEEE Std 802.1AS ScaledNs type represents signed values + of time and time interval in units of 2^16 ns, as a signed + 96-bit integer. Each of the 12 octets is represented as a + pair of hexadecimal characters, using uppercase for a letter. + Octets are separated by a dash character. The most + significant octet is first."; + reference + "6.4.3.1 of IEEE Std 802.1AS"; + } + typedef uscaled-ns { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){11}"; + } + description + "The IEEE Std 802.1AS UScaledNs type represents unsigned + values of time and time interval in units of 2^16 ns, as an + unsigned 96-bit integer. Each of the 12 octets is represented + as a pair of hexadecimal characters, using uppercase for a + letter. Octets are separated by a dash character. The most + significant octet is first."; + reference + "6.4.3.2 of IEEE Std 802.1AS"; + } + typedef float64 { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){7}"; + } + description + "The IEEE Std 802.1AS Float64 type represents IEEE Std 754 + binary64. Each of the 8 octets is represented as a pair of + hexadecimal characters, using uppercase for a letter. Octets + are separated by a dash character. The most significant octet + is first."; + reference + "6.4.2 of IEEE Std 802.1AS"; + } + typedef uinteger48 { + type uint64 { + range "0..281474976710655"; + } + description + "48-bit unsigned integer data type."; + reference + "6.4.2 of IEEE Std 802.1AS"; + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds" { + description + "Augment IEEE Std 1588 defaultDS."; + leaf gm-capable { + type boolean; + config false; + description + "The value is true if the time-aware system is capable of + being a Grandmaster, and false if the time-aware system is + not capable of being a Grandmaster."; + reference + "14.2.7 of IEEE Std 802.1AS"; + } + leaf current-utc-offset { + when + "../current-utc-offset-valid='true'"; + type int16; + config false; + description + "Offset from UTC (TAI - UTC). The offset is in units of + seconds. This leaf applies to the ClockTimeTransmitter + entity (i.e., local only, unrelated to a remote GM)."; + reference + "14.2.8 of IEEE Std 802.1AS"; + } + leaf current-utc-offset-valid { + type boolean; + config false; + description + "The value of current-utc-offset-valid shall be true if the + value of current-utc-offset is known to be correct, + otherwise it shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.9 of IEEE Std 802.1AS"; + } + leaf leap59 { + type boolean; + config false; + description + "If the timescale is PTP, a true value for leap59 shall + indicate that the last minute of the current UTC day + contains 59 seconds. If the timescale is not PTP, the value + shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.10 of IEEE Std 802.1AS"; + } + leaf leap61 { + type boolean; + config false; + description + "If the timescale is PTP, a true value for leap61 shall + indicate that the last minute of the current UTC day + contains 61 seconds. If the timescale is not PTP, the value + shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.11 of IEEE Std 802.1AS"; + } + leaf time-traceable { + type boolean; + config false; + description + "The value of time-traceable shall be true if the timescale + is traceable to a primary reference; otherwise, the value + shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.12 of IEEE Std 802.1AS"; + } + leaf frequency-traceable { + type boolean; + config false; + description + "The value of frequency-traceable shall be true if the + frequency determining the timescale is traceable to a + primary reference; otherwise, the value shall be false. + This leaf applies to the ClockTimeTransmitter entity + (i.e., local only, unrelated to a remote GM)."; + reference + "14.2.13 of IEEE Std 802.1AS"; + } + leaf ptp-timescale { + type boolean; + config false; + description + "If ptp-timescale is true, the timescale of the + ClockTimeTransmitter entity is PTP, which is the elapsed + time since the PTP epoch measured using the second defined + by International Atomic Time (TAI). If ptp-timescale is + false, the timescale of the ClockTimeTransmitter entity is + ARB, which is the elapsed time since an arbitrary epoch. + This leaf applies to the ClockTimeTransmitter entity + (i.e., local only, unrelated to a remote GM)."; + reference + "14.2.14 of IEEE Std 802.1AS"; + } + leaf time-source { + type identityref { + base ptp-tt:time-source; + } + config false; + description + "The source of time used by the Grandmaster Clock. This leaf + applies to the ClockTimeTransmitter entity (i.e., local + only, unrelated to a remote GM)."; + reference + "14.2.15 of IEEE Std 802.1AS"; + } + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:current-ds" { + description + "Augment IEEE Std 1588 currentDS."; + leaf last-gm-phase-change { + type scaled-ns; + config false; + description + "Phase change that occurred on the most recent change in + either the Grandmaster PTP Instance or + gm-timebase-indicator leaf."; + reference + "14.3.4 of IEEE Std 802.1AS"; + } + leaf last-gm-freq-change { + type float64; + config false; + description + "Frequency change that occurred on the most recent change in + either the Grandmaster PTP Instance or + gm-timebase-indicator leaf."; + reference + "14.3.5 of IEEE Std 802.1AS"; + } + leaf gm-timebase-indicator { + type uint16; + config false; + description + "The timeBaseIndicator of the current Grandmaster PTP + Instance."; + reference + "14.3.6 of IEEE Std 802.1AS"; + } + leaf gm-change-count { + type yang:counter32; + config false; + description + "This statistics counter tracks the number of times the + Grandmaster PTP Instance has changed in a gPTP domain."; + reference + "14.3.7 of IEEE Std 802.1AS"; + } + leaf time-of-last-gm-change { + type yang:timestamp; + config false; + description + "System time when the most recent Grandmaster Clock change + occurred in a gPTP domain. This leaf's type is YANG + timestamp, which is based on system time. System time is an + unsigned integer in units of 10 milliseconds, using an + epoch defined by the implementation (typically time of + boot-up)."; + reference + "14.3.8 of IEEE Std 802.1AS"; + } + leaf time-of-last-phase-change { + type yang:timestamp; + config false; + description + "System time when the most recent change in Grandmaster + Clock phase occurred. This leaf's type is YANG timestamp, + which is based on system time. System time is an unsigned + integer in units of 10 milliseconds, using an epoch defined + by the implementation (typically time of boot-up)."; + reference + "14.3.9 of IEEE Std 802.1AS"; + } + leaf time-of-last-freq-change { + type yang:timestamp; + config false; + description + "System time when the most recent change in Grandmaster + Clock frequency occurred. This leaf's type is YANG + timestamp, which is based on system time. System time is an + unsigned integer in units of 10 milliseconds, using an + epoch defined by the implementation (typically time of + boot-up)."; + reference + "14.3.10 of IEEE Std 802.1AS"; + } + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:parent-ds" { + description + "Augment IEEE Std 1588 parentDS."; + leaf cumulative-rate-ratio { + type int32; + config false; + description + "Estimate of the ratio of the frequency of the Grandmaster + Clock to the frequency of the LocalClock entity of this PTP + Instance. cumulative-rate-ratio is expressed as the + fractional frequency offset multiplied by 2^41, i.e., the + quantity (rateRatio - 1.0)(2^41)."; + reference + "14.4.3 of IEEE Std 802.1AS"; + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:instances"+ + "/ptp-tt:instance"+ + "/ptp-tt:ports"+ + "/ptp-tt:port"+ + "/ptp-tt:port-ds" { + description + "Augment IEEE Std 1588 portDS. + + 14.10.4 of IEEE Std 802.1AS specifies ptpPortEnabled + (ptp-port-enabled), which is provided in YANG as the + semantically equivalent node in ieee1588-ptp-tt named + port-enable (in port-ds). + + 14.10.16 of IEEE Std 802.1AS specifies + mgtSettableLogAnnounceInterval (mgt-log-announce-interval), + which is provided in YANG as the semantically equivalent node + in ieee1588-ptp-tt named log-announce-interval (in port-ds). + In the context of IEEE Std 802.1AS, log-announce-interval + cannot be used unless use-mgt-log-announce-interval is true. + + 14.10.21 of IEEE Std 802.1AS specifies + mgtSettableLogSyncInterval (mgt-log-sync-interval), which is + provided in YANG as the semantically equivalent node in + ieee1588-ptp-tt named log-sync-interval (in port-ds). In the + context of IEEE Std 802.1AS, log-sync-interval cannot be used + unless use-mgt-log-sync-interval is true."; + leaf is-measuring-delay { + type boolean; + config false; + description + "Boolean that is true if the port is measuring PTP Link + propagation delay."; + reference + "14.10.6 of IEEE Std 802.1AS"; + } + leaf as-capable { + type boolean; + config false; + description + "Boolean that is true if and only if it is determined that + this PTP Instance and the PTP Instance at the other end of + the link attached to this port can interoperate with each + other via the IEEE Std 802.1AS protocol."; + reference + "10.2.5.1 of IEEE Std 802.1AS + 14.10.7 of IEEE Std 802.1AS"; + } + leaf mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "Propagation time threshold for mean-link-delay, above which + a port is not considered capable of participating in the + IEEE Std 802.1AS protocol."; + reference + "14.10.9 of IEEE Std 802.1AS"; + } + leaf neg-mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "The negative propagation time threshold for + mean-link-delay, below which a port is not considered + capable of participating in the IEEE Std 802.1AS + protocol."; + reference + "14.10.10 of IEEE Std 802.1AS"; + } + leaf neighbor-rate-ratio { + type int32; + config false; + description + "Estimate of the ratio of the frequency of the LocalClock + entity of the PTP Instance at the other end of the link + attached to this PTP Port, to the frequency of the + LocalClock entity of this PTP Instance. neighbor-rate-ratio + is expressed as the fractional frequency offset multiplied + by 2^41, i.e., the quantity (rateRatio - 1.0)(2^41)."; + reference + "14.10.12 of IEEE Std 802.1AS"; + } + leaf initial-log-announce-interval { + type int8; + description + "When use-mgt-log-announce-interval is false (i.e., change + with Signaling message), this is the logarithm to base 2 of + the announce interval used when the port is initialized."; + reference + "14.10.13 of IEEE Std 802.1AS"; + } + leaf current-log-announce-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current announce interval."; + reference + "14.10.14 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-announce-interval { + type boolean; + description + "Boolean that determines the source of the announce + interval. If the value is true, the announce interval + (current-log-announce-interval) is set equal to the value + of mgt-log-announce-interval. If the value is false, the + announce interval is determined by the + AnnounceIntervalSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.15 of IEEE Std 802.1AS"; + } + leaf initial-log-sync-interval { + type int8; + description + "When use-mgt-log-sync-interval is false (i.e., change with + Signaling message), this is the logarithm to base 2 of the + sync interval used when the port is initialized."; + reference + "14.10.18 of IEEE Std 802.1AS"; + } + leaf current-log-sync-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current sync interval."; + reference + "14.10.19 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-sync-interval { + type boolean; + description + "Boolean that determines the source of the sync interval. + If the value is true, the sync interval + (current-log-sync-interval) is set equal to the value of + mgt-log-sync-interval. If the value is false, the sync + interval is determined by the SyncIntervalSetting + state machine (i.e., changed with Signaling message)."; + reference + "14.10.20 of IEEE Std 802.1AS"; + } + leaf sync-receipt-timeout { + type uint8; + description + "Number of sync intervals that a timeReceiver port waits + without receiving synchronization information, before + assuming that the timeTransmitter is no longer transmitting + synchronization information and that the BTCA needs to be + run, if appropriate."; + reference + "14.10.22 of IEEE Std 802.1AS"; + } + leaf sync-receipt-timeout-interval { + type uscaled-ns; + config false; + description + "Time interval after which sync receipt timeout occurs if + time-synchronization information has not been received + during the interval."; + reference + "14.10.23 of IEEE Std 802.1AS"; + } + leaf initial-log-pdelay-req-interval { + type int8; + description + "When use-mgt-log-pdelay-req-interval is false (i.e., change + with Signaling message), this is the logarithm to base 2 of + the Pdelay_Req transmit interval used when the port is + initialized."; + reference + "14.10.24 of IEEE Std 802.1AS"; + } + leaf current-log-pdelay-req-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current Pdelay_Req transmit + interval."; + reference + "14.10.25 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-pdelay-req-interval { + type boolean; + description + "Boolean that determines the source of the Pdelay_Req + transmit interval. If the value is true, the Pdelay_Req + transmit interval (current-log-pdelay-req-interval) is set + equal to the value of mgt-log-pdelay-req-interval. If the + value is false, the Pdelay_Req transmit interval is + determined by the LinkDelayIntervalSetting state machine + (i.e., changed with Signaling message)."; + reference + "14.10.26 of IEEE Std 802.1AS"; + } + leaf mgt-log-pdelay-req-interval { + type int8; + description + "Logarithm to base 2 of the Pdelay_Req transmit interval, + used if use-mgt-log-pdelay-req-interval is true. This value + is not used if use-mgt-log-pdelay-req-interval is false."; + reference + "14.10.27 of IEEE Std 802.1AS"; + } + leaf initial-log-gptp-cap-interval { + type int8; + description + "When use-mgt-log-gptp-cap-interval is false (i.e., change + with Signaling message), this is the logarithm to base 2 of + the gPTP capable message interval used when the port is + initialized."; + reference + "14.10.28 of IEEE Std 802.1AS"; + } + leaf current-log-gptp-cap-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current gPTP capable message + interval."; + reference + "14.10.29 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-gptp-cap-interval { + type boolean; + description + "Boolean that determines the source of the gPTP capable + message interval. If the value is true, the gPTP capable + message interval (current-log-gptp-cap-interval) is set + equal to the value of mgt-gptp-cap-req-interval. If the + value is false, the gPTP capable message interval is + determined by the GptpCapableMessageIntervalSetting state + machine (i.e., changed with Signaling message)."; + reference + "14.10.30 of IEEE Std 802.1AS"; + } + leaf mgt-log-gptp-cap-interval { + type int8; + description + "Logarithm to base 2 of the gPTP capable message interval, + used if use-mgt-log-gptp-cap-interval is true. This value + is not used if use-mgt-log-pdelay-req-interval is false."; + reference + "14.10.31 of IEEE Std 802.1AS"; + } + leaf initial-compute-neighbor-rate-ratio { + type boolean; + description + "When use-mgt-compute-neighbor-rate-ratio is false + (i.e., change with Signaling message), this is the initial + value of computeNeighborRateRatio."; + reference + "14.10.32 of IEEE Std 802.1AS"; + } + leaf current-compute-neighbor-rate-ratio { + type boolean; + config false; + description + "Current value of computeNeighborRateRatio."; + reference + "14.10.33 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-neighbor-rate-ratio { + type boolean; + description + "Boolean that determines the source of + computeNeighborRateRatio. If the value is true, + computeNeighborRateRatio is set equal to the value of + mgt-compute-neighbor-rate-ratio. If the value is false, + computeNeighborRateRatio is determined by the + LinkDelayIntervalSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.34 of IEEE Std 802.1AS"; + } + leaf mgt-compute-neighbor-rate-ratio { + type boolean; + description + "Value of computeNeighborRateRatio, used if + use-mgt-compute-neighbor-rate-ratio is true. This value is + not used if use-mgt-compute-neighbor-rate-ratio is false."; + reference + "14.10.35 of IEEE Std 802.1AS"; + } + leaf initial-compute-mean-link-delay { + type boolean; + description + "When use-mgt-compute-mean-link-delay is false (i.e., change + with Signaling message), this is the initial value of + computeMeanLinkDelay."; + reference + "14.10.36 of IEEE Std 802.1AS"; + } + leaf current-compute-mean-link-delay { + type boolean; + config false; + description + "Current value of computeMeanLinkDelay."; + reference + "14.10.37 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-mean-link-delay { + type boolean; + description + "Boolean that determines the source of computeMeanLinkDelay. + If the value is true, computeMeanLinkDelay is set equal to + the value of mgt-compute-mean-link-delay. If the value is + false, computeMeanLinkDelay is determined by the + LinkDelayIntervalSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.38 of IEEE Std 802.1AS"; + } + leaf mgt-compute-mean-link-delay { + type boolean; + description + "Value of computeMeanLinkDelay, used if + use-mgt-compute-mean-link-delay is true. This value is not + used if use-mgt-compute-mean-link-delay is false."; + reference + "14.10.39 of IEEE Std 802.1AS"; + } + leaf allowed-lost-responses { + type uint8; + description + "Number of Pdelay_Req messages for which a valid response is + not received, above which a port is considered to not be + exchanging peer delay messages with its neighbor."; + reference + "14.10.40 of IEEE Std 802.1AS"; + } + leaf allowed-faults { + type uint8; + description + "Number of faults above which asCapable is set to false."; + reference + "14.10.41 of IEEE Std 802.1AS"; + } + leaf gptp-cap-receipt-timeout { + type uint8; + description + "Number of transmission intervals that a port waits without + receiving the gPTP-capable TLV, before assuming that the + neighbor port is no longer invoking the gPTP protocol."; + reference + "14.10.42 of IEEE Std 802.1AS"; + } + leaf nup { + type float64; + description + "For an OLT port of an IEEE Std 802.3 EPON link, this value + is the effective index of refraction for the EPON upstream + wavelength light of the optical path."; + reference + "14.10.44 of IEEE Std 802.1AS"; + } + leaf ndown { + type float64; + description + "For an OLT port of an IEEE 802.3 EPON link, this value is + the effective index of refraction for the EPON downstream + wavelength light of the optical path."; + reference + "14.10.45 of IEEE Std 802.1AS"; + } + leaf one-step-tx-oper { + type boolean; + config false; + description + "This value is true if the port is sending one-step Sync + messages, and false if the port is sending two-step Sync + and Follow_Up messages."; + reference + "14.10.46 of IEEE Std 802.1AS"; + } + leaf one-step-receive { + type boolean; + config false; + description + "This value is true if the port is capable of receiving and + processing one-step Sync messages."; + reference + "14.10.47 of IEEE Std 802.1AS"; + } + leaf one-step-transmit { + type boolean; + config false; + description + "This value is true if the port is capable of transmitting + one-step Sync messages."; + reference + "14.10.48 of IEEE Std 802.1AS"; + } + leaf initial-one-step-tx-oper { + type boolean; + description + "When use-mgt-one-step-tx-oper is false (i.e., change with + Signaling message), this is the initial value of + current-one-step-tx-oper."; + reference + "14.10.49 of IEEE Std 802.1AS"; + } + leaf current-one-step-tx-oper { + type boolean; + config false; + description + "This value is true if the port is configured to transmit + one-step Sync messages, either via management + (mgt-one-step-tx-oper) or Signaling. If both + current-one-step-tx-oper and one-step-transmit are true, + the port transmits one-step Sync messages + (i.e., one-step-tx-oper true)."; + reference + "14.10.50 of IEEE Std 802.1AS"; + } + leaf use-mgt-one-step-tx-oper { + type boolean; + description + "Boolean that determines the source of + current-one-step-tx-oper. If the value is true, + current-one-step-tx-oper is set equal to the value of + mgt-one-step-tx-oper. If the value is false, + current-one-step-tx-oper is determined by the + OneStepTxOperSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.51 of IEEE Std 802.1AS"; + } + leaf mgt-one-step-tx-oper { + type boolean; + description + "If use-mgt-one-step-tx-oper is true, + current-one-step-tx-oper is set equal to this value. This + value is not used if use-mgt-one-step-tx-oper is false."; + reference + "14.10.52 of IEEE Std 802.1AS"; + } + leaf sync-locked { + type boolean; + config false; + description + "This value is true if the port will transmit a Sync as soon + as possible after the timeReceiver port receives a Sync + message."; + reference + "14.10.53 of IEEE Std 802.1AS"; + } + leaf-list pdelay-truncated-timestamps { + type uinteger48; + config false; + description + "For full-duplex IEEE Std 802.3 media, and CSN media that + use the peer-to-peer delay mechanism to measure path delay, + the values of the four elements of this leaf-list + correspond to the timestamps t1, t2, t3, and t4, listed in + that order. Each timestamp is expressed in units of + 2^-16 ns (i.e., the value of each array element is equal to + the remainder obtained upon dividing the respective + timestamp, expressed in units of 2^-16 ns, by 2^48). + At any given time, the timestamp values stored in the array + are for the same, and most recently completed, peer delay + message exchange. For each timestamp, only 48-bits are + valid (the upper 16-bits are always zero)."; + reference + "14.10.54 of IEEE Std 802.1AS"; + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:instances"+ + "/ptp-tt:instance"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add port-statistics-ds to IEEE Std 1588 PTP + Port."; + container port-statistics-ds { + description + "Provides counters associated with the port of the PTP + Instance."; + reference + "14.12 of IEEE Std 802.1AS"; + leaf rx-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time synchronization + information is received."; + reference + "14.12.2 of IEEE Std 802.1AS"; + } + leaf rx-one-step-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time a one-step Sync + message is received."; + reference + "14.12.3 of IEEE Std 802.1AS"; + } + leaf rx-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Follow_Up message is + received."; + reference + "14.12.4 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-req-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Req message + is received."; + reference + "14.12.5 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Resp message + is received."; + reference + "14.12.6 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a + Pdelay_Resp_Follow_Up message is received."; + reference + "14.12.7 of IEEE Std 802.1AS"; + } + leaf rx-announce-count { + type yang:counter32; + config false; + description + "Counter that increments every time an Announce message is + received."; + reference + "14.12.8 of IEEE Std 802.1AS"; + } + leaf rx-packet-discard-count { + type yang:counter32; + config false; + description + "Counter that increments every time a PTP message of the + respective PTP Instance is discarded."; + reference + "14.12.9 of IEEE Std 802.1AS"; + } + leaf sync-receipt-timeout-count { + type yang:counter32; + config false; + description + "Counter that increments every time a sync receipt timeout + occurs."; + reference + "14.12.10 of IEEE Std 802.1AS"; + } + leaf announce-receipt-timeout-count { + type yang:counter32; + config false; + description + "Counter that increments every time an announce receipt + timeout occurs."; + reference + "14.12.11 of IEEE Std 802.1AS"; + } + leaf pdelay-allowed-lost-exceeded-count { + type yang:counter32; + config false; + description + "Counter that increments every time the value of the + variable lostResponses exceeds the value of the variable + allowedLostResponses, in the RESET state of the + MDPdelayReq state machine."; + reference + "14.12.12 of IEEE Std 802.1AS"; + } + leaf tx-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time synchronization + information is transmitted."; + reference + "14.12.13 of IEEE Std 802.1AS"; + } + leaf tx-one-step-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time a one-step Sync + message is transmitted."; + reference + "14.12.14 of IEEE Std 802.1AS"; + } + leaf tx-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Follow_Up message is + transmitted."; + reference + "14.12.15 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-req-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Req message + is transmitted."; + reference + "14.12.16 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Resp message + is transmitted."; + reference + "14.12.17 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a + Pdelay_Resp_Follow_Up message is transmitted."; + reference + "14.12.18 of IEEE Std 802.1AS"; + } + leaf tx-announce-count { + type yang:counter32; + config false; + description + "Counter that increments every time an Announce message is + transmitted."; + reference + "14.12.19 of IEEE Std 802.1AS"; + } + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:instances"+ + "/ptp-tt:instance"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add asymmetry-measurement-mode-ds to IEEE Std 1588 + PTP Port."; + container asymmetry-measurement-mode-ds { + description + "Represents the capability to enable/disable the Asymmetry + Compensation Measurement Procedure on a PTP Port. This data + set is used instead of the CMLDS + asymmetry-measurement-mode-ds when only a single PTP + Instance is present (i.e., CMLDS is not used)."; + reference + "14.15 of IEEE Std 802.1AS + Annex G of IEEE Std 802.1AS"; + leaf enabled { + type boolean; + description + "For full-duplex IEEE Std 802.3 media, the value is true + if an asymmetry measurement is being performed for the + link attached to this PTP Port, and false otherwise. For + all other media, the value shall be false."; + reference + "14.15.2 of IEEE Std 802.1AS"; + } + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:common-services"+ + "/ptp-tt:cmlds"+ + "/ptp-tt:ports"+ + "/ptp-tt:port"+ + "/ptp-tt:link-port-ds" { + description + "Augment IEEE Std 1588 cmldsLinkPortDS."; + leaf cmlds-link-port-enabled { + type boolean; + config false; + description + "Boolean that is true if both delay-mechanism is common-p2p + and the value of ptp-port-enabled is true, for at least one + PTP Port that uses the CMLDS; otherwise, the value is + false."; + reference + "11.2.18.1 of IEEE Std 802.1AS + 14.18.3 of IEEE Std 802.1AS"; + } + leaf is-measuring-delay { + type boolean; + config false; + description + "This leaf is analogous to is-measuring-delay for a PTP + Port, but applicable to this Link Port."; + reference + "14.18.4 of IEEE Std 802.1AS"; + } + leaf as-capable-across-domains { + type boolean; + config false; + description + "This leaf is true when all PTP Instances (domains) for this + Link Port detect proper exchange of Pdelay messages."; + reference + "11.2.2 of IEEE Std 802.1AS + 14.18.5 of IEEE Std 802.1AS"; + } + leaf mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "Propagation time threshold for mean-link-delay, above which + a Link Port is not considered capable of participating in + the IEEE Std 802.1AS protocol."; + reference + "14.18.7 of IEEE Std 802.1AS"; + } + leaf neg-mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "The negative propagation time threshold for + mean-link-delay, below which a Link Port is not considered + capable of participating in the IEEE Std 802.1AS + protocol."; + reference + "14.18.8 of IEEE Std 802.1AS"; + } + leaf initial-log-pdelay-req-interval { + type int8; + description + "This leaf is analogous to initial-log-pdelay-req-interval + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.11 of IEEE Std 802.1AS"; + } + leaf current-log-pdelay-req-interval { + type int8; + config false; + description + "This leaf is analogous to current-log-pdelay-req-interval + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.12 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-pdelay-req-interval { + type boolean; + description + "This leaf is analogous to use-mgt-log-pdelay-req-interval + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.13 of IEEE Std 802.1AS"; + } + leaf mgt-log-pdelay-req-interval { + type int8; + description + "This leaf is analogous to mgt-log-pdelay-req-interval for + a PTP Port, but applicable to this Link Port."; + reference + "14.18.14 of IEEE Std 802.1AS"; + } + leaf initial-compute-neighbor-rate-ratio { + type boolean; + description + "This leaf is analogous to + initial-compute-neighbor-rate-ratio for a PTP Port, but + applicable to this Link Port."; + reference + "14.18.15 of IEEE Std 802.1AS"; + } + leaf current-compute-neighbor-rate-ratio { + type boolean; + config false; + description + "This leaf is analogous to + current-compute-neighbor-rate-ratio for a PTP Port, but + applicable to this Link Port."; + reference + "14.18.16 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-neighbor-rate-ratio { + type boolean; + description + "This leaf is analogous to + use-mgt-compute-neighbor-rate-ratio for a PTP Port, but + applicable to this Link Port."; + reference + "14.18.17 of IEEE Std 802.1AS"; + } + leaf mgt-compute-neighbor-rate-ratio { + type boolean; + description + "This leaf is analogous to mgt-compute-neighbor-rate-ratio + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.18 of IEEE Std 802.1AS"; + } + leaf initial-compute-mean-link-delay { + type boolean; + description + "This leaf is analogous to initial-compute-mean-link-delay + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.19 of IEEE Std 802.1AS"; + } + leaf current-compute-mean-link-delay { + type boolean; + config false; + description + "This leaf is analogous to current-compute-mean-link-delay + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.20 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-mean-link-delay { + type boolean; + description + "This leaf is analogous to use-mgt-compute-mean-link-delay + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.21 of IEEE Std 802.1AS"; + } + leaf mgt-compute-mean-link-delay { + type boolean; + description + "This leaf is analogous to mgt-compute-mean-link-delay for a + PTP Port, but applicable to this Link Port."; + reference + "14.18.22 of IEEE Std 802.1AS"; + } + leaf allowed-lost-responses { + type uint8; + description + "This leaf is analogous to allowed-lost-responses for a PTP + Port, but applicable to this Link Port."; + reference + "14.18.23 of IEEE Std 802.1AS"; + } + leaf allowed-faults { + type uint8; + description + "This leaf is analogous to allowed-faults for a PTP Port, + but applicable to this Link Port."; + reference + "14.18.24 of IEEE Std 802.1AS"; + } + leaf-list pdelay-truncated-timestamps { + type uinteger48; + config false; + description + "This leaf is analogous to pdelay-truncated-timestamps for a + PTP Port, but applicable to this Link Port."; + reference + "14.18.26 of IEEE Std 802.1AS"; + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:common-services"+ + "/ptp-tt:cmlds"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add port-statistics-ds to IEEE Std 1588 Link + Port."; + container port-statistics-ds { + description + "This container is analogous to port-statistics-ds for a PTP + Port, but applicable to this Link Port."; + reference + "14.19 of IEEE Std 802.1AS"; + leaf rx-pdelay-req-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-pdelay-req-count for a PTP + Port, but applicable to this Link Port."; + reference + "14.19.2 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-pdelay-resp-count for a PTP + Port, but applicable to this Link Port."; + reference + "14.19.3 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-pdelay-resp-follow-up-count + for a PTP Port, but applicable to this Link Port."; + reference + "14.19.4 of IEEE Std 802.1AS"; + } + leaf rx-packet-discard-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-packet-discard-count for a + PTP Port, but applicable to this Link Port."; + reference + "14.19.5 of IEEE Std 802.1AS"; + } + leaf pdelay-allowed-lost-exceeded-count { + type yang:counter32; + config false; + description + "This leaf is analogous to + pdelay-allowed-lost-exceeded-count for a PTP Port, but + applicable to this Link Port."; + reference + "14.19.6 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-req-count { + type yang:counter32; + config false; + description + "This leaf is analogous to tx-pdelay-req-count for a + PTP Port, but applicable to this Link Port."; + reference + "14.19.7 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-count { + type yang:counter32; + config false; + description + "This leaf is analogous to tx-pdelay-resp-count for a + PTP Port, but applicable to this Link Port."; + reference + "14.19.8 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "This leaf is analogous to tx-pdelay-resp-follow-up-count + for a PTP Port, but applicable to this Link Port."; + reference + "14.19.9 of IEEE Std 802.1AS"; + } + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:common-services"+ + "/ptp-tt:cmlds"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add asymmetry-measurement-mode-ds to IEEE + Std 1588 Link Port."; + container asymmetry-measurement-mode-ds { + description + "This container is analogous to + asymmetry-measurement-mode-ds for a PTP Port, but + applicable to this Link Port."; + reference + "14.20 of IEEE Std 802.1AS"; + leaf enabled { + type boolean; + description + "This leaf is analogous to + asymmetry-measurement-mode-ds.enabled for a PTP Port, but + applicable to this Link Port."; + reference + "14.20.2 of IEEE Std 802.1AS"; + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-ptp.yang b/src/confd/yang/confd/infix-if-ptp.yang new file mode 100644 index 000000000..256f73e83 --- /dev/null +++ b/src/confd/yang/confd/infix-if-ptp.yang @@ -0,0 +1,102 @@ +submodule infix-if-ptp { + yang-version 1.1; + belongs-to infix-interfaces { + prefix infix-if; + } + + import ietf-interfaces { + prefix if; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "PTP timestamping capabilities for ietf-interfaces."; + + revision 2026-04-09 { + description "Initial revision."; + reference "internal"; + } + + /* + * Data Nodes + */ + + augment "/if:interfaces/if:interface" { + description + "PTP timestamping capabilities reported by the network driver. + Data is probed at boot via ethtool --json -T and is read-only."; + + container ptp-capabilities { + config false; + description + "PTP hardware and software timestamping capabilities of this + interface, as reported by the driver via ethtool -T. Absent + on virtual interfaces (bridges, VLANs, etc.) that have no + underlying physical device."; + + leaf-list capabilities { + type enumeration { + enum software-transmit { + description "Software TX timestamping supported."; + } + enum software-receive { + description "Software RX timestamping supported."; + } + enum software-system-clock { + description "System clock can be used for SW timestamping."; + } + enum hardware-transmit { + description "Hardware TX timestamping supported."; + } + enum hardware-receive { + description "Hardware RX timestamping supported."; + } + enum hardware-raw-clock { + description "Raw hardware clock (PHC) exposed to userspace."; + } + } + description + "Set of timestamping capability flags reported by the driver. + The presence of hardware-transmit indicates that ptp4l can + use hardware timestamping on this interface."; + } + + leaf phc-index { + type uint32; + description + "PTP Hardware Clock device index (e.g. 0 for /dev/ptp0). + Absent when the interface has no associated PHC."; + } + + leaf-list tx-types { + type string; + description + "Hardware TX timestamp types supported (e.g. 'off', 'on', + 'onestep-sync'). Empty when hardware-transmit is not in + the capabilities set."; + } + + leaf-list rx-filters { + type string; + description + "Hardware RX timestamp filter modes supported (e.g. + 'ptpv2-l2-event', 'ptpv2-l4-sync'). Empty when + hardware-receive is not in the capabilities set."; + } + + leaf hwtstamp-provider-index { + type uint32; + description + "Hardware timestamp provider index. Present only on kernels + and drivers that expose per-provider timestamping (Linux 6.x+)."; + } + + leaf hwtstamp-provider-qualifier { + type string; + description + "Human-readable quality descriptor for the hardware timestamp + provider, e.g. 'Precise (IEEE 1588 quality)'."; + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-ptp@2026-04-09.yang b/src/confd/yang/confd/infix-if-ptp@2026-04-09.yang new file mode 120000 index 000000000..ee7aa240b --- /dev/null +++ b/src/confd/yang/confd/infix-if-ptp@2026-04-09.yang @@ -0,0 +1 @@ +infix-if-ptp.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 3e6d88ba0..0ace0fe2c 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -35,11 +35,17 @@ module infix-interfaces { include infix-if-vxlan; include infix-if-wifi; include infix-if-wireguard; + include infix-if-ptp; organization "KernelKit"; contact "kernelkit@googlegroups.com"; description "Linux bridge and lag extensions for ietf-interfaces."; + revision 2026-04-09 { + description "Add ptp-capabilities submodule for per-interface PTP timestamping info."; + reference "internal"; + } + revision 2025-11-06 { description "Use new tunnel-common grouping for local, remote, ttl, and tos."; reference "internal"; diff --git a/src/confd/yang/confd/infix-interfaces@2025-11-06.yang b/src/confd/yang/confd/infix-interfaces@2026-04-09.yang similarity index 100% rename from src/confd/yang/confd/infix-interfaces@2025-11-06.yang rename to src/confd/yang/confd/infix-interfaces@2026-04-09.yang diff --git a/src/confd/yang/confd/infix-ptp.yang b/src/confd/yang/confd/infix-ptp.yang new file mode 100644 index 000000000..e0638019c --- /dev/null +++ b/src/confd/yang/confd/infix-ptp.yang @@ -0,0 +1,247 @@ +module infix-ptp { + yang-version 1.1; + namespace "urn:infix:ptp:ns:yang:1.0"; + prefix infix-ptp; + + import ieee1588-ptp-tt { + prefix ptp-tt; + } + import ieee802-dot1as-gptp { + prefix dot1as-gptp; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "Augments and deviations for IEEE 1588-2019 and + IEEE 802.1AS-2020 PTP support. + + Profile selection via the profile leaf covers all + protocol-mandatory settings for each profile. The + standard sdo-id leaf is deviated not-supported; the + profile leaf is the authoritative selector. + + Transparent Clock support uses the modern instance-type + approach (p2p-tc / e2e-tc) from IEEE 1588-2019. The + deprecated IEEE 1588-2008 transparent-clock-default-ds + and transparent-clock-ports containers are deviated + not-supported."; + + revision 2026-04-07 { + description "Initial revision."; + reference "internal"; + } + + /* + * Augments + */ + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds" { + description + "Profile selection for this PTP instance. + + The profile leaf selects the PTP profile and applies all + protocol-mandatory settings for that profile. The standard + sdo-id leaf is deviated not-supported; use profile instead."; + + leaf profile { + type enumeration { + enum ieee1588 { + value 0; + description + "IEEE 1588-2019 default profile. Uses UDP/IPv4 transport + and E2E delay measurement by default. Network transport + and delay mechanism are user-configurable per port."; + } + enum ieee802-dot1as { + value 1; + description + "IEEE 802.1AS-2020 gPTP profile. Applies all + protocol-mandatory settings: IEEE 802.3 (Layer 2) + transport, P2P delay measurement, and the 802.1AS + multicast group address. Also enables path trace, + follow-up information, and neighbor propagation delay + thresholds as required by the standard. + + User-configurable: priority1, priority2, domain-number, + time-receiver-only, and timer interval leaves."; + } + } + default ieee1588; + description + "PTP profile for this instance. Selects the complete set of + protocol-mandatory settings for the chosen profile. + + The combination of domain-number and profile must be unique + across all PTP instances on this node."; + } + } + + /* + * Deviations from ieee1588-ptp-tt + */ + + /* + * /ptp/instances/instance/default-ds + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds/ptp-tt:sdo-id" { + deviate not-supported; + description + "Only the upper 4-bit majorSdoId field is configurable. + Use the profile leaf to select the correct majorSdoId value."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds/ptp-tt:current-time" { + deviate not-supported; + description + "Setting PTP instance time via YANG is not supported. + The current PTP time is only observable via pmc and is + presented in operational data."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds/ptp-tt:external-port-config-enable" { + deviate not-supported; + description + "The external-port-config feature is not supported."; + } + + /* + * /ptp/instances/instance/description-ds + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:description-ds" { + deviate not-supported; + description + "The description data set is not exposed by pmc and is + not supported in this implementation."; + } + + /* + * /ptp/instances/instance/fault-log-ds + * (feature-gated; not enabled) + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:fault-log-ds" { + deviate not-supported; + description + "Structured fault log is not supported. ptp4l does not + expose a fault log via pmc. Faults are observable in + syslog (tagged ptp4l)."; + } + + /* + * /ptp/instances/instance/path-trace-ds + * (feature-gated; not enabled) + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:path-trace-ds" { + deviate not-supported; + description + "Path trace mechanism is not supported in this implementation."; + } + + /* + * /ptp/instances/instance/alternate-timescale-ds + * (feature-gated; not enabled) + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:alternate-timescale-ds" { + deviate not-supported; + description + "Alternate timescale mechanism is not supported."; + } + + /* + * /ptp/instances/instance/ports/port/port-ds + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:peer-mean-path-delay" { + deviate not-supported; + description + "Deprecated in IEEE 1588-2019; superseded by mean-link-delay. + Not supported."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:version-number" { + deviate not-supported; + description + "PTP version is determined by ptp4l at runtime and is not + user-configurable."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:minor-version-number" { + deviate not-supported; + description + "PTP minor version is determined by ptp4l at runtime and is + not user-configurable."; + } + + /* + * /ptp/transparent-clock-default-ds (deprecated IEEE 1588-2008) + */ + + deviation "/ptp-tt:ptp/ptp-tt:transparent-clock-default-ds" { + deviate not-supported; + description + "Deprecated IEEE 1588-2008 container. Use the modern + instance-type = p2p-tc or e2e-tc approach in + /ptp/instances/instance/default-ds/instance-type instead."; + } + + /* + * /ptp/transparent-clock-ports (deprecated IEEE 1588-2008) + */ + + deviation "/ptp-tt:ptp/ptp-tt:transparent-clock-ports" { + deviate not-supported; + description + "Deprecated IEEE 1588-2008 container. Transparent Clock + ports are managed via /ptp/instances/instance/ports instead."; + } + + /* + * /ptp/common-services (CMLDS — Phase 2) + */ + + deviation "/ptp-tt:ptp/ptp-tt:common-services" { + deviate not-supported; + description + "CMLDS (Common Mean Link Delay Service) is not supported yet."; + } + + /* + * Deviations from ieee802-dot1as-gptp + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/dot1as-gptp:nup" { + deviate not-supported; + description + "EPON upstream refraction index — not applicable to + Ethernet/TSN deployments."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/dot1as-gptp:ndown" { + deviate not-supported; + description + "EPON downstream refraction index — not applicable to + Ethernet/TSN deployments."; + } +} diff --git a/src/confd/yang/confd/infix-ptp@2026-04-07.yang b/src/confd/yang/confd/infix-ptp@2026-04-07.yang new file mode 120000 index 000000000..a0f235955 --- /dev/null +++ b/src/confd/yang/confd/infix-ptp@2026-04-07.yang @@ -0,0 +1 @@ +infix-ptp.yang \ No newline at end of file diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index b47727f9e..250cd8378 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -422,6 +422,13 @@ echo "Public: $pub" show nacm + + + + + show ptp $KLISH_PARAM_instance + + diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 1fee3f0f7..bf77df3db 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -5636,6 +5636,203 @@ def show_bfd(json_data): show_bfd_peers_brief(json_data) +def _ptp_ns(yang_val): + """Convert YANG time-interval (ns × 2^16, stored as str) to integer nanoseconds.""" + try: + return int(yang_val) // 65536 + except (TypeError, ValueError): + return None + + +def _ptp_strip(identity): + """Strip YANG module prefix from identityref value. + + 'ieee1588-ptp-tt:cc-default' → 'cc-default' + """ + if identity and ':' in identity: + return identity.split(':', 1)[1] + return identity or '' + + +_PTP_INSTANCE_TYPE_NAMES = { + "oc": "Ordinary Clock", + "bc": "Boundary Clock", + "p2p-tc": "P2P Transparent Clock", + "e2e-tc": "E2E Transparent Clock", +} + +_PTP_PORT_STATE_COLOR = { + "time-transmitter": Decore.green, + "time-receiver": Decore.green, + "pre-time-transmitter": Decore.yellow, + "uncalibrated": Decore.yellow, + "listening": Decore.yellow, + "passive": lambda x: x, + "faulty": Decore.red, + "disabled": Decore.red, + "initializing": lambda x: x, +} + + +def show_ptp(json_data, instance_index=None): + """Show PTP instance status.""" + ptp = json_data.get("ieee1588-ptp-tt:ptp", {}) + instances = ptp.get("instances", {}).get("instance", []) + + if not instances: + print("PTP: no instances configured.") + return + + for inst in instances: + idx = inst.get("instance-index", "?") + + if instance_index is not None and str(idx) != str(instance_index): + continue + + dds = inst.get("default-ds", {}) + cds = inst.get("current-ds", {}) + pds = inst.get("parent-ds", {}) + tpds = inst.get("time-properties-ds", {}) + ports = inst.get("ports", {}).get("port", []) + + itype = _PTP_INSTANCE_TYPE_NAMES.get(dds.get("instance-type", "oc"), "Unknown") + domain = dds.get("domain-number", 0) + clock_id = dds.get("clock-identity", "?") + + # ── Header ──────────────────────────────────────────────────────────── + header = f"PTP Instance {idx}" + subtitle = f"{itype} · domain {domain}" + pad_len = max(1, 40 - len(header)) + pad = " " * pad_len + rule_w = max(68, len(header) + pad_len + len(subtitle)) + print(f"{Decore.bold(header)}{pad}{subtitle}") + print("─" * rule_w) + + # ── Clock / GM identity ─────────────────────────────────────────────── + W = 24 + gm_id = pds.get("grandmaster-identity", "") + print(f" {'Clock identity':<{W}}: {clock_id}") + if gm_id and gm_id != clock_id: + print(f" {'Grandmaster':<{W}}: {gm_id}") + else: + print(f" {'Grandmaster':<{W}}: (self)") + + # ── Priorities ──────────────────────────────────────────────────────── + p1, p2 = dds.get("priority1", "?"), dds.get("priority2", "?") + print(f" {'Priority1/Priority2':<{W}}: {p1} / {p2}") + + if gm_id and gm_id != clock_id: + gp1 = pds.get("grandmaster-priority1", "?") + gp2 = pds.get("grandmaster-priority2", "?") + print(f" {'GM Priority1/Priority2':<{W}}: {gp1} / {gp2}") + + # ── Clock quality ───────────────────────────────────────────────────── + cq = dds.get("clock-quality", {}) + cc = _ptp_strip(cq.get("clock-class", "")) + if cc: + print(f" {'Clock class':<{W}}: {cc}") + + if gm_id and gm_id != clock_id: + gcq = pds.get("grandmaster-clock-quality", {}) + gcc = _ptp_strip(gcq.get("clock-class", "")) + if gcc and gcc != cc: + print(f" {'GM clock class':<{W}}: {gcc}") + + # ── Time source ─────────────────────────────────────────────────────── + ts = _ptp_strip(tpds.get("time-source", "")) + if ts: + print(f" {'Time source':<{W}}: {ts}") + + if dds.get("time-receiver-only"): + print(f" {'Mode':<{W}}: time-receiver only") + + # ── Time properties ─────────────────────────────────────────────────── + ptp_ts = "yes" if tpds.get("ptp-timescale") else "no" + t_trace = "yes" if tpds.get("time-traceable") else "no" + f_trace = "yes" if tpds.get("frequency-traceable") else "no" + utc_off = tpds.get("current-utc-offset") + utc_str = f"{utc_off} s" if utc_off is not None else "N/A" + print(f" {'PTP timescale':<{W}}: {ptp_ts}") + print(f" {'UTC offset':<{W}}: {utc_str}") + print(f" {'Time traceable':<{W}}: {t_trace}") + print(f" {'Freq. traceable':<{W}}: {f_trace}") + + # ── Sync status ─────────────────────────────────────────────────────── + offset = _ptp_ns(cds.get("offset-from-time-transmitter")) + delay = _ptp_ns(cds.get("mean-delay")) + steps = cds.get("steps-removed") + if offset is not None: + print(f" {'Offset from GM':<{W}}: {offset} ns") + if delay is not None: + print(f" {'Mean path delay':<{W}}: {delay} ns") + if steps is not None: + print(f" {'Steps removed':<{W}}: {steps}") + + # ── Ports ───────────────────────────────────────────────────────────── + if ports: + print() + Decore.title("Ports", width=rule_w) + + port_table = SimpleTable([ + Column("PORT", align='right'), + Column("INTERFACE", flexible=True), + Column("STATE", flexible=True), + Column("DELAY"), + Column("LINK DELAY (ns)", align='right'), + ]) + stats_table = SimpleTable([ + Column("PORT", align='right'), + Column("INTERFACE", flexible=True), + Column("SYNC \u25bc", align='right'), + Column("SYNC \u25b2", align='right'), + Column("ANN \u25bc", align='right'), + Column("ANN \u25b2", align='right'), + Column("PD \u25bc", align='right'), + Column("PD \u25b2", align='right'), + ]) + has_stats = False + + for port in ports: + pidx = port.get("port-index", "?") + pds_ = port.get("port-ds", {}) + iface = port.get("underlying-interface", + pds_.get("port-identity", {}).get("clock-identity", "?")) + + state_raw = pds_.get("port-state", "?") + color_fn = _PTP_PORT_STATE_COLOR.get(state_raw, lambda x: x) + state_str = color_fn(state_raw) + + dm = (pds_.get("delay-mechanism") or "?").upper() + mld = _ptp_ns(pds_.get("mean-link-delay")) + mld_str = str(mld) if mld is not None else "" + + port_table.row(str(pidx), iface, state_str, dm, mld_str) + + st = port.get("ieee802-dot1as-gptp:port-statistics-ds", {}) + if st: + has_stats = True + stats_table.row( + str(pidx), iface, + str(st.get("rx-sync-count", 0)), + str(st.get("tx-sync-count", 0)), + str(st.get("rx-announce-count", 0)), + str(st.get("tx-announce-count", 0)), + str(st.get("rx-pdelay-req-count", 0)), + str(st.get("tx-pdelay-req-count", 0)), + ) + + port_table.adjust_padding(rule_w) + port_table.print() + + if has_stats: + print() + Decore.title("Message Statistics (\u25bc\u202frx \u25b2\u202ftx)", width=rule_w) + stats_table.adjust_padding(rule_w) + stats_table.print() + + print() + + def main(): global UNIT_TEST @@ -5695,6 +5892,9 @@ def main(): ks_parser.add_argument('-t', '--type', help='Key type (symmetric or asymmetric)') ks_parser.add_argument('-n', '--name', help='Key name') + subparsers.add_parser('show-ptp', help='Show PTP instance status') \ + .add_argument('instance', nargs='?', help='Instance index (optional)') + subparsers.add_parser('show-ntp', help='Show NTP status') \ .add_argument('-a', '--address', help='Show details for specific address') subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status') @@ -5768,6 +5968,8 @@ def main(): show_nacm_user(json_data) elif args.command == "show-keystore": show_keystore(json_data, getattr(args, 'type', None), args.name) + elif args.command == "show-ptp": + show_ptp(json_data, getattr(args, 'instance', None)) elif args.command == "show-ntp": show_ntp(json_data, args.address) elif args.command == "show-ntp-tracking": diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index f7097c078..c88d4648f 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -123,6 +123,9 @@ def main(): elif model == 'ietf-bfd-ip-sh': from . import ietf_bfd_ip_sh yang_data = ietf_bfd_ip_sh.operational() + elif model == 'ieee1588-ptp-tt': + from . import ieee1588_ptp + yang_data = ieee1588_ptp.operational() else: common.LOG.warning("Unsupported model %s", model) sys.exit(1) diff --git a/src/statd/python/yanger/ieee1588_ptp.py b/src/statd/python/yanger/ieee1588_ptp.py new file mode 100644 index 000000000..8236ffed9 --- /dev/null +++ b/src/statd/python/yanger/ieee1588_ptp.py @@ -0,0 +1,624 @@ +"""Operational data for ieee1588-ptp-tt (and ieee802-dot1as-gptp). + +Queries each running ptp4l instance via pmc and maps the output to the +YANG model structure. One ptp4l process runs per instance-index, with +its config at /etc/linuxptp/ptp4l-.conf and its UDS socket at +/var/run/ptp4l-. +""" + +import glob +import os +import re + +from .common import insert +from .host import HOST + + +# --------------------------------------------------------------------------- +# pmc helpers +# --------------------------------------------------------------------------- + +def _pmc_get(conf, command): + """Run 'pmc -b 0 -f GET ' and return parsed key→value dict. + + pmc output looks like: + \t\t + Blank lines and lines not starting with whitespace are ignored. + Multiple response blocks (one per port for PORT_DATA_SET) each get + their own dict; returns a list of dicts in that case. + """ + lines = HOST.run_multiline( + ["pmc", "-u", "-b", "0", "-f", conf, f"GET {command}"], default=[]) + + blocks = [] + current = {} + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("sending:") or \ + "RESPONSE MANAGEMENT" in stripped or \ + "SIGNALING" in stripped: + if current: + blocks.append(current) + current = {} + continue + m = re.match(r'^\s+(\S+)\s+(.+)$', line) + if m: + current[m.group(1)] = m.group(2).strip() + + if current: + blocks.append(current) + + return blocks + + +def _pmc_get_one(conf, command): + """Like _pmc_get but return only the first (or only) block.""" + blocks = _pmc_get(conf, command) + return blocks[0] if blocks else {} + + +# --------------------------------------------------------------------------- +# clockIdentity formatting +# --------------------------------------------------------------------------- + +def _fmt_clock_identity(raw): + """Convert pmc clockIdentity 'aabbcc.fffe.ddeeff' to YANG format 'AA-BB-CC-FF-FE-DD-EE-FF'. + + The YANG typedef clock-identity requires the pattern [0-9A-F]{2}(-[0-9A-F]{2}){7}. + pmc outputs in its own dotted notation e.g. '005182.fffe.112202'. + """ + raw = raw.replace(".", "").replace("-", "").replace(":", "").upper() + if len(raw) == 16: + return "-".join(raw[i:i+2] for i in range(0, 16, 2)) + return raw + + +def _fmt_port_identity(raw): + """Convert 'aabbccfffe001122-1' to dict {clock-identity, port-number}.""" + parts = raw.rsplit("-", 1) + cid = _fmt_clock_identity(parts[0]) if parts else raw + pnum = int(parts[1]) if len(parts) == 2 else 0 + return {"clock-identity": cid, "port-number": pnum} + + +# --------------------------------------------------------------------------- +# clockAccuracy identity mapping +# --------------------------------------------------------------------------- + +# Map clockClass decimal values to ieee1588-ptp-tt identity names (identityref, not uint8). +_CLOCK_CLASS_MAP = { + 6: "ieee1588-ptp-tt:cc-primary-sync", + 7: "ieee1588-ptp-tt:cc-primary-sync-lost", + 13: "ieee1588-ptp-tt:cc-application-specific-sync", + 14: "ieee1588-ptp-tt:cc-application-specific-sync-lost", + 52: "ieee1588-ptp-tt:cc-primary-sync-alternative-a", + 58: "ieee1588-ptp-tt:cc-application-specific-alternative-a", + 187: "ieee1588-ptp-tt:cc-primary-sync-alternative-b", + 193: "ieee1588-ptp-tt:cc-application-specific-alternative-b", + 248: "ieee1588-ptp-tt:cc-default", + 255: "ieee1588-ptp-tt:cc-time-receiver-only", +} + + +def _clock_class_identity(raw): + """Return the YANG identity string for a pmc clockClass decimal value, or None.""" + try: + return _CLOCK_CLASS_MAP.get(int(raw)) + except (ValueError, TypeError): + return None + + +# Map clockAccuracy hex values to ieee1588-ptp-tt identity names (identityref, not uint8). +# Identity names use the 'ca-' prefix as defined in ieee1588-ptp-tt@2023-08-14.yang. +# 0xfe (unknown) has no corresponding identity and is omitted by returning None. +_CLOCK_ACCURACY_MAP = { + 0x17: "ieee1588-ptp-tt:ca-time-accurate-to-1000-fs", + 0x18: "ieee1588-ptp-tt:ca-time-accurate-to-2500-fs", + 0x19: "ieee1588-ptp-tt:ca-time-accurate-to-10-ps", + 0x1a: "ieee1588-ptp-tt:ca-time-accurate-to-25ps", + 0x1b: "ieee1588-ptp-tt:ca-time-accurate-to-100-ps", + 0x1c: "ieee1588-ptp-tt:ca-time-accurate-to-250-ps", + 0x1d: "ieee1588-ptp-tt:ca-time-accurate-to-1000-ps", + 0x1e: "ieee1588-ptp-tt:ca-time-accurate-to-2500-ps", + 0x1f: "ieee1588-ptp-tt:ca-time-accurate-to-10-ns", + 0x20: "ieee1588-ptp-tt:ca-time-accurate-to-25-ns", + 0x21: "ieee1588-ptp-tt:ca-time-accurate-to-100-ns", + 0x22: "ieee1588-ptp-tt:ca-time-accurate-to-250-ns", + 0x23: "ieee1588-ptp-tt:ca-time-accurate-to-1000-ns", + 0x24: "ieee1588-ptp-tt:ca-time-accurate-to-2500-ns", + 0x25: "ieee1588-ptp-tt:ca-time-accurate-to-10-us", + 0x26: "ieee1588-ptp-tt:ca-time-accurate-to-25-us", + 0x27: "ieee1588-ptp-tt:ca-time-accurate-to-100-us", + 0x28: "ieee1588-ptp-tt:ca-time-accurate-to-250-us", + 0x29: "ieee1588-ptp-tt:ca-time-accurate-to-1000-us", + 0x2a: "ieee1588-ptp-tt:ca-time-accurate-to-2500-us", + 0x2b: "ieee1588-ptp-tt:ca-time-accurate-to-10-ms", + 0x2c: "ieee1588-ptp-tt:ca-time-accurate-to-25-ms", + 0x2d: "ieee1588-ptp-tt:ca-time-accurate-to-100-ms", + 0x2e: "ieee1588-ptp-tt:ca-time-accurate-to-250-ms", + 0x2f: "ieee1588-ptp-tt:ca-time-accurate-to-1-s", + 0x30: "ieee1588-ptp-tt:ca-time-accurate-to-10-s", + 0x31: "ieee1588-ptp-tt:ca-time-accurate-to-gt-10-s", +} + + +def _clock_accuracy_identity(raw): + """Return the YANG identity string for a pmc clockAccuracy hex value, or None.""" + try: + return _CLOCK_ACCURACY_MAP.get(int(raw, 16)) + except (ValueError, TypeError): + return None + + +# --------------------------------------------------------------------------- +# time-source identity mapping +# --------------------------------------------------------------------------- + +_TIME_SOURCE_MAP = { + "0x10": "ieee1588-ptp-tt:atomic-clock", + "0x20": "ieee1588-ptp-tt:gnss", + "0x30": "ieee1588-ptp-tt:terrestrial-radio", + "0x39": "ieee1588-ptp-tt:serial-time-code", + "0x40": "ieee1588-ptp-tt:ptp", + "0x50": "ieee1588-ptp-tt:ntp", + "0x60": "ieee1588-ptp-tt:hand-set", + "0x90": "ieee1588-ptp-tt:other", + "0xa0": "ieee1588-ptp-tt:internal-oscillator", +} + + +def _time_source_identity(raw): + return _TIME_SOURCE_MAP.get(raw.lower(), + "ieee1588-ptp-tt:internal-oscillator") + + +# --------------------------------------------------------------------------- +# delay-mechanism and port-state mapping +# --------------------------------------------------------------------------- + +_DELAY_MECH_MAP = { + "E2E": "e2e", + "P2P": "p2p", + "AUTO": "no-mechanism", +} + +_PORT_STATE_MAP = { + "INITIALIZING": "initializing", + "FAULTY": "faulty", + "DISABLED": "disabled", + "LISTENING": "listening", + "PRE_MASTER": "pre-time-transmitter", + "MASTER": "time-transmitter", + "PASSIVE": "passive", + "UNCALIBRATED": "uncalibrated", + "SLAVE": "time-receiver", + "GRAND_MASTER": "time-transmitter", +} + + +# --------------------------------------------------------------------------- +# Per-dataset builders +# --------------------------------------------------------------------------- + +def _build_default_ds(d): + """Map pmc DEFAULT_DATA_SET response to YANG default-ds.""" + ds = {} + + cid = d.get("clockIdentity") + if cid: + ds["clock-identity"] = _fmt_clock_identity(cid) + + v = d.get("numberPorts") + if v: + ds["number-ports"] = int(v) + + cq = {} + v = d.get("clockClass") + if v: + cc = _clock_class_identity(v) + if cc: + cq["clock-class"] = cc + v = d.get("clockAccuracy") + if v: + ca = _clock_accuracy_identity(v) + if ca: + cq["clock-accuracy"] = ca + v = d.get("offsetScaledLogVariance") + if v: + cq["offset-scaled-log-variance"] = int(v, 16) + if cq: + ds["clock-quality"] = cq + + v = d.get("priority1") + if v: + ds["priority1"] = int(v) + v = d.get("priority2") + if v: + ds["priority2"] = int(v) + + v = d.get("domainNumber") + if v: + ds["domain-number"] = int(v) + + v = d.get("slaveOnly") + if v is not None: + ds["time-receiver-only"] = (v == "1") + + # instance-type: derive from ptp4l GM/time-receiver state (read-only, operational) + # pmc doesn't directly expose clockType in DEFAULT_DATA_SET + # We'll fill instance-type later from the instance's config if possible + + return ds + + +def _build_current_ds(d): + """Map pmc CURRENT_DATA_SET response to YANG current-ds.""" + ds = {} + + v = d.get("stepsRemoved") + if v: + ds["steps-removed"] = int(v) + + v = d.get("offsetFromMaster") + if v: + # ptp4l reports nanoseconds as float; YANG time-interval is ns * 2^16. + # RFC 7951: int64 must be JSON-encoded as a string. + try: + ds["offset-from-time-transmitter"] = str(int(float(v) * 65536)) + except ValueError: + pass + + v = d.get("meanPathDelay") + if v: + try: + ds["mean-delay"] = str(int(float(v) * 65536)) + except ValueError: + pass + + return ds + + +def _build_parent_ds(d): + """Map pmc PARENT_DATA_SET response to YANG parent-ds.""" + ds = {} + + v = d.get("parentPortIdentity") + if v: + ds["parent-port-identity"] = _fmt_port_identity(v) + + v = d.get("parentStats") + if v: + ds["parent-stats"] = (v == "1") + + v = d.get("observedParentOffsetScaledLogVariance") + if v: + try: + ds["observed-parent-offset-scaled-log-variance"] = int(v, 16) + except ValueError: + pass + + v = d.get("observedParentClockPhaseChangeRate") + if v: + try: + ds["observed-parent-clock-phase-change-rate"] = int(v) + except ValueError: + pass + + v = d.get("grandmasterIdentity") + if v: + ds["grandmaster-identity"] = _fmt_clock_identity(v) + + gcq = {} + v = d.get("gm.ClockClass") + if v: + cc = _clock_class_identity(v) + if cc: + gcq["clock-class"] = cc + v = d.get("gm.ClockAccuracy") + if v: + ca = _clock_accuracy_identity(v) + if ca: + gcq["clock-accuracy"] = ca + v = d.get("gm.OffsetScaledLogVariance") + if v: + try: + gcq["offset-scaled-log-variance"] = int(v, 16) + except ValueError: + pass + if gcq: + ds["grandmaster-clock-quality"] = gcq + + v = d.get("grandmasterPriority1") + if v: + ds["grandmaster-priority1"] = int(v) + v = d.get("grandmasterPriority2") + if v: + ds["grandmaster-priority2"] = int(v) + + return ds + + +def _build_time_properties_ds(d): + """Map pmc TIME_PROPERTIES_DATA_SET response to YANG time-properties-ds.""" + ds = {} + + # current-utc-offset has a when condition requiring current-utc-offset-valid='true' + if d.get("currentUtcOffsetValid") in ("1", "true"): + v = d.get("currentUtcOffset") + if v: + ds["current-utc-offset"] = int(v) + + v = d.get("leap61") + if v is not None: + ds["leap61"] = (v == "1") + v = d.get("leap59") + if v is not None: + ds["leap59"] = (v == "1") + v = d.get("currentUtcOffsetValid") + if v is not None: + ds["current-utc-offset-valid"] = (v == "1") + v = d.get("ptpTimescale") + if v is not None: + ds["ptp-timescale"] = (v == "1") + v = d.get("timeTraceable") + if v is not None: + ds["time-traceable"] = (v == "1") + v = d.get("frequencyTraceable") + if v is not None: + ds["frequency-traceable"] = (v == "1") + + v = d.get("timeSource") + if v: + ds["time-source"] = _time_source_identity(v) + + return ds + + +def _build_port_ds(d): + """Map pmc PORT_DATA_SET response to YANG port-ds.""" + ds = {} + + v = d.get("portIdentity") + if v: + ds["port-identity"] = _fmt_port_identity(v) + + v = d.get("portState") + if v: + ds["port-state"] = _PORT_STATE_MAP.get(v, "disabled") + + v = d.get("logMinDelayReqInterval") + if v: + try: + ds["log-min-delay-req-interval"] = int(v) + except ValueError: + pass + + v = d.get("peerMeanPathDelay") + if v: + try: + # RFC 7951: int64 must be JSON-encoded as a string. + ds["mean-link-delay"] = str(int(float(v) * 65536)) + except ValueError: + pass + + v = d.get("logAnnounceInterval") + if v: + try: + ds["log-announce-interval"] = int(v) + except ValueError: + pass + + v = d.get("announceReceiptTimeout") + if v: + try: + ds["announce-receipt-timeout"] = int(v) + except ValueError: + pass + + v = d.get("logSyncInterval") + if v: + try: + ds["log-sync-interval"] = int(v) + except ValueError: + pass + + v = d.get("delayMechanism") + if v: + ds["delay-mechanism"] = _DELAY_MECH_MAP.get(v, "e2e") + + v = d.get("logMinPdelayReqInterval") + if v: + try: + ds["log-min-pdelay-req-interval"] = int(v) + except ValueError: + pass + + v = d.get("versionNumber") + if v: + try: + ds["version-number"] = int(v) + except ValueError: + pass + + v = d.get("portEnable") + if v is not None: + ds["port-enable"] = (v == "1") + + return ds + + +def _build_port_stats(d): + """Map pmc PORT_STATS_NP response to ieee802-dot1as-gptp port-statistics-ds.""" + stats = {} + mapping = { + "rx_Sync": "rx-sync-count", + "rx_Follow_Up": "rx-follow-up-count", + "rx_Pdelay_Req": "rx-pdelay-req-count", + "rx_Pdelay_Resp": "rx-pdelay-resp-count", + "rx_Pdelay_Resp_Follow_Up": "rx-pdelay-resp-follow-up-count", + "rx_Announce": "rx-announce-count", + "tx_Sync": "tx-sync-count", + "tx_Follow_Up": "tx-follow-up-count", + "tx_Pdelay_Req": "tx-pdelay-req-count", + "tx_Pdelay_Resp": "tx-pdelay-resp-count", + "tx_Pdelay_Resp_Follow_Up": "tx-pdelay-resp-follow-up-count", + "tx_Announce": "tx-announce-count", + } + for pmc_key, yang_key in mapping.items(): + v = d.get(pmc_key) + if v is not None: + try: + stats[yang_key] = int(v) + except ValueError: + pass + return stats + + +# --------------------------------------------------------------------------- +# Per-instance builder +# --------------------------------------------------------------------------- + +def _port_interfaces(conf_path): + """Return ordered list of interface names from ptp4l conf (non-global section headers).""" + ifaces = [] + try: + with open(conf_path) as f: + for line in f: + s = line.strip() + if s.startswith('[') and s.endswith(']') and s[1:-1] != 'global': + ifaces.append(s[1:-1]) + except OSError: + pass + return ifaces + + +def _instance_type_from_config(conf_path): + """Read instance-type from a saved config file (best effort).""" + try: + with open(conf_path, "r") as f: + for line in f: + m = re.match(r'\s*clockType\s+(\S+)', line) + if m: + ct = m.group(1).upper() + if ct == "P2P_TC": + return "p2p-tc" + if ct == "E2E_TC": + return "e2e-tc" + if ct == "BOUNDARY_CLOCK": + return "bc" + # Default: if more than one port, bc; otherwise oc + # (approximation — proper detection requires DEFAULT_DATA_SET numberPorts) + except OSError: + pass + return None + + +def _build_instance(idx, conf_path): + """Build one instance dict from pmc queries for instance index idx.""" + inst = {"instance-index": idx} + + # default-ds + dd = _pmc_get_one(conf_path, "DEFAULT_DATA_SET") + if dd: + dds = _build_default_ds(dd) + # Derive instance-type from numberPorts + config file + num_ports = int(dd.get("numberPorts", "0") or "0") + it = _instance_type_from_config(conf_path) + if it is None: + it = "bc" if num_ports > 1 else "oc" + dds["instance-type"] = it + inst["default-ds"] = dds + + # current-ds + cd = _pmc_get_one(conf_path, "CURRENT_DATA_SET") + if cd: + cds = _build_current_ds(cd) + if cds: + inst["current-ds"] = cds + + # parent-ds + pd = _pmc_get_one(conf_path, "PARENT_DATA_SET") + if pd: + pds = _build_parent_ds(pd) + if pds: + inst["parent-ds"] = pds + + # time-properties-ds + tp = _pmc_get_one(conf_path, "TIME_PROPERTIES_DATA_SET") + if tp: + tpds = _build_time_properties_ds(tp) + if tpds: + inst["time-properties-ds"] = tpds + + # ports: PORT_DATA_SET returns one block per port + port_blocks = _pmc_get(conf_path, "PORT_DATA_SET") + stats_blocks = _pmc_get(conf_path, "PORT_STATS_NP") + ifaces = _port_interfaces(conf_path) + + # Build a stats map keyed by portIdentity for quick lookup + stats_by_id = {} + for sb in stats_blocks: + pid = sb.get("portIdentity") + if pid: + stats_by_id[pid] = _build_port_stats(sb) + + ports = [] + for i, pb in enumerate(port_blocks, start=1): + pid_raw = pb.get("portIdentity", "") + port_entry = {} + + # port-index = port number from portIdentity + pid_dict = _fmt_port_identity(pid_raw) + port_entry["port-index"] = pid_dict.get("port-number", i) + + if i <= len(ifaces): + port_entry["underlying-interface"] = ifaces[i - 1] + + pds = _build_port_ds(pb) + if pds: + port_entry["port-ds"] = pds + + # 802.1AS port-statistics-ds + stats = stats_by_id.get(pid_raw) + if stats: + port_entry["ieee802-dot1as-gptp:port-statistics-ds"] = stats + + ports.append(port_entry) + + if ports: + inst["ports"] = {"port": ports} + + return inst + + +# --------------------------------------------------------------------------- +# Top-level entry point +# --------------------------------------------------------------------------- + +def operational(): + """Return operational data for ieee1588-ptp-tt.""" + out = {} + instances = [] + + conf_files = sorted(glob.glob("/etc/linuxptp/ptp4l-*.conf")) + for conf_path in conf_files: + m = re.search(r'ptp4l-(\d+)\.conf$', conf_path) + if not m: + continue + idx = int(m.group(1)) + + # Only include instances with a live UDS socket (i.e. ptp4l running) + uds_path = f"/var/run/ptp4l-{idx}" + if not HOST.exists(uds_path): + continue + + try: + inst = _build_instance(idx, conf_path) + instances.append(inst) + except Exception: + pass # ptp4l may have just started; skip silently + + if instances: + insert(out, "ieee1588-ptp-tt:ptp", "instances", "instance", instances) + + return out diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index 60665e0ce..4f99c8769 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -117,9 +117,38 @@ def interface_common(iplink, ipaddr): return interface -def interface(iplink, ipaddr): +def ptp_capabilities(ifname, systemjson): + """Return infix-interfaces:ptp-capabilities dict for ifname, or None.""" + caps = systemjson.get("interfaces", {}).get(ifname, {}).get("ptp-capabilities") + if not caps: + return None + + result = {} + if cl := caps.get("capabilities"): + result["capabilities"] = cl + if (phc := caps.get("phc-index")) is not None: + result["phc-index"] = phc + if tx := caps.get("tx-types"): + result["tx-types"] = tx + if rx := caps.get("rx-filters"): + result["rx-filters"] = rx + if (idx := caps.get("hwtstamp-provider-index")) is not None: + result["hwtstamp-provider-index"] = idx + if qual := caps.get("hwtstamp-provider-qualifier"): + result["hwtstamp-provider-qualifier"] = qual + + return result or None + + +def interface(iplink, ipaddr, systemjson=None): interface = interface_common(iplink, ipaddr) + if systemjson is None: + systemjson = {} + + if ptpcap := ptp_capabilities(iplink["ifname"], systemjson): + interface["infix-interfaces:ptp-capabilities"] = ptpcap + match interface["type"]: case "infix-if-type:bridge": if br := bridge.bridge(iplink): @@ -163,8 +192,11 @@ def interface(iplink, ipaddr): def interfaces(ifname=None): + from ..host import HOST + links = common.iplinks(ifname) addrs = common.ipaddrs(ifname) + systemjson = HOST.read_json("/run/system.json", {}) interfaces = [] for ifname, iplink in links.items(): @@ -177,6 +209,6 @@ def interfaces(ifname=None): ipaddr = addrs.get(ifname, {}) - interfaces.append(interface(iplink, ipaddr)) + interfaces.append(interface(iplink, ipaddr, systemjson)) return interfaces diff --git a/src/statd/statd.c b/src/statd/statd.c index affc90740..dac055836 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -52,6 +52,7 @@ #define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp" #define XPATH_FIREWALL_BASE "/infix-firewall:firewall" #define XPATH_NTP_BASE "/ietf-ntp:ntp" +#define XPATH_PTP_BASE "/ieee1588-ptp-tt:ptp" TAILQ_HEAD(sub_head, sub); @@ -111,7 +112,7 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent err = lyd_parse_data_fd(ctx, fd, LYD_JSON, LYD_PARSE_ONLY, 0, parent); if (err) - ERROR("Error, parsing yanger data (%d)", err); + ERROR("Error, parsing yanger data (%d): %s", err, ly_errmsg(ctx)); fclose(stream); /* Note: fclose() already closes the underlying fd from fdopen() */ @@ -455,6 +456,8 @@ static int subscribe_to_all(struct statd *statd) return SR_ERR_INTERNAL; if (subscribe(statd, "ietf-ntp", XPATH_NTP_BASE, sr_generic_cb)) return SR_ERR_INTERNAL; + if (subscribe(statd, "ieee1588-ptp-tt", XPATH_PTP_BASE, sr_generic_cb)) + return SR_ERR_INTERNAL; INFO("Successfully subscribed to all models"); return SR_ERR_OK; From f3fcac0107d110193feb9c365408d8c3e01956ca Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 18 Apr 2026 07:14:28 +0200 Subject: [PATCH 09/11] Add phc2sys companion service for BC/TC instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On multi-chip DSA hardware (e.g., boards with multiple mv88e6xxx chips), each switch chip has its own independent PHC device. With boundary_clock_jbod enabled, ptp4l starts but only disciplines the active slave port's PHC — the others drift. Automatically start phc2sys -a alongside any BC or TC instance using hardware timestamping. It subscribes to ptp4l's UDS socket, tracks BMCA, and disciplines all non-active PHCs to match the active one. On single-chip hardware it is a harmless no-op. CLOCK_REALTIME is intentionally left untouched. Syncing the system clock to PTP (phc2sys -rr), feeding the PHC from GPS/NTP (ts2phc, phc2sys reverse), and full multi-source coordination (timemaster) are planned as follow-on phases; see the issue tracker for the roadmap. Signed-off-by: Joachim Wiberg --- .../etc/finit.d/available/phc2sys@.conf | 3 + src/confd/src/ptp.c | 81 ++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 board/common/rootfs/etc/finit.d/available/phc2sys@.conf diff --git a/board/common/rootfs/etc/finit.d/available/phc2sys@.conf b/board/common/rootfs/etc/finit.d/available/phc2sys@.conf new file mode 100644 index 000000000..3dfe8e508 --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/phc2sys@.conf @@ -0,0 +1,3 @@ +service name:phc2sys :%i log:prio:daemon,tag:phc2sys-%i \ + [2345] phc2sys -a -z /var/run/ptp4l-%i \ + -- PHC synchronization for PTP instance %i diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c index 4b97575d4..bd51b7516 100644 --- a/src/confd/src/ptp.c +++ b/src/confd/src/ptp.c @@ -291,6 +291,79 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) return SR_ERR_OK; } +/* + * True when a PTP instance needs a phc2sys companion to keep all its + * PHC devices in sync. Required for BC and TC instances on hardware + * with multiple switch chips, where each chip has its own /dev/ptpN. + * On single-chip hardware the function is a no-op: phc2sys -a finds + * no second PHC and exits immediately. OC has one port → one PHC, + * so no sync is ever needed. + */ +static bool needs_phc2sys(struct lyd_node *inst, json_t *root) +{ + struct lyd_node *default_ds = lydx_get_child(inst, "default-ds"); + const char *type = lydx_get_cattr(default_ds, "instance-type"); + + if (!type) + return false; + if (strcmp(type, "bc") && strcmp(type, "p2p-tc") && strcmp(type, "e2e-tc")) + return false; + return !strcmp(instance_time_stamping(inst, root), "hardware"); +} + +/* + * Enable the phc2sys@ companion service for a multi-port HW instance. + * phc2sys -a subscribes to ptp4l's UDS, discovers the active slave + * port via BMCA, and disciplines all other PHCs to match it. + * No config file is needed — the UDS path is passed on the command line. + */ +static void activate_phc2sys(uint16_t idx) +{ + finit_enablef("phc2sys@%u", idx); + finit_reloadf("phc2sys@%u", idx); +} + +static void deactivate_phc2sys(uint16_t idx) +{ + finit_disablef("phc2sys@%u", idx); +} + +/* + * Disable any phc2sys@ services whose index is no longer configured. + */ +static void cleanup_stale_phc2sys(struct lyd_node *config) +{ + struct lyd_node *inst; + struct dirent *ent; + DIR *d; + int idx; + + d = opendir(FINIT_RCSD "/enabled"); + if (!d) + return; + + while ((ent = readdir(d))) { + bool found = false; + + if (sscanf(ent->d_name, "phc2sys@%d.conf", &idx) != 1) + continue; + + LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL), + inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (v && atoi(v) == idx) { + found = true; + break; + } + } + + if (!found) + deactivate_phc2sys((uint16_t)idx); + } + + closedir(d); +} + /* * Remove staging config for one instance. */ @@ -410,12 +483,18 @@ static int change(sr_session_ctx_t *session, struct lyd_node *config, const char *v = lydx_get_cattr(inst, "instance-index"); if (!v) continue; - if ((rc = activate_instance((uint16_t)atoi(v)))) + uint16_t idx = (uint16_t)atoi(v); + if ((rc = activate_instance(idx))) return rc; + if (needs_phc2sys(inst, confd->root)) + activate_phc2sys(idx); + else + deactivate_phc2sys(idx); } /* Disable stale services not in current config */ cleanup_stale_instances(config); + cleanup_stale_phc2sys(config); if (!instances) return SR_ERR_OK; From 86700c41e6f51610494db0b7bdc252de696d9e07 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 13 Apr 2026 18:19:00 +0200 Subject: [PATCH 10/11] test: add some basic ptp tests Signed-off-by: Joachim Wiberg --- doc/testing.md | 45 +++++ src/confd/src/ptp.c | 22 ++- test/case/all.yaml | 3 + test/case/ptp/Readme.adoc | 46 +++++ test/case/ptp/all.yaml | 15 ++ test/case/ptp/basic/Readme.adoc | 6 + test/case/ptp/basic/ieee1588.adoc | 28 +++ test/case/ptp/basic/ieee1588.py | 1 + test/case/ptp/basic/ieee802dot1as.adoc | 28 +++ test/case/ptp/basic/ieee802dot1as.py | 1 + test/case/ptp/basic/test.adoc | 29 +++ test/case/ptp/basic/test.py | 107 +++++++++++ test/case/ptp/basic/test.yaml | 11 ++ test/case/ptp/basic/topology.dot | 33 ++++ test/case/ptp/basic/topology.svg | 62 ++++++ test/case/ptp/bmca/Readme.adoc | 6 + test/case/ptp/bmca/ieee1588.adoc | 39 ++++ test/case/ptp/bmca/ieee1588.py | 1 + test/case/ptp/bmca/ieee802dot1as.adoc | 39 ++++ test/case/ptp/bmca/ieee802dot1as.py | 1 + test/case/ptp/bmca/test.adoc | 5 + test/case/ptp/bmca/test.py | 113 +++++++++++ test/case/ptp/bmca/test.yaml | 11 ++ test/case/ptp/bmca/topology.dot | 33 ++++ test/case/ptp/bmca/topology.svg | 60 ++++++ test/case/ptp/boundary_clock/Readme.adoc | 6 + test/case/ptp/boundary_clock/ieee1588.adoc | 43 +++++ test/case/ptp/boundary_clock/ieee1588.py | 1 + .../ptp/boundary_clock/ieee802dot1as.adoc | 43 +++++ test/case/ptp/boundary_clock/ieee802dot1as.py | 1 + test/case/ptp/boundary_clock/test.adoc | 5 + test/case/ptp/boundary_clock/test.py | 179 ++++++++++++++++++ test/case/ptp/boundary_clock/test.yaml | 11 ++ test/case/ptp/boundary_clock/topology.dot | 42 ++++ test/case/ptp/boundary_clock/topology.svg | 90 +++++++++ test/case/ptp/port_recovery/Readme.adoc | 1 + test/case/ptp/port_recovery/test.adoc | 32 ++++ test/case/ptp/port_recovery/test.py | 118 ++++++++++++ test/case/ptp/port_recovery/topology.dot | 33 ++++ test/case/ptp/port_recovery/topology.svg | 62 ++++++ test/case/ptp/transparent_clock/Readme.adoc | 10 + test/case/ptp/transparent_clock/e2e.adoc | 44 +++++ test/case/ptp/transparent_clock/e2e.py | 1 + .../ptp/transparent_clock/ieee802dot1as.adoc | 44 +++++ .../ptp/transparent_clock/ieee802dot1as.py | 1 + test/case/ptp/transparent_clock/p2p.adoc | 44 +++++ test/case/ptp/transparent_clock/p2p.py | 1 + test/case/ptp/transparent_clock/test.adoc | 46 +++++ test/case/ptp/transparent_clock/test.py | 176 +++++++++++++++++ test/case/ptp/transparent_clock/test.yaml | 15 ++ test/case/ptp/transparent_clock/topology.dot | 42 ++++ test/case/ptp/transparent_clock/topology.svg | 90 +++++++++ test/infamy/ptp.py | 105 ++++++++++ test/spec/Readme.adoc.in | 4 + 54 files changed, 2029 insertions(+), 6 deletions(-) create mode 100644 test/case/ptp/Readme.adoc create mode 100644 test/case/ptp/all.yaml create mode 100644 test/case/ptp/basic/Readme.adoc create mode 100644 test/case/ptp/basic/ieee1588.adoc create mode 120000 test/case/ptp/basic/ieee1588.py create mode 100644 test/case/ptp/basic/ieee802dot1as.adoc create mode 120000 test/case/ptp/basic/ieee802dot1as.py create mode 100644 test/case/ptp/basic/test.adoc create mode 100755 test/case/ptp/basic/test.py create mode 100644 test/case/ptp/basic/test.yaml create mode 100644 test/case/ptp/basic/topology.dot create mode 100644 test/case/ptp/basic/topology.svg create mode 100644 test/case/ptp/bmca/Readme.adoc create mode 100644 test/case/ptp/bmca/ieee1588.adoc create mode 120000 test/case/ptp/bmca/ieee1588.py create mode 100644 test/case/ptp/bmca/ieee802dot1as.adoc create mode 120000 test/case/ptp/bmca/ieee802dot1as.py create mode 100644 test/case/ptp/bmca/test.adoc create mode 100755 test/case/ptp/bmca/test.py create mode 100644 test/case/ptp/bmca/test.yaml create mode 100644 test/case/ptp/bmca/topology.dot create mode 100644 test/case/ptp/bmca/topology.svg create mode 100644 test/case/ptp/boundary_clock/Readme.adoc create mode 100644 test/case/ptp/boundary_clock/ieee1588.adoc create mode 120000 test/case/ptp/boundary_clock/ieee1588.py create mode 100644 test/case/ptp/boundary_clock/ieee802dot1as.adoc create mode 120000 test/case/ptp/boundary_clock/ieee802dot1as.py create mode 100644 test/case/ptp/boundary_clock/test.adoc create mode 100755 test/case/ptp/boundary_clock/test.py create mode 100644 test/case/ptp/boundary_clock/test.yaml create mode 100644 test/case/ptp/boundary_clock/topology.dot create mode 100644 test/case/ptp/boundary_clock/topology.svg create mode 120000 test/case/ptp/port_recovery/Readme.adoc create mode 100644 test/case/ptp/port_recovery/test.adoc create mode 100755 test/case/ptp/port_recovery/test.py create mode 100644 test/case/ptp/port_recovery/topology.dot create mode 100644 test/case/ptp/port_recovery/topology.svg create mode 100644 test/case/ptp/transparent_clock/Readme.adoc create mode 100644 test/case/ptp/transparent_clock/e2e.adoc create mode 120000 test/case/ptp/transparent_clock/e2e.py create mode 100644 test/case/ptp/transparent_clock/ieee802dot1as.adoc create mode 120000 test/case/ptp/transparent_clock/ieee802dot1as.py create mode 100644 test/case/ptp/transparent_clock/p2p.adoc create mode 120000 test/case/ptp/transparent_clock/p2p.py create mode 100644 test/case/ptp/transparent_clock/test.adoc create mode 100755 test/case/ptp/transparent_clock/test.py create mode 100644 test/case/ptp/transparent_clock/test.yaml create mode 100644 test/case/ptp/transparent_clock/topology.dot create mode 100644 test/case/ptp/transparent_clock/topology.svg create mode 100644 test/infamy/ptp.py diff --git a/doc/testing.md b/doc/testing.md index eb3d14760..1fea58e3f 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -359,6 +359,51 @@ $ make test-spec ... ``` +### Node and Link Capabilities + +Logical topology files (`topology.dot`) declare what each node and link +*requires*; physical topology files declare what each node and link +*provides*. When mapping a logical topology to physical hardware, infamy +only assigns a physical node to a logical node when the physical node's +`provides` set is a superset of the logical node's `requires` set. Tests +are skipped if no matching physical topology can be found. + +#### Declaring requirements (logical topology) + +```dot +dut [ + requires="infix ptp-hwts", +]; + +host:data -- dut:data [requires="ieee-mc"] +``` + +#### Declaring capabilities (physical topology) + +```dot +switch1 [ + provides="infix ptp-hwts", +]; +``` + +#### Node capabilities + +| Capability | Meaning | +|-------------------|-------------------------------------------------------------------------| +| `controller` | Reserved for the host/controller node; never assigned to a DUT | +| `infix` | Node runs Infix OS — required by virtually all DUT nodes | +| `infix gps` | Node has a GPS receiver available as a time reference | +| `infix watchdog` | Node has a hardware watchdog device | +| `infix ptp-hwts` | Node has PTP Hardware Clock (PHC) support on its network interfaces, enabling hardware timestamping and sub-microsecond PTP convergence | + +#### Link capabilities + +| Capability | Meaning | +|-------------------|-------------------------------------------------------------------------| +| `mgmt` | Link is a management path (typically coloured grey in diagrams) | +| `ieee-mc` | Link carries IEEE multicast traffic (required by LAG and some L2 tests) | +| `link-ctrl copper`| Link supports copper speed/duplex control | + ### Test Development For adding a new test to the automated regression test suite, it's best diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c index bd51b7516..fcf2c47fb 100644 --- a/src/confd/src/ptp.c +++ b/src/confd/src/ptp.c @@ -39,7 +39,7 @@ static const char *instance_type_to_clock_type(const char *type) * gmCapable, follow_up_info, assume_two_step, path_trace_enabled, * and the tighter neighborPropDelayThresh. */ -static bool emit_profile_globals(FILE *fp, const char *profile) +static bool emit_profile_globals(FILE *fp, const char *profile, bool hw_ts) { bool dot1as = profile && !strcmp(profile, "ieee802-dot1as"); @@ -53,7 +53,16 @@ static bool emit_profile_globals(FILE *fp, const char *profile) fprintf(fp, "follow_up_info 1\n"); fprintf(fp, "assume_two_step 1\n"); fprintf(fp, "path_trace_enabled 1\n"); - fprintf(fp, "neighborPropDelayThresh 800\n"); + /* + * 802.1AS P2P gate: if meanLinkDelay exceeds this threshold the + * port stays asCapable=false and never leaves LISTENING. 800 ns + * is the 802.1AS default and is appropriate only for hardware + * timestamping — software timestamps (QEMU, tap interfaces) can + * easily produce peer delays in the microsecond range, which + * would keep both ports stuck in LISTENING indefinitely. + */ + if (hw_ts) + fprintf(fp, "neighborPropDelayThresh 800\n"); } else { fprintf(fp, "transportSpecific 0\n"); } @@ -130,7 +139,7 @@ static const char *instance_time_stamping(struct lyd_node *inst, json_t *root) static int write_instance_conf(struct lyd_node *inst, json_t *root) { struct lyd_node *default_ds, *port, *port_ds; - const char *instance_type, *clock_type, *profile; + const char *instance_type, *clock_type, *profile, *ts; const char *v; char path[256]; uint16_t idx; @@ -154,7 +163,7 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) default_ds = lydx_get_child(inst, "default-ds"); instance_type = lydx_get_cattr(default_ds, "instance-type"); - profile = lydx_get_cattr(default_ds, "infix-ptp:profile"); + profile = lydx_get_cattr(default_ds, "profile"); clock_type = instance_type_to_clock_type(instance_type); tc = (clock_type != NULL); @@ -164,10 +173,11 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) fprintf(fp, "uds_address /var/run/ptp4l-%u\n", idx); /* Timestamping mode: hardware if all ports support it, software otherwise */ - fprintf(fp, "time_stamping %s\n", instance_time_stamping(inst, root)); + ts = instance_time_stamping(inst, root); + fprintf(fp, "time_stamping %s\n", ts); /* Profile — sets transportSpecific and all protocol-mandatory options */ - dot1as = emit_profile_globals(fp, profile); + dot1as = emit_profile_globals(fp, profile, !strcmp(ts, "hardware")); /* domainNumber */ v = lydx_get_cattr(default_ds, "domain-number"); diff --git a/test/case/all.yaml b/test/case/all.yaml index 9177fcdf6..7b718cc6b 100644 --- a/test/case/all.yaml +++ b/test/case/all.yaml @@ -35,6 +35,9 @@ - name: "NTP Server" suite: ntp/all.yaml +- name: "PTP" + suite: ptp/all.yaml + - name: "Routing" suite: routing/all.yaml diff --git a/test/case/ptp/Readme.adoc b/test/case/ptp/Readme.adoc new file mode 100644 index 000000000..f47f2f3ae --- /dev/null +++ b/test/case/ptp/Readme.adoc @@ -0,0 +1,46 @@ +:testgroup: +== PTP Tests + +Tests for PTP (IEEE 1588 Precision Time Protocol) functionality across +different clock types and operational scenarios: + + - Basic smoke test: full stack end-to-end + - BTCA: grandmaster election and runtime re-election via `priority1` + - Boundary Clock: two-port BC forwarding time with `steps-removed` accounting + - Transparent Clock: E2E and P2P TC on hardware-timestamping nodes + - Port fault recovery: link-down/link-up state machine and re-convergence + +The offset convergence threshold in the tests varies with the timestamping +capability of the nodes under test: + +[cols="1,1,3",options="header"] +|=== +| Timestamping | Default Threshold | Typical scenario +| Software | 100 000 ns | Virtual machines, QEMU tap interfaces +| Hardware | 1 000 ns | Nodes with PHC hardware timestamping +|=== + +Tests that accept a `--threshold-ns` option may use that value instead. +When no option is given, the threshold is selected automatically based +on the `ptp-hwts` capability of the relevant node in the physical test +system's topology file. + +<<< + +include::basic/Readme.adoc[] + +<<< + +include::bmca/Readme.adoc[] + +<<< + +include::boundary_clock/Readme.adoc[] + +<<< + +include::transparent_clock/Readme.adoc[] + +<<< + +include::port_recovery/Readme.adoc[] diff --git a/test/case/ptp/all.yaml b/test/case/ptp/all.yaml new file mode 100644 index 000000000..e63a53d2e --- /dev/null +++ b/test/case/ptp/all.yaml @@ -0,0 +1,15 @@ +--- +- name: PTP basic + suite: basic/test.yaml + +- name: PTP BTCA grandmaster election + suite: bmca/test.yaml + +- name: PTP boundary clock + suite: boundary_clock/test.yaml + +- name: PTP transparent clock + suite: transparent_clock/test.yaml + +- name: PTP port fault recovery + case: port_recovery/test.py diff --git a/test/case/ptp/basic/Readme.adoc b/test/case/ptp/basic/Readme.adoc new file mode 100644 index 000000000..cb3079ad3 --- /dev/null +++ b/test/case/ptp/basic/Readme.adoc @@ -0,0 +1,6 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/basic/ieee1588.adoc b/test/case/ptp/basic/ieee1588.adoc new file mode 100644 index 000000000..bfd582fee --- /dev/null +++ b/test/case/ptp/basic/ieee1588.adoc @@ -0,0 +1,28 @@ +=== PTP basic (IEEE 1588) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/basic] + +==== Description + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. The test is run once per supported +profile, covering both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P). + +==== Topology + +image::topology.svg[PTP basic (IEEE 1588) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee1588, priority1=1) and time receiver (ieee1588, priority1=128, client-only) +. Wait for grandmaster and time receiver ports to reach active states +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/basic/ieee1588.py b/test/case/ptp/basic/ieee1588.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/basic/ieee1588.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/basic/ieee802dot1as.adoc b/test/case/ptp/basic/ieee802dot1as.adoc new file mode 100644 index 000000000..3fbc7a1bf --- /dev/null +++ b/test/case/ptp/basic/ieee802dot1as.adoc @@ -0,0 +1,28 @@ +=== PTP basic (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/basic] + +==== Description + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. The test is run once per supported +profile, covering both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P). + +==== Topology + +image::topology.svg[PTP basic (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee802-dot1as, priority1=1) and time receiver (ieee802-dot1as, priority1=128, client-only) +. Wait for grandmaster and time receiver ports to reach active states +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/basic/ieee802dot1as.py b/test/case/ptp/basic/ieee802dot1as.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/basic/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/basic/test.adoc b/test/case/ptp/basic/test.adoc new file mode 100644 index 000000000..3b02110b7 --- /dev/null +++ b/test/case/ptp/basic/test.adoc @@ -0,0 +1,29 @@ +=== PTP basic + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/basic] + +==== Description + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. Software timestamping is used, making +the test suitable for virtual machines as well as real hardware. + +This is the smoke test: if it passes, the full PTP stack is working. + +==== Topology + +image::topology.svg[PTP basic topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (priority1=1) and time receiver (priority1=128, client-only) +. Wait for grandmaster and time receiver ports to reach active states +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/basic/test.py b/test/case/ptp/basic/test.py new file mode 100755 index 000000000..848fcd7ab --- /dev/null +++ b/test/case/ptp/basic/test.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""PTP basic + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. The test is run once per supported +profile, covering both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P). +""" + +import infamy +import infamy.ptp as ptp +from infamy import until +from infamy.util import parallel + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, client_only, profile, ip=None): + config = { + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + }] + } + }] + } + } + } + } + + # Always enable the underlying interface — ptp4l needs it up regardless + # of profile. IEEE 1588 also needs an IPv4 address for UDP transport. + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + config["ietf-interfaces"] = { + "interfaces": {"interface": [iface_cfg]} + } + + # Fast timers: 250 ms announce/sync intervals speed up port state transitions + # and convergence compared to the 1 s defaults. + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + config["ieee1588-ptp-tt"]["ptp"]["instances"]["instance"][0] \ + ["ports"]["port"][0]["port-ds"] = port_ds + + return config + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + gm = env.attach("gm", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, receiver_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver") + + with test.step(f"Configure grandmaster (OC, {profile}, priority1=1) and time receiver ({profile}, priority1=128, client-only)"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + client_only=False, profile=profile, + ip="192.168.100.1")) + receiver.put_config_dicts(configure_oc(receiver_iface, priority1=128, + client_only=True, profile=profile, + ip="192.168.100.2")) + + with test.step("Wait for grandmaster and time receiver ports to reach active states"): + parallel(lambda: until(lambda: ptp.is_time_transmitter(gm), attempts=60), + lambda: until(lambda: ptp.is_time_receiver(receiver), attempts=60)) + + with test.step("Wait for time receiver offset to converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + test.succeed() diff --git a/test/case/ptp/basic/test.yaml b/test/case/ptp/basic/test.yaml new file mode 100644 index 000000000..4a4424f8b --- /dev/null +++ b/test/case/ptp/basic/test.yaml @@ -0,0 +1,11 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP basic (IEEE 1588) + case: ieee1588.py + opts: ["--profile", "ieee1588"] + +- name: PTP basic (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/basic/topology.dot b/test/case/ptp/basic/topology.dot new file mode 100644 index 000000000..efa656b63 --- /dev/null +++ b/test/case/ptp/basic/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-basic" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- receiver:data [label="\n\n192.168.100.0/30 ", dir="both"] +} diff --git a/test/case/ptp/basic/topology.svg b/test/case/ptp/basic/topology.svg new file mode 100644 index 000000000..92ee2de21 --- /dev/null +++ b/test/case/ptp/basic/topology.svg @@ -0,0 +1,62 @@ + + + + + + +ptp-basic + + + +host + +mgmt1 + +host + +mgmt2 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt2--receiver:mgmt + + + + +gm:data--receiver:data + + + +192.168.100.0/30   + + + diff --git a/test/case/ptp/bmca/Readme.adoc b/test/case/ptp/bmca/Readme.adoc new file mode 100644 index 000000000..cb3079ad3 --- /dev/null +++ b/test/case/ptp/bmca/Readme.adoc @@ -0,0 +1,6 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/bmca/ieee1588.adoc b/test/case/ptp/bmca/ieee1588.adoc new file mode 100644 index 000000000..8ec36caf7 --- /dev/null +++ b/test/case/ptp/bmca/ieee1588.adoc @@ -0,0 +1,39 @@ +=== PTP BTCA grandmaster election (IEEE 1588) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/bmca] + +==== Description + +Verify that the Best TimeTransmitter Clock Algorithm (BTCA) selects the clock +with the lowest `priority1` as grandmaster, and that a change of `priority1` +at runtime triggers a new election with the correct result. + +Two Ordinary Clocks are connected back-to-back. Both announce themselves as +potential grandmasters. In round one, *alpha* holds `priority1=1` and wins +the election; *beta* (`priority1=128`) becomes the time receiver. In round +two, *alpha* is reconfigured to priority1=200 without restarting; the BTCA +re-runs and beta wins, becoming the new grandmaster. The test verifies that +alpha's `parent-ds` `grandmaster-identity` changes to beta's `clock-identity`, +confirming that the re-election is reflected in the operational datastore. + +Announce intervals are reduced to 250 ms (`log-announce-interval -2`) and the +announce receipt timeout to 2 intervals (500 ms) to make re-election complete +in roughly one second rather than the default three. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP BTCA grandmaster election (IEEE 1588) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure both DUTs (ieee1588); alpha has lower priority1 +. Verify initial election: alpha is grandmaster, beta is time receiver +. Reconfigure alpha with worse priority1=200 +. Verify beta wins re-election (is own grandmaster) +. Verify alpha tracks beta as grandmaster + + diff --git a/test/case/ptp/bmca/ieee1588.py b/test/case/ptp/bmca/ieee1588.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/bmca/ieee1588.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/bmca/ieee802dot1as.adoc b/test/case/ptp/bmca/ieee802dot1as.adoc new file mode 100644 index 000000000..625efadb4 --- /dev/null +++ b/test/case/ptp/bmca/ieee802dot1as.adoc @@ -0,0 +1,39 @@ +=== PTP BTCA grandmaster election (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/bmca] + +==== Description + +Verify that the Best TimeTransmitter Clock Algorithm (BTCA) selects the clock +with the lowest `priority1` as grandmaster, and that a change of `priority1` +at runtime triggers a new election with the correct result. + +Two Ordinary Clocks are connected back-to-back. Both announce themselves as +potential grandmasters. In round one, *alpha* holds `priority1=1` and wins +the election; *beta* (`priority1=128`) becomes the time receiver. In round +two, *alpha* is reconfigured to priority1=200 without restarting; the BTCA +re-runs and beta wins, becoming the new grandmaster. The test verifies that +alpha's `parent-ds` `grandmaster-identity` changes to beta's `clock-identity`, +confirming that the re-election is reflected in the operational datastore. + +Announce intervals are reduced to 250 ms (`log-announce-interval -2`) and the +announce receipt timeout to 2 intervals (500 ms) to make re-election complete +in roughly one second rather than the default three. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP BTCA grandmaster election (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure both DUTs (ieee802-dot1as); alpha has lower priority1 +. Verify initial election: alpha is grandmaster, beta is time receiver +. Reconfigure alpha with worse priority1=200 +. Verify beta wins re-election (is own grandmaster) +. Verify alpha tracks beta as grandmaster + + diff --git a/test/case/ptp/bmca/ieee802dot1as.py b/test/case/ptp/bmca/ieee802dot1as.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/bmca/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/bmca/test.adoc b/test/case/ptp/bmca/test.adoc new file mode 100644 index 000000000..1080fefc5 --- /dev/null +++ b/test/case/ptp/bmca/test.adoc @@ -0,0 +1,5 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] diff --git a/test/case/ptp/bmca/test.py b/test/case/ptp/bmca/test.py new file mode 100755 index 000000000..fb7a07575 --- /dev/null +++ b/test/case/ptp/bmca/test.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""PTP BTCA grandmaster election + +Verify that the Best TimeTransmitter Clock Algorithm (BTCA) selects the clock +with the lowest `priority1` as grandmaster, and that a change of `priority1` +at runtime triggers a new election with the correct result. + +Two Ordinary Clocks are connected back-to-back. Both announce themselves as +potential grandmasters. In round one, *alpha* holds `priority1=1` and wins +the election; *beta* (`priority1=128`) becomes the time receiver. In round +two, *alpha* is reconfigured to priority1=200 without restarting; the BTCA +re-runs and beta wins, becoming the new grandmaster. The test verifies that +alpha's `parent-ds` `grandmaster-identity` changes to beta's `clock-identity`, +confirming that the re-election is reflected in the operational datastore. + +Announce intervals are reduced to 250 ms (`log-announce-interval -2`) and the +announce receipt timeout to 2 intervals (500 ms) to make re-election complete +in roughly one second rather than the default three. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until +from infamy.util import parallel + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + + +def configure_oc(iface, priority1, profile, ip=None): + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + + return { + "ietf-interfaces": { + "interfaces": {"interface": [iface_cfg]} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": False, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": port_ds, + }] + } + }] + } + } + } + } + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + alpha = env.attach("alpha", "mgmt") + beta = env.attach("beta", "mgmt") + + _, if_alpha = env.ltop.xlate("alpha", "data") + _, if_beta = env.ltop.xlate("beta", "data") + + with test.step(f"Configure both DUTs ({profile}); alpha has lower priority1"): + alpha.put_config_dicts(configure_oc(if_alpha, priority1=1, + profile=profile, ip="192.168.100.1")) + beta.put_config_dicts(configure_oc(if_beta, priority1=128, + profile=profile, ip="192.168.100.2")) + + with test.step("Verify initial election: alpha is grandmaster, beta is time receiver"): + parallel(lambda: until(lambda: ptp.is_own_gm(alpha), attempts=60), + lambda: until(lambda: ptp.is_time_receiver(beta), attempts=60)) + + with test.step("Reconfigure alpha with worse priority1=200"): + alpha.put_config_dicts(configure_oc(if_alpha, priority1=200, + profile=profile, ip="192.168.100.1")) + + with test.step("Verify beta wins re-election (is own grandmaster)"): + until(lambda: ptp.is_own_gm(beta), attempts=30) + + with test.step("Verify alpha tracks beta as grandmaster"): + until(lambda: ptp.grandmaster_identity(alpha) == ptp.clock_identity(beta), + attempts=30) + + test.succeed() diff --git a/test/case/ptp/bmca/test.yaml b/test/case/ptp/bmca/test.yaml new file mode 100644 index 000000000..3246043c7 --- /dev/null +++ b/test/case/ptp/bmca/test.yaml @@ -0,0 +1,11 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP BTCA grandmaster election (IEEE 1588) + case: ieee1588.py + opts: ["--profile", "ieee1588"] + +- name: PTP BTCA grandmaster election (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/bmca/topology.dot b/test/case/ptp/bmca/topology.dot new file mode 100644 index 000000000..1d9dcc79b --- /dev/null +++ b/test/case/ptp/bmca/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-bmca" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + alpha [ + label="{ mgmt | data } | { alpha }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + beta [ + label="{ data | mgmt } | { beta }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- alpha:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- beta:mgmt [requires="mgmt", color="lightgray"] + + alpha:data -- beta:data [label="192.168.101.0/30 ", dir="both"] +} diff --git a/test/case/ptp/bmca/topology.svg b/test/case/ptp/bmca/topology.svg new file mode 100644 index 000000000..04f715050 --- /dev/null +++ b/test/case/ptp/bmca/topology.svg @@ -0,0 +1,60 @@ + + + + + + +ptp-bmca + + + +host + +mgmt1 + +host + +mgmt2 + + + +alpha + +mgmt + +data + +alpha + + + +host:mgmt1--alpha:mgmt + + + + +beta + +data + +mgmt + +beta + + + +host:mgmt2--beta:mgmt + + + + +alpha:data--beta:data + + + +192.168.101.0/30 + + + diff --git a/test/case/ptp/boundary_clock/Readme.adoc b/test/case/ptp/boundary_clock/Readme.adoc new file mode 100644 index 000000000..cb3079ad3 --- /dev/null +++ b/test/case/ptp/boundary_clock/Readme.adoc @@ -0,0 +1,6 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/boundary_clock/ieee1588.adoc b/test/case/ptp/boundary_clock/ieee1588.adoc new file mode 100644 index 000000000..656af5dd7 --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee1588.adoc @@ -0,0 +1,43 @@ +=== PTP boundary clock (IEEE 1588) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/boundary_clock] + +==== Description + +Verify that a Boundary Clock (BC) correctly receives time on one port and +distributes it on another, and that the downstream time receiver sees exactly +one additional hop (`steps-removed=2`). + +Three nodes are connected in a chain: a grandmaster Ordinary Clock (OC, +`priority1=1`), a Boundary Clock (BC, `priority1=64`) with two ports, and a +time-receiver Ordinary Clock (OC, `priority1=128`). + +The BC's upstream port (toward the GM) must reach time-receiver state; the +downstream port (toward the time receiver) must reach time-transmitter state. +The time receiver's `steps-removed` counter must equal 2: the BC increments +`steps-removed` to 1 in the ANNOUNCE messages it forwards, and the time +receiver adds 1 more when it stores the value in its `currentDS`. An OC +directly connected to the GM shows 1, so the BC adds exactly one extra hop. +This distinguishes BC from TC behavior, where `steps-removed` remains 1 (the +TC passes ANNOUNCE messages through without incrementing `steps-removed`). + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP boundary clock (IEEE 1588) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee1588, priority1=1) +. Configure boundary clock (BC, ieee1588, priority1=64, two ports) +. Configure time receiver (OC, ieee1588, priority1=128, client-only) +. Wait for BC uplink port to become time-receiver +. Wait for BC dnlink port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 2 (one BC hop) +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/boundary_clock/ieee1588.py b/test/case/ptp/boundary_clock/ieee1588.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee1588.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/boundary_clock/ieee802dot1as.adoc b/test/case/ptp/boundary_clock/ieee802dot1as.adoc new file mode 100644 index 000000000..739beafca --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee802dot1as.adoc @@ -0,0 +1,43 @@ +=== PTP boundary clock (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/boundary_clock] + +==== Description + +Verify that a Boundary Clock (BC) correctly receives time on one port and +distributes it on another, and that the downstream time receiver sees exactly +one additional hop (`steps-removed=2`). + +Three nodes are connected in a chain: a grandmaster Ordinary Clock (OC, +`priority1=1`), a Boundary Clock (BC, `priority1=64`) with two ports, and a +time-receiver Ordinary Clock (OC, `priority1=128`). + +The BC's upstream port (toward the GM) must reach time-receiver state; the +downstream port (toward the time receiver) must reach time-transmitter state. +The time receiver's `steps-removed` counter must equal 2: the BC increments +`steps-removed` to 1 in the ANNOUNCE messages it forwards, and the time +receiver adds 1 more when it stores the value in its `currentDS`. An OC +directly connected to the GM shows 1, so the BC adds exactly one extra hop. +This distinguishes BC from TC behavior, where `steps-removed` remains 1 (the +TC passes ANNOUNCE messages through without incrementing `steps-removed`). + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP boundary clock (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee802-dot1as, priority1=1) +. Configure boundary clock (BC, ieee802-dot1as, priority1=64, two ports) +. Configure time receiver (OC, ieee802-dot1as, priority1=128, client-only) +. Wait for BC uplink port to become time-receiver +. Wait for BC dnlink port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 2 (one BC hop) +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/boundary_clock/ieee802dot1as.py b/test/case/ptp/boundary_clock/ieee802dot1as.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/boundary_clock/test.adoc b/test/case/ptp/boundary_clock/test.adoc new file mode 100644 index 000000000..1080fefc5 --- /dev/null +++ b/test/case/ptp/boundary_clock/test.adoc @@ -0,0 +1,5 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] diff --git a/test/case/ptp/boundary_clock/test.py b/test/case/ptp/boundary_clock/test.py new file mode 100755 index 000000000..152331ff4 --- /dev/null +++ b/test/case/ptp/boundary_clock/test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""PTP boundary clock + +Verify that a Boundary Clock (BC) correctly receives time on one port and +distributes it on another, and that the downstream time receiver sees exactly +one additional hop (`steps-removed=2`). + +Three nodes are connected in a chain: a grandmaster Ordinary Clock (OC, +`priority1=1`), a Boundary Clock (BC, `priority1=64`) with two ports, and a +time-receiver Ordinary Clock (OC, `priority1=128`). + +The BC's upstream port (toward the GM) must reach time-receiver state; the +downstream port (toward the time receiver) must reach time-transmitter state. +The time receiver's `steps-removed` counter must equal 2: the BC increments +`steps-removed` to 1 in the ANNOUNCE messages it forwards, and the time +receiver adds 1 more when it stores the value in its `currentDS`. An OC +directly connected to the GM shows 1, so the BC adds exactly one extra hop. +This distinguishes BC from TC behavior, where `steps-removed` remains 1 (the +TC passes ANNOUNCE messages through without incrementing `steps-removed`). + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, profile, client_only=False, ip=None): + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + + return { + "ietf-interfaces": { + "interfaces": {"interface": [iface_cfg]} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": port_ds, + }] + } + }] + } + } + } + } + + +def configure_bc(uplink_iface, dnlink_iface, profile, + uplink_ip=None, dnlink_ip=None, priority1=64): + ifaces = [{"name": uplink_iface, "enabled": True}, + {"name": dnlink_iface, "enabled": True}] + if profile == "ieee1588": + ifaces[0]["ipv4"] = {"address": [{"ip": uplink_ip, "prefix-length": 30}]} + ifaces[1]["ipv4"] = {"address": [{"ip": dnlink_ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + + return { + "ietf-interfaces": { + "interfaces": {"interface": ifaces} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "bc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + }, + "ports": { + "port": [ + { + "port-index": 1, + "underlying-interface": uplink_iface, + "port-ds": port_ds, + }, + { + "port-index": 2, + "underlying-interface": dnlink_iface, + "port-ds": port_ds, + } + ] + } + }] + } + } + } + } + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + gm = env.attach("gm", "mgmt") + bc = env.attach("bc", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, bc_uplink = env.ltop.xlate("bc", "uplink") + _, bc_dnlink = env.ltop.xlate("bc", "dnlink") + _, recv_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver") + + with test.step(f"Configure grandmaster (OC, {profile}, priority1=1)"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + profile=profile, ip="192.168.100.1")) + + with test.step(f"Configure boundary clock (BC, {profile}, priority1=64, two ports)"): + bc.put_config_dicts(configure_bc(bc_uplink, bc_dnlink, profile=profile, + uplink_ip="192.168.100.2", + dnlink_ip="192.168.101.1")) + + with test.step(f"Configure time receiver (OC, {profile}, priority1=128, client-only)"): + receiver.put_config_dicts(configure_oc(recv_iface, priority1=128, + profile=profile, client_only=True, + ip="192.168.101.2")) + + with test.step("Wait for BC uplink port to become time-receiver"): + until(lambda: ptp.is_time_receiver(bc, port_idx=1), attempts=60) + + with test.step("Wait for BC dnlink port to become time-transmitter"): + until(lambda: ptp.is_time_transmitter(bc, port_idx=2), attempts=60) + + with test.step("Wait for time receiver to reach time-receiver state"): + until(lambda: ptp.is_time_receiver(receiver), attempts=60) + + with test.step("Verify time receiver steps-removed equals 2 (one BC hop)"): + until(lambda: ptp.steps_removed(receiver) == 2, attempts=30) + + with test.step("Wait for time receiver offset to converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + test.succeed() diff --git a/test/case/ptp/boundary_clock/test.yaml b/test/case/ptp/boundary_clock/test.yaml new file mode 100644 index 000000000..5277fe19d --- /dev/null +++ b/test/case/ptp/boundary_clock/test.yaml @@ -0,0 +1,11 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP boundary clock (IEEE 1588) + case: ieee1588.py + opts: ["--profile", "ieee1588"] + +- name: PTP boundary clock (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/boundary_clock/topology.dot b/test/case/ptp/boundary_clock/topology.dot new file mode 100644 index 000000000..66a895d14 --- /dev/null +++ b/test/case/ptp/boundary_clock/topology.dot @@ -0,0 +1,42 @@ +graph "ptp-boundary-clock" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n | mgmt2 | mgmt3 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.5!", + fontsize=12, + requires="infix", + ]; + + bc [ + label="{ uplink | mgmt | dnlink } | { bc\npriority1=64 }", + pos="2,15!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.5!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- bc:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- bc:uplink [label="192.168.102.0/30 ", dir="both"] + bc:dnlink -- receiver:data [label="192.168.103.0/30 ", dir="both"] +} diff --git a/test/case/ptp/boundary_clock/topology.svg b/test/case/ptp/boundary_clock/topology.svg new file mode 100644 index 000000000..411c29ae0 --- /dev/null +++ b/test/case/ptp/boundary_clock/topology.svg @@ -0,0 +1,90 @@ + + + + + + +ptp-boundary-clock + + + +host + +mgmt1 + +host + +mgmt2 + +mgmt3 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +bc + +uplink + +mgmt + +dnlink + +bc +priority1=64 + + + +host:mgmt2--bc:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt3--receiver:mgmt + + + + +gm:data--bc:uplink + + + +192.168.102.0/30 + + + +bc:dnlink--receiver:data + + + +192.168.103.0/30 + + + diff --git a/test/case/ptp/port_recovery/Readme.adoc b/test/case/ptp/port_recovery/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/ptp/port_recovery/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ptp/port_recovery/test.adoc b/test/case/ptp/port_recovery/test.adoc new file mode 100644 index 000000000..e58d2f0c2 --- /dev/null +++ b/test/case/ptp/port_recovery/test.adoc @@ -0,0 +1,32 @@ +=== PTP port fault recovery + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/port_recovery] + +==== Description + +Verify that the PTP port state machine correctly detects a link fault +and recovers to time-receiver state once the link is restored. + +Two Ordinary Clocks are connected back-to-back. Once the time receiver +has converged, the grandmaster's data interface is disabled. The time +receiver must leave time-receiver state within a short timeout. When +the interface is re-enabled, the time receiver must return to +time-receiver state and its offset must converge again to within the +configured threshold. + +==== Topology + +image::topology.svg[PTP port fault recovery topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (priority1=1) and time receiver (client-only) +. Wait for initial convergence +. Disable grandmaster data interface to trigger fault +. Verify time receiver leaves time-receiver state +. Re-enable grandmaster data interface +. Wait for time receiver to return to time-receiver state after recovery +. Wait for offset to re-converge + + diff --git a/test/case/ptp/port_recovery/test.py b/test/case/ptp/port_recovery/test.py new file mode 100755 index 000000000..740d907ff --- /dev/null +++ b/test/case/ptp/port_recovery/test.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""PTP port fault recovery + +Verify that the PTP port state machine correctly detects a link fault +and recovers to time-receiver state once the link is restored. + +Two Ordinary Clocks are connected back-to-back. Once the time receiver +has converged, the grandmaster's data interface is disabled. The time +receiver must leave time-receiver state within a short timeout. When +the interface is re-enabled, the time receiver must return to +time-receiver state and its offset must converge again to within the +configured threshold. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until + + +def configure_oc(iface, ip, priority1, client_only, dm="e2e"): + return { + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": iface, + "enabled": True, + "ipv4": { + "address": [{"ip": ip, "prefix-length": 30}] + } + }] + } + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": "ieee1588", + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": { + "delay-mechanism": dm, + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + }] + } + }] + } + } + } + } + + +def set_iface_enabled(target, iface, enabled): + target.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [{ + "name": iface, + "enabled": enabled, + }] + } + }) + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--threshold-ns", type=int, default=None) + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + gm = env.attach("gm", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, receiver_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver") + + with test.step("Configure grandmaster (priority1=1) and time receiver (client-only)"): + gm.put_config_dicts(configure_oc(gm_iface, "192.168.100.1", + priority1=1, client_only=False)) + receiver.put_config_dicts(configure_oc(receiver_iface, "192.168.100.2", + priority1=128, client_only=True)) + + with test.step("Wait for initial convergence"): + until(lambda: ptp.is_time_receiver(receiver) and ptp.has_converged(receiver, threshold_ns), + attempts=120) + + with test.step("Disable grandmaster data interface to trigger fault"): + set_iface_enabled(gm, gm_iface, False) + + with test.step("Verify time receiver leaves time-receiver state"): + until(lambda: not ptp.is_time_receiver(receiver), attempts=30) + + with test.step("Re-enable grandmaster data interface"): + set_iface_enabled(gm, gm_iface, True) + + with test.step("Wait for time receiver to return to time-receiver state after recovery"): + until(lambda: ptp.is_time_receiver(receiver), attempts=120) + + with test.step("Wait for offset to re-converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + test.succeed() diff --git a/test/case/ptp/port_recovery/topology.dot b/test/case/ptp/port_recovery/topology.dot new file mode 100644 index 000000000..4544ca5ad --- /dev/null +++ b/test/case/ptp/port_recovery/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-port-recovery" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- receiver:data [label="\n\n192.168.106.0/30 ", dir="both"] +} diff --git a/test/case/ptp/port_recovery/topology.svg b/test/case/ptp/port_recovery/topology.svg new file mode 100644 index 000000000..8c5b1311d --- /dev/null +++ b/test/case/ptp/port_recovery/topology.svg @@ -0,0 +1,62 @@ + + + + + + +ptp-port-recovery + + + +host + +mgmt1 + +host + +mgmt2 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt2--receiver:mgmt + + + + +gm:data--receiver:data + + + +192.168.106.0/30   + + + diff --git a/test/case/ptp/transparent_clock/Readme.adoc b/test/case/ptp/transparent_clock/Readme.adoc new file mode 100644 index 000000000..164bfb7f6 --- /dev/null +++ b/test/case/ptp/transparent_clock/Readme.adoc @@ -0,0 +1,10 @@ +include::e2e.adoc[] + +<<< + +include::p2p.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/transparent_clock/e2e.adoc b/test/case/ptp/transparent_clock/e2e.adoc new file mode 100644 index 000000000..58691a804 --- /dev/null +++ b/test/case/ptp/transparent_clock/e2e.adoc @@ -0,0 +1,44 @@ +=== PTP transparent clock (E2E) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock with hardware timestamping, and a +time-receiver Ordinary Clock (`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset +must converge within the tight threshold appropriate for hardware timestamping. + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. +The TC node requires hardware PTP timestamping support. + +==== Topology + +image::topology.svg[PTP transparent clock (E2E) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm}-tc, {profile}) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/e2e.py b/test/case/ptp/transparent_clock/e2e.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/transparent_clock/e2e.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/transparent_clock/ieee802dot1as.adoc b/test/case/ptp/transparent_clock/ieee802dot1as.adoc new file mode 100644 index 000000000..f5fda6ea6 --- /dev/null +++ b/test/case/ptp/transparent_clock/ieee802dot1as.adoc @@ -0,0 +1,44 @@ +=== PTP transparent clock (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock with hardware timestamping, and a +time-receiver Ordinary Clock (`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset +must converge within the tight threshold appropriate for hardware timestamping. + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. +The TC node requires hardware PTP timestamping support. + +==== Topology + +image::topology.svg[PTP transparent clock (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm}-tc, ieee802-dot1as) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/ieee802dot1as.py b/test/case/ptp/transparent_clock/ieee802dot1as.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/transparent_clock/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/transparent_clock/p2p.adoc b/test/case/ptp/transparent_clock/p2p.adoc new file mode 100644 index 000000000..aa7fb54e1 --- /dev/null +++ b/test/case/ptp/transparent_clock/p2p.adoc @@ -0,0 +1,44 @@ +=== PTP transparent clock (P2P) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock with hardware timestamping, and a +time-receiver Ordinary Clock (`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset +must converge within the tight threshold appropriate for hardware timestamping. + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. +The TC node requires hardware PTP timestamping support. + +==== Topology + +image::topology.svg[PTP transparent clock (P2P) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm}-tc, {profile}) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/p2p.py b/test/case/ptp/transparent_clock/p2p.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/test/case/ptp/transparent_clock/p2p.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/transparent_clock/test.adoc b/test/case/ptp/transparent_clock/test.adoc new file mode 100644 index 000000000..a07dcf860 --- /dev/null +++ b/test/case/ptp/transparent_clock/test.adoc @@ -0,0 +1,46 @@ +=== PTP transparent clock (P2P) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing +transparently through a hardware switch without adding a boundary-clock +hop, and that the downstream time receiver converges to the +grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(priority1=1), a Transparent Clock with hardware timestamping, and a +time-receiver Ordinary Clock (priority1=128). + +The TC updates the correction field in each Sync and Delay_Req message +to account for its own residence time. Because a TC is transparent, +the time receiver's steps-removed counter must equal 1 — unlike a +Boundary Clock, which would give 2. A TC passes ANNOUNCE messages +unchanged (stepsRemoved=0 from the GM), and the time receiver adds 1 +when it stores the value in currentDS, giving a total of 1. A BC +increments stepsRemoved to 1 before forwarding, and the receiver adds +1 more, giving 2. The time receiver's offset must converge within the +tight threshold appropriate for hardware timestamping. + +The delay mechanism (E2E or P2P) is injected via the --delay-mechanism +argument from the test suite YAML, allowing both variants to run from +the same test script. The TC node requires hardware PTP timestamping +support (capability: ptp-hwts). + +==== Topology + +image::topology.svg[PTP transparent clock (P2P) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm.upper()}-TC) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 (TC adds no boundary-clock hop) +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/test.py b/test/case/ptp/transparent_clock/test.py new file mode 100755 index 000000000..03c73d8e7 --- /dev/null +++ b/test/case/ptp/transparent_clock/test.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""PTP transparent clock + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock with hardware timestamping, and a +time-receiver Ordinary Clock (`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset +must converge within the tight threshold appropriate for hardware timestamping. + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. +The TC node requires hardware PTP timestamping support. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + self.args.add_argument("--delay-mechanism", default="e2e", + choices=["e2e", "p2p"]) + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, profile, client_only=False, ip=None, dm="e2e"): + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = dm + + return { + "ietf-interfaces": { + "interfaces": {"interface": [iface_cfg]} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": port_ds, + }] + } + }] + } + } + } + } + + +def configure_tc(uplink_iface, dnlink_iface, profile, dm="e2e", + uplink_ip=None, dnlink_ip=None): + if profile == "ieee802-dot1as": + instance_type = "p2p-tc" + else: + instance_type = "p2p-tc" if dm == "p2p" else "e2e-tc" + + ifaces = [{"name": uplink_iface, "enabled": True}, + {"name": dnlink_iface, "enabled": True}] + if profile == "ieee1588": + ifaces[0]["ipv4"] = {"address": [{"ip": uplink_ip, "prefix-length": 30}]} + ifaces[1]["ipv4"] = {"address": [{"ip": dnlink_ip, "prefix-length": 30}]} + + return { + "ietf-interfaces": { + "interfaces": {"interface": ifaces} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": instance_type, + "domain-number": 0, + "infix-ptp:profile": profile, + }, + "ports": { + "port": [ + { + "port-index": 1, + "underlying-interface": uplink_iface, + "port-ds": {"log-sync-interval": -2}, + }, + { + "port-index": 2, + "underlying-interface": dnlink_iface, + "port-ds": {"log-sync-interval": -2}, + } + ] + } + }] + } + } + } + } + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + dm = "p2p" if profile == "ieee802-dot1as" else env.args.delay_mechanism + gm = env.attach("gm", "mgmt") + tc = env.attach("tc", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, tc_uplink = env.ltop.xlate("tc", "uplink") + _, tc_dnlink = env.ltop.xlate("tc", "dnlink") + _, receiver_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "tc") + + with test.step(f"Configure grandmaster (OC, priority1=1, {dm})"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + profile=profile, ip="192.168.100.1", dm=dm)) + + with test.step(f"Configure transparent clock ({dm}-tc, {profile})"): + tc.put_config_dicts(configure_tc(tc_uplink, tc_dnlink, profile=profile, dm=dm, + uplink_ip="192.168.100.2", + dnlink_ip="192.168.101.1")) + + with test.step("Configure time receiver (OC, priority1=128, client-only)"): + receiver.put_config_dicts(configure_oc(receiver_iface, priority1=128, + profile=profile, client_only=True, + ip="192.168.101.2", dm=dm)) + + with test.step("Wait for grandmaster port to become time-transmitter"): + until(lambda: ptp.is_time_transmitter(gm), attempts=60) + + with test.step("Wait for time receiver to reach time-receiver state"): + until(lambda: ptp.is_time_receiver(receiver), attempts=60) + + with test.step("Verify time receiver steps-removed equals 1"): + until(lambda: ptp.steps_removed(receiver) == 1, attempts=60) + + with test.step("Wait for time receiver offset to converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=180) + + test.succeed() diff --git a/test/case/ptp/transparent_clock/test.yaml b/test/case/ptp/transparent_clock/test.yaml new file mode 100644 index 000000000..4bbc87daf --- /dev/null +++ b/test/case/ptp/transparent_clock/test.yaml @@ -0,0 +1,15 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP transparent clock (E2E) + case: e2e.py + opts: ["--delay-mechanism", "e2e"] + +- name: PTP transparent clock (P2P) + case: p2p.py + opts: ["--delay-mechanism", "p2p"] + +- name: PTP transparent clock (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/transparent_clock/topology.dot b/test/case/ptp/transparent_clock/topology.dot new file mode 100644 index 000000000..87c8f5eda --- /dev/null +++ b/test/case/ptp/transparent_clock/topology.dot @@ -0,0 +1,42 @@ +graph "ptp-transparent-clock" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n | mgmt2 | mgmt3 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.5!", + fontsize=12, + requires="infix", + ]; + + tc [ + label="{ uplink | mgmt | dnlink } | { tc\nE2E-TC or P2P-TC }", + pos="2,15!", + fontsize=12, + requires="infix ptp-hwts", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.5!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- tc:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- tc:uplink [label="192.168.104.0/30 ", dir="both"] + tc:dnlink -- receiver:data [label="192.168.105.0/30 ", dir="both"] +} diff --git a/test/case/ptp/transparent_clock/topology.svg b/test/case/ptp/transparent_clock/topology.svg new file mode 100644 index 000000000..cfa0ceb65 --- /dev/null +++ b/test/case/ptp/transparent_clock/topology.svg @@ -0,0 +1,90 @@ + + + + + + +ptp-transparent-clock + + + +host + +mgmt1 + +host + +mgmt2 + +mgmt3 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +tc + +uplink + +mgmt + +dnlink + +tc +E2E-TC or P2P-TC + + + +host:mgmt2--tc:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt3--receiver:mgmt + + + + +gm:data--tc:uplink + + + +192.168.104.0/30 + + + +tc:dnlink--receiver:data + + + +192.168.105.0/30 + + + diff --git a/test/infamy/ptp.py b/test/infamy/ptp.py new file mode 100644 index 000000000..2e03877ae --- /dev/null +++ b/test/infamy/ptp.py @@ -0,0 +1,105 @@ +"""PTP (IEEE 1588) test helpers + +Query PTP operational data from the ieee1588-ptp-tt YANG model. +All functions are None-safe and intended for use with until(): + + until(lambda: ptp.is_time_receiver(target), attempts=60) +""" + + +def _get_instance(target, idx=0): + data = target.get_data("/ieee1588-ptp-tt:ptp") or {} + instances = (data.get("ptp", {}) + .get("instances", {}) + .get("instance", [])) + for inst in instances: + if inst.get("instance-index") == idx: + return inst + return None + + +def port_state(target, port_idx=1, inst_idx=0): + """Return port-state string for given port, or None.""" + inst = _get_instance(target, inst_idx) + if not inst: + return None + for port in inst.get("ports", {}).get("port", []): + if port.get("port-index") == port_idx: + return port.get("port-ds", {}).get("port-state") + return None + + +def is_time_receiver(target, port_idx=1, inst_idx=0): + """True when port is in time-receiver state.""" + return port_state(target, port_idx, inst_idx) == "time-receiver" + + +def is_time_transmitter(target, port_idx=1, inst_idx=0): + """True when port is in time-transmitter state.""" + return port_state(target, port_idx, inst_idx) == "time-transmitter" + + +def offset_ns(target, inst_idx=0): + """Return offset-from-time-transmitter in nanoseconds, or None. + + The YANG value is scaled nanoseconds (int64 × 2^16 stored as string). + """ + inst = _get_instance(target, inst_idx) + if not inst: + return None + raw = inst.get("current-ds", {}).get("offset-from-time-transmitter") + try: + return int(raw) // 65536 + except (TypeError, ValueError): + return None + + +def steps_removed(target, inst_idx=0): + """Return steps-removed count, or None.""" + inst = _get_instance(target, inst_idx) + return inst.get("current-ds", {}).get("steps-removed") if inst else None + + +def grandmaster_identity(target, inst_idx=0): + """Return grandmaster-identity string from parent-ds, or None.""" + inst = _get_instance(target, inst_idx) + return inst.get("parent-ds", {}).get("grandmaster-identity") if inst else None + + +def clock_identity(target, inst_idx=0): + """Return this device's clock-identity string from default-ds, or None.""" + inst = _get_instance(target, inst_idx) + return inst.get("default-ds", {}).get("clock-identity") if inst else None + + +def is_own_gm(target, inst_idx=0): + """True when device is its own grandmaster (acting as GM). + + Compares clock-identity to grandmaster-identity; equal means the + device won the BTCA election and is distributing its own time. + """ + cid = clock_identity(target, inst_idx) + gm = grandmaster_identity(target, inst_idx) + return cid is not None and cid == gm + + +def has_converged(target, threshold_ns=100_000, inst_idx=0): + """True when |offset-from-time-transmitter| < threshold_ns.""" + off = offset_ns(target, inst_idx) + if off is None: + return False + return abs(off) < threshold_ns + + +def default_threshold(env, logical_node): + """Return a convergence threshold suited to the node's timestamping capability. + + Queries the physical topology for the ptp-hwts capability on the node + matched to logical_node. Returns 1000 ns (1 µs) for hardware-timestamping + nodes or 100000 ns (100 µs) for software timestamping. + + Pass --threshold-ns on the command line to override. + """ + phys = env.ltop.xlate(logical_node) + provides = env.ptop.g.nodes.get(phys, {}).get("provides", set()) + return 1_000 if "ptp-hwts" in provides else 100_000 diff --git a/test/spec/Readme.adoc.in b/test/spec/Readme.adoc.in index 0e6a1dbe4..524941533 100644 --- a/test/spec/Readme.adoc.in +++ b/test/spec/Readme.adoc.in @@ -56,6 +56,10 @@ include::../case/ntp/Readme.adoc[] <<< +include::../case/ptp/Readme.adoc[] + +<<< + include::../case/hardware/Readme.adoc[] <<< From b98743b328f426c08a368f482fad56f65e38ba6f Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 18 Apr 2026 08:38:47 +0200 Subject: [PATCH 11/11] doc: update ChangeLog Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index cfc754c31..8df2caae1 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -9,6 +9,9 @@ All notable changes to the project are documented in this file. ### Changes - Upgrade Linux kernel to 6.18.23 (LTS) +- Add support for PTP/gPTP (IEEE 1588-2019 / 802.1AS) clock synchronization. + Supported clock types: Ordinary Clock, Boundary Clock, and Transparent Clock. + See the User Guide for configuration details - Add support for [Banana Pi BPI-R4][BPI-R4], quad-core Cortex-A73 router with 4x 2.5 GbE switching, dual 10 GbE SFP+. Variants BPI-R4-2g5 and BPI-R4P have one SFP+ replaced by a 2.5 GbE RJ45, with optional PoE on the R4P