From aa4d4c7d7c9d33e693319bf7b1a3adacb6dba7b3 Mon Sep 17 00:00:00 2001 From: YiLin <482244139@qq.com> Date: Thu, 12 Mar 2026 17:03:56 +0800 Subject: [PATCH] first commit --- ...-德达隧道出口-DK637+524-DK637+403-山区.xlsx | Bin 0 -> 10153 bytes __pycache__/check_station.cpython-312.pyc | Bin 0 -> 8849 bytes __pycache__/driver_utils.cpython-312.pyc | Bin 0 -> 35568 bytes __pycache__/permissions.cpython-312.pyc | Bin 0 -> 10776 bytes actions.py | 302 +++ check_station.png | Bin 0 -> 159914 bytes check_station.py | 211 ++ ck/.gitignore | 25 + ck/README.md | 292 +++ ck/data_models.py | 82 + ck/device_a.py | 103 + ck/device_b.py | 117 + ck/requirements.txt | 1 + ck/serial_protocol.py | 298 +++ globals/__pycache__/apis.cpython-312.pyc | Bin 0 -> 20668 bytes .../__pycache__/create_link.cpython-312.pyc | Bin 0 -> 8654 bytes .../__pycache__/driver_utils.cpython-312.pyc | Bin 0 -> 37967 bytes globals/__pycache__/ex_apis.cpython-312.pyc | Bin 0 -> 12070 bytes .../global_variable.cpython-312.pyc | Bin 0 -> 609 bytes globals/__pycache__/ids.cpython-312.pyc | Bin 0 -> 2614 bytes globals/apis.py | 520 ++++ globals/create_link.py | 273 ++ globals/driver_utils.py | 1195 +++++++++ globals/ex_apis.py | 298 +++ globals/global_variable.py | 17 + globals/ids.py | 59 + .../download_tabbar_page.cpython-312.pyc | Bin 0 -> 20511 bytes .../__pycache__/login_page.cpython-312.pyc | Bin 0 -> 7244 bytes .../measure_tabbar_page.cpython-312.pyc | Bin 0 -> 18263 bytes .../more_download_page.cpython-312.pyc | Bin 0 -> 15172 bytes .../screenshot_page.cpython-312.pyc | Bin 0 -> 77843 bytes ...ection_mileage_config_page.cpython-312.pyc | Bin 0 -> 46087 bytes .../upload_config_page.cpython-312.pyc | Bin 0 -> 76996 bytes page_objects/call_xie.py | 46 + page_objects/download_tabbar_page.py | 405 +++ page_objects/login_page.py | 179 ++ page_objects/measure_tabbar_page.py | 403 +++ page_objects/more_download_page.py | 343 +++ page_objects/screenshot_page.py | 1907 ++++++++++++++ page_objects/section_mileage_config_page.py | 1112 ++++++++ page_objects/upload_config_page.py | 2227 +++++++++++++++++ permissions.py | 279 +++ test/__pycache__/protocol.cpython-312.pyc | Bin 0 -> 1285 bytes test/control.py | 23 + test/device.py | 29 + test/protocol.py | 22 + test/server.py | 41 + test/test_play_data.py | 149 ++ 48 files changed, 10958 insertions(+) create mode 100644 CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx create mode 100644 __pycache__/check_station.cpython-312.pyc create mode 100644 __pycache__/driver_utils.cpython-312.pyc create mode 100644 __pycache__/permissions.cpython-312.pyc create mode 100644 actions.py create mode 100644 check_station.png create mode 100644 check_station.py create mode 100644 ck/.gitignore create mode 100644 ck/README.md create mode 100644 ck/data_models.py create mode 100644 ck/device_a.py create mode 100644 ck/device_b.py create mode 100644 ck/requirements.txt create mode 100644 ck/serial_protocol.py create mode 100644 globals/__pycache__/apis.cpython-312.pyc create mode 100644 globals/__pycache__/create_link.cpython-312.pyc create mode 100644 globals/__pycache__/driver_utils.cpython-312.pyc create mode 100644 globals/__pycache__/ex_apis.cpython-312.pyc create mode 100644 globals/__pycache__/global_variable.cpython-312.pyc create mode 100644 globals/__pycache__/ids.cpython-312.pyc create mode 100644 globals/apis.py create mode 100644 globals/create_link.py create mode 100644 globals/driver_utils.py create mode 100644 globals/ex_apis.py create mode 100644 globals/global_variable.py create mode 100644 globals/ids.py create mode 100644 page_objects/__pycache__/download_tabbar_page.cpython-312.pyc create mode 100644 page_objects/__pycache__/login_page.cpython-312.pyc create mode 100644 page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc create mode 100644 page_objects/__pycache__/more_download_page.cpython-312.pyc create mode 100644 page_objects/__pycache__/screenshot_page.cpython-312.pyc create mode 100644 page_objects/__pycache__/section_mileage_config_page.cpython-312.pyc create mode 100644 page_objects/__pycache__/upload_config_page.cpython-312.pyc create mode 100644 page_objects/call_xie.py create mode 100644 page_objects/download_tabbar_page.py create mode 100644 page_objects/login_page.py create mode 100644 page_objects/measure_tabbar_page.py create mode 100644 page_objects/more_download_page.py create mode 100644 page_objects/screenshot_page.py create mode 100644 page_objects/section_mileage_config_page.py create mode 100644 page_objects/upload_config_page.py create mode 100644 permissions.py create mode 100644 test/__pycache__/protocol.cpython-312.pyc create mode 100644 test/control.py create mode 100644 test/device.py create mode 100644 test/protocol.py create mode 100644 test/server.py create mode 100644 test/test_play_data.py diff --git a/CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx b/CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..dc6a8c8d3ec3cdad95bffa66acde0fc41f79f596 GIT binary patch literal 10153 zcmeHtRa9NevMBEE7TjHf1b4R}!JXjl5ZocS220T365QS0gIkaw3nxh6F334MJDmN_ zxbO3g@%m%VwPu%eS5;ScSCyg+I0PKXV+)br5PW?9yTJl~7~2{tI@sDdGARPVFn}Lm zp1{iGj)av#K|mtFKtNFb4rX9y$LMBll@T{0-OY>9uBdo}3(GNmw%j zxI%T@79DMbD4LUg=Z?gQmqTWE z>a^%hpl=ANH4(QwBA>(2tgCg~wJ_mpyxY9E{&{$Ewnt$i6z!_>weHf=Jw|~?gxcl3 zwcr*^C*!(sHkgmjXhWBtFV(xXD3T5F^fV5Ts2eY>NqM{n2C>O|mE}yZRbdAC5WAv;-wuuaQrsX={!&-;%K>4G0gl}j` z9@QGg2a_)xE~Yu_WKi=5J01ZtJ>wKOi^BLj8XIRIvtfl$%_asl%i+$oM>b_JxW{U? z(>=?P^gs@(%s);AOMyF{7_+^{W&E?&w4Z*kaCsagGC zH-&PQ!qB5ju#B%B-CMRF$GC44c!$x(c$9VXdQ%h7h~_(}BY^1YjasBVOKe;F?ZV3b zDrR$4m{E-AJuLiEKx8LtZM2J`Zq=d&hlGjrBt2z(NU)lb+@@PVHdy;I-XuoR`w=_vp}+D|hW8uIqsaf+1AzlY?q>C5`~M7p^b5e%*1^)z%*4d$xt~IF?I<+_>MjTh1O(?l zV4qM}9+mh$aWQIv86_A%ckzOB)fTCpP%sb^taKqlx_GjyMvAhVG)%{nxar+ZR0bkN zMC>Kl!1W>EDw5A=JDSNElRyZcBrSo$U>MAdH8sLCxhhK_(}wVkehepfxiL76CuA2P zc9y`1R^e4XM7u8mUVIgr(nxl{F~#&!pt6t%ThKIz#LaY5KQ$t+tD=rr4p(c01tZ=p z_wcy{udz}n!Yu2n>2dwH*`GEfPtugBr*c(62?~0DWxn{*DiQX}HbeLq zaOoqm_L+{4C36JaTEokTy!&P>V~`oyEjNN9u~jClT9jns=r#jPprzdS(EWAzpNU2K z3)SA}Tk6w&TVcGDR<;^h2D2Ps!;p9*q?%`yIPP%PcWBsGE#|ba>9qv7+WNk&0nLqZ zr?@Vy)+QEZGMHa7*E)0wAF30EG0fG>*JC}UJzQf2P7B&pzELX}uOWP5qNc4mGlFU< z$;Y_i?vyLYdV}iTDWSVmB*J^LX~bkRL&AT$iT?Dy+^l>2?zPbeXs%Zc`B6&LcD14Q zV#C#YiJvza`|S*gH=wUjmiMtQfJkANDF0S za5qK24Y+ghyF4Fg9^FDnzTqQ2`P4W(4s0{6FKnI;jI}=%e261_{`4zbUYEIzK)+%R zwDA9fU9vveW&DuMA~TBE732*v#1-ndeDm*u%8K*M3u1mB_l)FZ$ikQh3p|6ladJc0Y4nVyW&16~!9Y)({=cyMH@A|Hmdn87|q!uOLV@cmb_^qnh zu40hgi0p!fiuof_0K%5uqkKB*=#TjXj{l0mgN!D>6}`hfMvlk0+AVp9C@^;LG}UfX zudBUQmBXr%5aG=S62j`-&jROzK|z=E&Ea!y+~VaCndo7Q4USu-D_MJNMVB}gc_rdx zrsg}0bA~^o6<%lg-Luh?{WP${Mm@-GXqgPpj9V5%8HayM&}ZdDQhfL&JRY>P407BQ zCeV~l>a<9b({4vhm7P}JzR_d*6T@`@W^~W=eW5-A6-(NYXS7=ip5BMJ9dsD>>8Y*M z$Sml2-`TLvP5}BxF0|qe3fj`2aEDX0sk8AZKQ4n2!iyE}xUeVVde6BlO{ zZrt$jjDxk}^NcD#bF)o7cbAdWEQQ>Z@x1StZ#{FlA}IDZrG>Q;A{a@>w?f>ih|8;%;^B(nh zj0SFEPG%<7CeMy>fj74ZKOjLsT7f>`@p$nU*c0&6&UC0T5{=J+(MEP65V^f_L_X5z zGGkS9{ax5~Q8m8y`-D__B>tG2@vEj0#2N{5a#11en0k9b!92AM<@*moq}0j}keP=A z-SPz4Sb-lcP;J-vZ0@-CK6ors*Bt4A9p>1F3+4>f`vk1<)CJl^`D_f@A24oui2 zYBqh(d%GwxH|K|+S7jgha5o{KiL=7_reTa|zO-b#Uk^CxdU9T*MG zVH-~y&v3w)aX}NEWR_F{vjAkCcm50-MS|=p#IVqpbXPF%rfZ+5&gGA2FjFE-CtZLM zx(dy>I046n!hnS(!ZXPASs%Smd{LbYuhbwk8@K;mk1-iOGT(%cDhnZ8iZ0P2&qZ-} z4up2jKEtD6wh;4%9Rx9CEx)p7qCu;?`UCeYSgJ(y(Wp(slJjxWy^Q5c*P)Ss&j)K9 z{k!EQ>-#gNO&c2={cFXRKV+2XEYt{o;u#;;aCjAHnybD-(hV?_p4`LZ2_LMH=a9gT zVaW;6b!&P3ov_;R2O6Xm_2;~NoG&UKVM!lvBVE^vg_X#q@cP|@`^;dftx2Sz(jg>_ z(PTKF7rOdyY=VOgttI8KLrJJotpbTmVc-l3FJ{gFx_mx(%^vq#ck5ryLIg4eJe=;2 zp>u{%c}4fSvAnKF3VggSl#Xww%Jul~m!h#>-X{YD?yn2R?gXi`^Te21-A?zvP%*XM zZPnQld9FbdAX>R)k-vf9Jl3#_pjb;oinQb=Jf4f+?F;2XqlQ{0oOyx!svX}^^j$rr zOMq*L$ip#c`N+|$+Dk+J_-?kd3od)#zQ_s6%@q)Bbw>@)F2lT^s1wcnU(!d)U0LPxcgyfI!w z0iJ;+(fWs$-V*07gpHhJd>r^Zw*QjfIaU!R)FTpL^gZwd?}Jpr50y(L;X_{{65QL! zgOTg;Ugy9-Sp{Yjp|^oS#{)}SYvqaO`{Vp~<^uU=-C_<>`TQ9Yp)B}cO{df%UT7Xc04%tW@ve1i@rKZic%UEo%x5o}C1vV=4!@xIt`p)t_o=I{ zRm8UFn@=kflNO7Uc?~Z6qIJu){GBI&!DM{zpQH6RT2#~wp7fnO+$k;^nZM$g{sppun`0>Ye26{Ax@fR|xBn^i- z=I9z_$AQYg2k4Wbv|oNgP1+YHl!p%8h-GC-uJ~Dzr8iaJ77U-om{4qo@eo~~XI2?! z_hDhLz}ybX6@ymTfLT<7jG)U8fiNCngM8W)aspP{2!Vpr{T+I%v3CDUw90|T{5Xvl zXrwI2)KYlsWdpHN2yz^%E3}*U@)KMT#b(|NT-wtIZHk-}3d_w(Hh#&B$R}=*A(N*0 z9NG2yx@T%#rh;j@ygcwwPQA7yA1!ETXD~t}MB7i=@IW)B;kDIO>D(`v!U*gr0cf~a zh6x>sT}e?vW>{n*Iw^)CCfJmF>~CiMi*?nj%PlkQdGk?V>bV2yI>En%en2@wmN<5x z@!?gnR`aYmt}98`zEB^=Uk_?_-d5%Hb(}8(#qV;NfxMG){u8?6b1#@WKkY%~kWvECj)Zn77lrh(cq z@jT`if&rHg5PzN@5H(HI`hXim0PxKC7=;0D6pm&F4kpI0ogB<<-a0;>9h_r2Y&w|{ zf&uZDNU;Di^8q+vb1vUYh((jS&m8Q^n+ZH(2Zz{{NK@I@mp-)*t5siDJE_aKzC=$8 z*C-RmG|qeS)E%GPUsgeO3@8D5CFeV_Sq~1+;tk|U0cds1x$K9zTQ==$ZkSZLOWZx& z^$Fj+l6XgUJqS@=DR;Jp~aO8m>yoCw8*Y+{u|fpzh&4mimL6)wwNlYL;0W=${k+#xv^9~P`gSXo3)Q8-95W8$Z? z*xEs1Ku`n6#w{vhj4QJ%O%nNuKXC%I>J6Oq@`L}~n4N&9G?^bu$UGe0^KF+*};MCwxbKzko zGXjvH9)FAUBLlqdqt>omcC|Y8bcuh!#2-AW=>UAyjn{F6prOc>%*-{BjTc>;x6#Kr zFCj{>DRGXY7i}Z;!TYMa7DZW!9!)8)ADz8x%6h6^x#Q!Y=4vT$zx@8h3ge4+__9kP zeXMYpY&5#kh6_#Ea#+U0MU0-m&11wlX%|FuAz+^tNWRz{EZVf3OT?n6}fgMNcQ@@`d_-swgQd0v{1 z4AqMQbOtoEkArDk8t`opi!@CkF>oy_ou5cQfdx%T5xCNR1J_v-)OObPe2|Sl^4`!} zOFbDHCg1FCs*Eq2c0O?T9$N&Y;*QDIzWX^}8T2Zj=uPG#YF@?%>J*z*{5MCR$-H-x zbj`zrQSgr1EBNLPLa9uNbphTNbce*el_O~q`M7W(>rgLFs1f>_W-P`9SYMFMpY-O# zD+Ff(a7et)C1&&w(t3#ng`;#5Eo;PQSm@OwQCu>-2~qlveYkMdZ!+0l3h$3sl^#PF zh51jdzBAhLXtg&BDC}!ZBJTPIR&8|gj_Ejf9Z68NWqtg68a$Tt`y$(wN<}C3%0(Bd zsG~r?@heK3dDNNnBosb(BM?psrO@;vcW~y-+GE7^1r zC;E)`nMjlU$UKDqZt+1yEGks+KD3{LNmr)-m4984SW@hXhQl4;ev1REC^*D zNu$?QyNe|UYwC+$i`P9$9S`drNH{r~ii#6*vgFcarEkTqDaRyBL18c<>XKHH5mcwv zc65-fXVf|)b^8czF6?AqWEZFU)e4yn@iF`?$Vh@R3 zVhu}&sWXv=h@q`Kzn>_WWXYlsBb>*ZsaP3_`JMB_ZH;h*S5OGgy}Gev@|LaRijIZd zfX0RlK>%#3iq%3A^^^xQ8ippMOd`kkZ=P5M#flqe>9k2$ZjC7I1btP9P8P+~X0za~ zjn*lqA^Cvs$3mgywi%HIqnn-hptKDZ(>7xJv9{@wtc0&mVIeZD5o0iO2nh}2TOp-e z=0rS@H=&bWF-Oa#cyJyc6iMTz&(+K7v}ER6OD30ejhQ+vf6mDo+5OIebiUGI>IBO6 zA#KXJq9AdktA3KDTqf7ENF0XY+)!)L+~UJ*9A2rK%xg^Zvzz^odapG#MWEO$OkO)r z_&`o`?q8_sOd6}wHo=Ep*!GV!)-SWazNw2dm3~m0jHKfGrtWAoWS)O+OEHpgUz*#X zahihdiZ{YgkUH6x_zSWbM#Y<>i|0dAT|M7AhB zhee_0R!3bi*dZ3y%O2ANM8N(TrAlizd495LhQVL_8Oygv9pNt}QOhTHtAUdJj6O|ja;L9?1%_=|IPe)QQweHL9gc`Wzaa)& zf#KR=*Qta1!&~D>d6u*`QJ;YxIS~|o2-T#$bDs$t>u|LMRm*Le792~JmC0n6de>Z((mHB!20Sh3i|J`ijTcPHuu;9pS2s?%LXYW2cq+Lj-v z3_*&;#JhRNrxC=RC(OEdEWJjzTjEuu{_@-9oI7OTyzwtWsf(3Eg79+X+M#xtTd1-w zj|m~J)6G|6ceDT?C^i1^E%^bPFtIsm@$)sg?4w{jW=p6AW0@HO?#Zloc7|UomNCST zW>>Kw2$wvs?>f%00XkmCi=qfhVWI1TE5H8%mN0yu zNh|b|F}7pJaN8t5!Fsj$C-Fouj~Z!MwC1pk__h-3=P`0tw^eVUZGkH$!q$|ZH}=`k z&LN_dHEDz`haf-mVqY1(`vxX%4uj#uTL^tvou>A8YQDl(A((2y8eCQRX7x?(F z8Fb&%wf8gO1)2=SotVn9w|MQK3+F!>N^M5%Ywcs=(e<# zHqP7K2$iI7LBnz?qSufb?A40taSa@Nw>x_WM?UDzZbuTz1R3C&RCnhmXVrX*mi^k& zvI4JU7Qa}4g(P92^iv0k#}r{Wqhu+M}l8LG2a8 zs`bMok#K$nsl^VdL+jEDUufyyt`~nvUI%{ThQDPZc%qloP$ARnHxMZe2X653b*Y1= zguDtd)r4C>tB`E0+&71p@pKTu-%19*Sv~R`(oP&&dK^Y5@*zfYL;7cso{i5S7>Mov z>i)`;{3TV1gqhxPUwix4FiMTKq=kghLZ4*Hh81I@bGtsvs>atT18k(LA%4EY@I>%aj2M^zC?Cvm@GQ1p)E3; zrPF+8+or7U>3MtGs1|S8k|k_9&iUXPO>tqdT2^kZ%5Vb@ay&>?*yM+}+Hqy{)@ai< z&htg=4N7WsNxA>miHG?xi@Qi;G6U|eV%G`L^!?QEHZWkDH(`O1 zJXXg`d7L%m6-~VP#GZx)ZBeH;(EjEfagQw!XS%ivmyge(lp`J}p_FO^D z*S5Ar`=e`osW~EJY;;>Hu^G-*F_@77UODq^C>LT-P~2k<&iq3FSLu+FQd0&*G3}s~ z*WpXwwA<|IQ~R{#iNYy9j$8{)P3?4c#7<~c4@vR_Qe^KxkhAu6ZzZ|&bLdGroH7)< zKAxoiS(g6_UH+3e#oR)jZ-HW*24*nufN$W9^cUFQuV+uvZHe!&=z6@D2DAoDyt9w9 zF-hQQi4-c=x&)zA+8FOfN~-0s6^Iu~6CPmvgx$ro{_sG*zI;Djvw_dYF-j|$jRUdJ znfSgG(`(x{nu$2HhQWECo)#xYKdY3`1u$=cl-$qG7q(_7tyHF@@1UXuYd4 z>iQwzyyn2!ewt&DJ&JQufseT96eC30Papvw<$641>!C$Ql>@T1aidsdh?A83WIPx; zRQncBqa!N{jI4k0WGtc=|FH5-pD6!TUP{uAer8hOdIec?q+x;sZ#;MKMI3^_DA54% zY0#!NDYwh4BlBK{!6bLe)+qB1$3Ssg%Fky>09$uRQb*uj_65*HAEUimBDOY8CN@rb zD(-eBjyjL`*_wnQ;2j>A?@7Etj;+IFQ7kN?1&iQMVo=_LuD=Wv?M8M-2xX$wH*2(>UNHAkbsmwLSmn zefCM7zcIa+8|DWqJCkqoS|RBOd3~x#^VROU(!}o@@?n%eYpH$Yi6ZX^LG$J%GLhN; z#O;-)q_dpD)u-?5xUJj8I+zZtr$zw3F4O|8K!v6?HQ|l|2k-l_*h#|EQw6C<_ZFrY zj?soVJd+r@QKfyeZjQ&7+7axB$@es?o{hd#G!C-y|%;EFP!U>Hw9m0zVT&$R?p6bL8?G4QEP@{cB6?nZ@*BGVXx@J@{dX z`fmt-N(qekZ?5b4K%e`#M?Sx#MDusF|L}Ov={%3#{ic%)EZ+I2Sl)9A&%+zPDbxeQ zfsYja3U>VO=Pm)<27##mPE7r2AiLkLNA7+Th(UYe`**SZw?RMF zc>HRQ8qWEp;YZ}ZDn9-k`SY5ON8Ddp8~YF3-xVOwhyOf7|C`EN=)Z>luMs@Gp7Qno ldn)QcI|K^sI&gd(6KZjd~28{|a35u696;Uc^T=-?(!Y8iN>#k+=_Q6KcE- z=ie1=3cgmhsi0Q))Sa3(4KC1Rv}N#P+D=`YZoW_7X=pRxm;!qeBb5g+QgvA&NNFP| zBdLCQC5F*91x68<=Y_YKC{stt{EMs-$8oG1@5Y^)TWGA+iE|lGbh8w*WvfRVp_8K?9|h%z#oyYAHRbqYON? zhSVQawGpHNMvS0>kt3S+P@b2!vu@TK@V9!z{X??V@UMjbJF~FfK`f-~%MD>2I2u~` zh~h(-OB(fOgp~645dJ$wNYPipBChg>lCy$qa!Ai@bp>e}I3r;f~CI*|I; zH-JrY{F=ZZIWU@heI!_jXp*0um^m?$JT^M}`!7*uq>G|Ey$l23 zz;GG9KzqB_-_EJL{@npCW1pM$qt|Q?!Vnij(MS#hSQ2EoY^Z68jWEWUy$q7=^0|Y4 zm_%|GVJhkq<~dcIilKbFX#=d8MwgXFr;kCG-T}|O<;{&dpQ8gEl!s+@dOF%YF4E1q zch0LI;8H>Y*E_@%mfa++iiS1EQI5!Y-$mOkvJ!5T3XCW@BCi>jhURg*kmo-P1HGjS1-(LFWrO9PGqC_cBJTqiHW51d2K+S}$ z5UMRxhP=3;I%=pMvyK;EEshx;pCYo4wH$64T02~SX~X#qkhjH^dC}i%EGtV&C=yv3)Tq&qoTk#fa@ZS7(%Pj+Bj-zhC|XvE~l!4aR9` ziVf2K2aFPL6Mli68^m-ND!KU}qHKEF23dSd%!zB_U}@;Zfk&c1(r#P#Ry%zWhUp<_9Hi z$ChHOWMj%Oy0lj*(|a+yVG1c1>ir@{YI~KxQT9J4)cf##*nZ`U*go883BCv`(xdx` zp4p@0$zvx4MBLK+^Y#Ae>qE&y;ps2FLVz`XZT73PsiPN#aTrYwyq650njbwhlDyh? z>(G&zFFum^aY{G2i^~uojM;GuxqD6}XY#na;6PHYK#=VUvYd*g_Oo0D>+PhVhr{)^ z&{=E+IOAcE%Lwe=?eqGnd$^89UPUkuiba)bbOh0s2pXo|?E@4RAQ}dc6?srsfwa-# zG;n$*xT}i}cqoS9lyuO~X&9D73aJ^E_I5$l<@2&M`i*JiNO0zMUtpKp=i2M0z3yE; zinBb~vh|6LEw1KGzuwZc$+e}K({0-Cp}LSuMk9{{KoKq@6ZFBN(TaFTFiZQpL=)>mpS-OVloRYzpj=z-1%pKB2ETxH(rHLGS!j^wmr_MEn zH_urxd&xQDDP!DL9<`NE+A89qqJ*9o1u*QAgdtmMLq&V9W8A$kN7%bra>0C7WZ` zClhNQKe07ttxpt`-LRJZ%>XR#=3+Tzach0lS|8R;Y0bxo!$c&nX3RNO5Xo5|({4zZ zZ4s^Qm%B#HV!w^yx(0-O0HcxO+Oe9krIErlabj(hSo`-mB@EtUs$tm&Hs&{(uy2Y~ zs4V5n6^+pTb14qxw`yc4*}O8){Qyw*7vdZaXJUy z8AR#ee+ABh_PmpFGw7%y+tgF^z)vC{-i+oWKNbv;mY|Ubwm>iv$_qv(aZm=hB%TJD zZiQ3X8bo-M8vi```ta-*S7r`;x&vYhbro??OXC0zfZ>8WvIV&thgF1V-eF zQVhC!A&ht!yn{O{!FUHBD2lv=nnlX<5AczISM0;2V1rbVYF2XhuV^IWrSxLGskmyK*k_t_IO%Pn|h0s24<6V|H{rb@c$+M1F~2!_lRb%@q5c7J3a5prxr- zf)vAo{RZsjEVSFpP_EqpDV9-mk({1}CAuhdT4)4UT!ts$XDL6+DZ$QjS}#k{bYLID zsr>=g@1~uIM(LycoE}Z$jXA?vwh5xpUI_NR0_dE2-imNaI1ES|rVGL?N|@;5Z3su8 z@CX4l`ms5+*H2RW!QBY#;|$`6KiJ6`#h1NqUy!1a)ngC{iYMPOZ=|x2k>VHQ+D!*K zT_CupWC8d;NU#+#tA=|7?nACaXPY82Bi70hW`v4l)yIg{Q+XwWq2r<9rm?J8UVZqf z1YwO6%c8`x7_kDPmHfhUMW>3UEO|rPxNT+BwsO+qOym_L3JRxk?cj3co5D}enXtm* za0^fyvm#k7G2+=o@yd8{O|-aXvbYX88yk-62h;;hqIl`B`;>RUFtv8w`2MT=zgB*| z@f%HS?UNUj!%ZXj`OWVd;*OfAqbBC4`+nPi@jtU2iR$$OEyD#jtQB+MVQjc-LNcu7 zSDMC%$w$^l$~R1SBUw+yh%Jfik_b`q%d|N!ZZ3_QOXKFcs2PMS=9j!|*gd>GlI4sM ztERH^{yt^4rN>r`;KNKLt2{%B9JITY zkRMa<5sWfzZbMRjszb`80n_G9SI_keXJIYA`^WUd;nqLY@$G z)rK@+3Z!@rq9)CD%&7}D$YDD*{3&3r943&je(vJz=*9WYw+{lk0RlsaG5z_)w&eA` zvkiXXGSPVV|ToT|@VuY@hI2EoRfbe_26LgY5SePfxkQ$33IUos0 zc<(^!>}f%VSt7>OLs!BegCdi82ojuRt4Bx~&4U`X-wW`2pZmEONeFR61;QYV z$Q$xS4XXf%1~v~pI#e;-9m%SR5w(fz{F|1-p?`{6oMH8pHE&4$=F@R&S=3rKX5l|Ibq|2K03NQ{FOu{e0IQl!Dj_ebyEtPx>m#6SC>vUKMav zw`0h245?lQ?~A?xsRJoxLP}S;HzSm>*p(EoGee}M!p960y1;{D{T1mHK}hmyk? z?1fyMY?hQ{hFMPmuxR;@AaV-XRqlT=q=lpm)**S`f<#opM*^+n+3P|&GUEYG+O&rL zCxAl7Q&h;b(Wfauij=HROIdW~Qep?z@Bw(^L=WiKtM*%DoQ*Qh5=BVUA$j0w8L$p% zC#7Eq@4q4AmKjn5)Md$G8T4JudoD`z6nt!+TzVwa>&Wv=DNK=~fMXY}#D@7IH zLs#_bWt8TSexV1>N)bfIBA)_Ci<~ow_3D44@82iXvh+}n;1>o@g6?}*wu@=3szSiJ z%gs=g-NF6fytsSzKpw;2Ug-&RRk^u!0Lw>}1kQ}|Lael2<7;LN}uBr61{JUjlU*{@!c zF`Aw@Iz91v>dN_Zk zH;;e=Ep*F*f5FS;U1&ZrAm}6r2%@CL%*Y$b&yLOh@iJuTQsMFGi4RjJKMbOfIC<>& z%!zY?7U1!t^TS&Z$mPw?M_xDVgP>ne<^rg$?DDtoaGL3DKqFX^C!vLWR+P2N@G_*g z9lkRtQCi|9M6(tRYv3CIfogIz3*Tx7`hL7oqiX+DbT;y(^> zX2!jjaxos7qWsLB0P8deUbMyMW?24n0`ix^EYm1y!J8`1B3kQLg7A^U%XV{?fcVj3 zo|?;C(BlKk#AS-pfE#>PfOYUjk2h|DlWhPI^A-+OLPBl@boN*lxYdv#JORDTS>BHZ z=NGbAb1E#eT@hA-HyqEajOKw)J(*V*CcqAbeKBppR6+4jXtLlDAQdJg3X3CJd%{#4 zSyFSuR5Pb2HUlo zf3@UHWf!8p66hN;4Zkw%iR7+|S<0s@)9`4b?Z%RO42lguS>9X=8N+;3 zHdvtivu-1*zRh0&<#)B#P3x6^(P~j?S-Pn~`IiPYv>_MQ+6o`#;2RVQg&w-) zxi&mUojYco!=U=QiLbtIx`W>CR6nY=3eu1aw8Boe*Y9#cEX`-QcSA3|0S#%o+<@~8 zy%D`Aya7IHrkA6)JRt+lRNj{fr_0f(RUDOnxTF!{@av-eh{(7Aa_B{N;-2;~Vaqqr z-7o_gXQl-TNC@NjkC^F4%LXIpp^|=l8zfH!~U; z5ophI|GNYJX6Dnkyx;!r^9QRnmV-0oQJm%Fln_6OPV(GK4+1z5OvCwCI zv$Z9zCXPNEnr$ui8asWCX^wA6s7a*H#%4!LQcY4za!qneN=-^jYE3GQ=bO`7(reP; zIa@G&m9NPVa)em8X9`Zi4EHR-@>Q-TTd+cyBjgHk5a$$Z@Rlp&33hj$5bw?x65RO* z4MO5q^)&^M#sO&x-G-(Oa(9I9O`F2c@>A1R`6+yVQ+for@`WTQuNdG;hWiYmKuCf6 zOn^5P?z4nKAr0=cg>-~3+ls^Zfs zTaVT?HwxR`9uHJfDTK2mD3vL3_1bVxaTa*S_ghT{oNHc~aqMc+$WP(B!27vx!K^x` zeN#%ub6&mtyWH!YT0!gA)+)95Ie62_sr)>=DS7-Fc!QeRCFtNyp8cGlhc~108@`#8 zHv_!U6@)Rg2DAK|U)z)>KZox^j9`3IY7-9-MNMN8V#8@nTmvVVzr%Ox1WTtL{tSLy z1-B38TW5^v!FtN-kNFmNPN(Xv3>l)PO!+B%_s96P7vf|HI`t1U8n&r*@LVgexEA?I8qLIr(F~7&v>Xm^zMR`{kXr>U)X2{;A9CarQu#^u$fNYi)4aDx&g0h$ z$x8c8D){d`8&e|SFP2Mcnjt@h??P%AP7^1j3F&@L$PhAxEZ7fn1ZRV(GuAs>PTy1_ zKZWo9SV0f}axdh)Db46kv)|k_C!9il67mmE=QuI_f!a`@ZbNIKDvwhxFMOB!wGir^ z7mk&mrp&kSc;34Qi`2FI^?uVq&2}!I^JdFs7I5O?PNU!0X?P&W>^LKZc-6QY<$f0| zLP9$1A?K9Y!*jbiSX;&?jXnIyi|pwbe~f!vyPG@4Pl2MO)AB$NOjFfuk?W4^8Ct95 zNz2KvL7i~FXPw{gG^*;1wkp`JI9*kz(%#5A?^8~(ccI+lex-z_MRHvD4yCGBTzHJ5 zK{vy@RL&GiAXnfs~Beh5B>JQg7xXYa5@4S5d<^J)L z&y4rH@E`ALxtnte|qBEXO(UmfBo$nKmOi$_bU_6eQW&qJLB&@Jw7l%Yx^snovbIEK0IHX zicp5ZfAskVTEBR5x}V;kZzaudzA8!9`|HF5UDltRYO2FA~I zjR(%%xcGIJjTSU!PU=Ch4ZBWvAZ78n3?ugeXx@!T!#LPOc`*Ck5MjAvB>+|IySmQtYX9fez ztLyK*HU9Rg@t=QvA}}yE@WR-;r>~#w^U;Rh>sQu4WlkS%O}I*mVVW zx2&VsR-nw@@wZL_D85t*mfS&78oDMRH;yP9g6nTz8vpk5(jc>_@xEs#`nt=UA%ld# zJ3}$eZ4Iz=8$!Cq)`M*#qM{+|k>)xtytLG|*0s1pW@(#YFLmL)C1k-Dc@GHLqy>YE zfu=RKw$+Mm7^F}#{#4r_*0p+TkGRE_MvA9kgR|WIhygtWIpj253EAF{F2kVw))9J^(`sD-z~Pv)g?n z6zlOFI3l*y1BDRMioVuRtmuaEZ>?_=+%R*z0%U|7?MK`pV=V>0wl-w)igoqw1F(~Z zVzEE!ytVGf>q7>*JUt=ZV?Nl(tRa)K4~uwMiHkA7QG%|cbWW{LZ1#wAF($6L&O=L) zNSfGiiV*J(>2PmUX0S9HYwKH}KjataQD3t+WR>4(i#=8-91j!ax>HzDws(uz)&$#= zXD@E?wL+b@Zm(RQx3Sq%a-=nlPwYvPW+?VAshmA|)RK5= z-sADmXVU3Bz;%V@`xO}>B9W}Cx>RPBr_f&>8pYk*JwgUZ$n>W-ySk&YjE2R zvQrq^<|bPYlFWvoghRw~2>Q^T)w{EA+mLN~w{FyEJ7qax={eTh-g}r7uO&(Ag2sm- zjmdf{_C##Y+>_QxgT-{1hsMSy_jr4EkJwXs)BASxKTH-?U8$PXYxClWCHuCK%bxz~ zqpv(lX05#9B{h$dLruY~!`+)kY+1v$>4UcE{jRIF*;5+5on%#iY~L{k{boCD>nrLn z?$00#5a{8ZhX;2aCOevmvn6P6y&cP$>}o*QkfgOiprsOQR1u(+V_8IPyJ#t z26BIKdpT!M`;6nE)!5COPi&TZv^u!s5wiQ>(2fRDeTZZ>4qKWAElpocYBB8>x8pcV z(q~+(J`Q`;dcxWp(`V{^g3KuY)%?qk{c_Re=_Gl5(6~YVHSTq9zw5ilhKm*t7A+>z zmXPG7L1S6?*Y@F}WrIb_hKg2@X)8(cLqX%JYv#n4kG}NypgHHYwUZjHHEG12e8zm* z{4$IZXNvoLQqNKPocd^K3mNs#jA*$ZNoD_SO{$ zkL%BHe!k1@g$Q;<A|(B7*glV1xof{OFg z9t+f&T`me}#0)hzs0laFDHnyvtcGYe*KY744n!{~<7lVOnLZIZ3Su(_&`tU09u82)pW0(vfrnS zSXS0C-mCC{!nC&6DYyQh7W!j0^vC^cjE~l5k)suEfl@o-cl3gK4n^|4iu`sG3THt9ZPD19q)5Q>w1??WRgeKhgiu8CK_H~!vB<0k-l{pO8}=O%hDj`w^Y zQ2dFnche*^7+2Gn6oV-Xqvwn+qv;g9Crp3-$GhF*r!L;?I`JRxo(LDHCIb(&wKbPH zrRr(L?AL11=SE_$=lej~v8c6m%^o)`Ws``@_73k1aXwsF64p!UMxbS{pMPg9pm>b^ z3-quLnQ#&z{q8&CfzzyoKvb*JOoKEM7_?t*c75~4`^OOk)FIj7%~>s(wfEjh@=w$%A>zl9ZkonO!Kz-ab!`(V4p?em1@DEAHi49{~X zV|83R_e1S5{ye|wJW?r|k~ty6kwapg2XJ1B=+(A(8a%j3JDpwPBXGZCTHfGp1rEBn zvSV8PA$R@ZTDgssCtk9gVXG@klyu*LY@T!oKEJ_z-o<^!8Eodhquob)(}K3Vzu?<{ z-qOL({}dO`7wbD_O655lJx+I{_mEq3dJ%_jtpk?5vrTliwt1Z%Z=DFQ9kc4&e9Z!l z0(ATFcBh2hJH2gA$m$d+0`K%tin=4t(^h}j?R6fiYi)4)j&!K#X-Ij{=YbL(Ep?B> zi&t!PL;WBNa0_n1hkGU)JVso~WT%K*if2;?!<*v$QOOkd4;C1)sYM$k6nFC0Yvr-u%0W)5F$2iVbr!LNU^FC`KBGPz(?;&5Z}<)4pQ8bp2Zc#J7izCVRN}_Lv6*Hot3NC)K*rA z$l4WGK@f^LROhMldPQ*`zUc+`0bfJNR__x<5CWmd2uXL*jOlS;L_C&67Y2tQ0Ior6 zd&st0VbrWa3Ne%%j@ZT~8jE+R5vpF8oQQHYyZ&~jwL{U=_MIEs@CUrCPsbjX}o}<0~plw!Q?eKy%R~M|2$&#Ah z4P^Q{lKybeQaO^CdM({KoIYnTeNMMcMsL%5Eu^68%0r}T4@uq|H0~R*C4N3?F8GYo z=#wH6Z6?WEg2t_%*c_u~!0&B(fH|d?e`)QR4W~D}w5i(!)QZJ+3wM!8&M+_bHa)_z*UZ-bdbS0hmo8tIsr^k^7KFde z)I&@?E?oISdY|Iz{~K^-JWf3K!_*;7r_Qev=Y;Vozs{@R<_fm`KFC*eM>$WX3LDoo z;qsbt$vfMdFop3FG*sgADo_Ngy$Bz@{Jfx5%3kc}qv798@p9_$T~Kh}PQ70*ru+4P zp(V<{Hz_!?90T?1{kljl{sXl3;s0i9r+XCuqS8nAgBfJ1Q(^ApRPBVbW$GowP${%g zO^XPoPJa`>40j1MzfyN%i{~wjY4P0 zWA7SiQ*ZrB{<8VUuG(mK_tW@8mu&9%=NFYwR=$HovJ>nK2;yHq|P$ z7Rb|0wMrZPT6j~S672AnB$p?|!<*6v^i83=gar5v8F=)fHH7=s)TBtM8vnJbb zg_*3ZT9_@!2c=ij68R~7_gnp@3psNAoo3ZMQbr}5TAK6DsdY%Qely7Wl~He69?mB} zN%%=gJr=tV9vz5Hq!s#@~6Oim${KkXiQlwJW@KjISf@|%SsrTsP)H1}@*boDv{ zIi$I1l~kDO)@gsBvWoro$no;q{hGrtW@6C;<)5L3#k*Ptz;8yKmN)Tif(ync?SV?0 z8By9AmK$~hv!9duNX&Vll4h$*g8a6FntBb-ZCCGo%AO$C11p&AR3+-%-gWZtYB+e< zPfz|p$9boY{(QdycFYCg9rJ0F-zzd_;4&=kjPu8V3_moe}gk z@}p=GWOc?p83$Sfqp*JADJ*TC0!2w@`~yKSOI3HgTzBN00j^3`XZ(}#CqwLh&pKc2 zj8oNl?-i;(drDRHD{Y7RRk{;?qu&5$6>tX+YhdPVB9?Z!o)WMj1qHE`3>WE00#qTT=wfY72Y!y0Gf#g5&WHd=n%9u3z9gf%sFx9 z4Pc#w32MrAxr@_-!oa9n2eeN7RTdRSS1ky`r)C#T)f#{%N5Jy6lL}m6tpQq82grx1 zoN)ye=Y}kbG!ZxnA`X?W0s^F1e*!830fn2zx@Q7J*I(~OcF@oMdF;}Q;r7#5ObGKAk-xip)i1bXPapfejamMQJY6hCr^{;l?97T7k)QIFyXf zOj)9`6*xl8AV&sia3~eyE9gY41ZiU<=xTP*UqnRXM8w`i+!96H4Ml`{lZeYzM2t*C z)JsHmm5AskW$=i&JVnGFMdb8}P7DxLl!W7yfyWwyiZryNV3cqxb~5fBi(|Z?>)(27 zqVKreoe^zU|3sO9Dt!Q|fdTV|)dRBvs)|UD06A~a`4Ca*`>#zrbx~0az=lVxWu|W6 zE5vx1?c?3w8o%@olr1w=A>wkTDh%QRd_p{3C6PxYR9uPy&I1uoZ@LSJEAR<{7FS|`2pZ!gVdsT8P14>L zN^GWzKnO@JC@%P5nzvJiPRQ=5JLs-OtpI83EKZczMd95e3{c1p`yu46RW_cGL)oI* zRk6vy0R=@0;B=v`Q7!Po+$~4Eo{%jZC%BvI+CwqYPgMCp+=A_xi@{b5wqdXxgK7*; zKmZCHUTp!$T(}%Fj`|#)43A<1i_m@l% zy{cGQAPr5;B|67nfHjo)dMuf_?A`4_r|WVD+2I~s=MFYDla|iEaiF(bq4^8D-NM^i z4cwaeb(%?zTd&!m`3&DSX#Q+YI$P8?qrWwnzG5VQ_HcgLV1C(f{>s7pl>?+l6R0a93uxGC(ibuy#oNTJ72FNr4RioF<7wXN*USRIJmws*m4AF z68Uls03-4bBQSUaC`7&rZ>{`#1ZJ~l3j)*1Z_$kAlnm!QG??=cv1Cf}X_8$__8%d& zkC9939n|dr{e|otqDvo1PU+e_mY7P?N{15X+~VB4KDj$) zQX}vts&TO4#D?C*eM|dh_E!gr23&8?3}lg&+eun=(6R$qR*pHr#JLd?cCuhEaqbJ+ zA04$j&cvRM?R_X{pE+Vrg~TZ{hLUG?uNx_t-Mubo$sNgqm!KtQB#*x5jO3T%yK`jP z0=(ys z?oE&_X9g&IShIR!LE$4lYt)f}Ovt{){Y(32l7-tz^`m5aEy>*W;8iRQJo0x+)Zi@4ee|sJDP~|@KACyu{VQqh9kExxj$hjaaMN)&KYHR z$?PhUwjSCKS{~NJ{I$I4!+A>v^Op43usW48OXZbPQdvW?5EAGIM}FVot1ty3JD`o^ z9tk=g8+8=*8v^Emy&pTO#v)VfC%JV&$AQ}x&Ys`5sDEkDws1;C6(nm-(7tvgC9Bul zcHN_C2F^C$uQ--|X`zAJgFMy$z~EW=Bs$Z)5Ks(9Q<3;}CH+4%?ds z?Nmhs(tZJoO{iL8W{XTw>**H)iwCy8y(}=BJhU^otA^~^JG5&b+4(3*uMJxEkJyr7 zod$9T)8=2b%^%5}*?(*x{`?bznag@i5xo~k8Auy2lhu19r6Xx6F8$bEh`p$qQ`IDU zN6^0W&xtUH3;JpTX+g)b5qr{Ktz7)HFDA_Z;2jT&3x1X4O4%B#`StA88n|7VVCJvwG!U`u-r9ki(T_C)2GABza|z!_(!X0i#6tt$+NZ?V~EB8F~q2c7-H4pv)!~c zMo-cTwrccbg&txCHF}5{jKNHB|;d6jAJ}AZ~X#-2*Vk8 zpL%(fh-gew)vt)ZPPt^#Xd#uBHYrS*a56LqIH<5?7#q4?BY+uBnDvYdfP8K)=i!e5 zGhjEz!@qVdQ?f7C`4F>YqO0p~UK~I5T_zX5@trrvU+4q8{f=p_tj0@eiB0EqpfpHj zm!L+xwzhK9n(B~w`{vc_YqwW#TjScO$kISzuvin)9cdH2A>QKwB9(JG*)bZ|=&8l{ z+Q!yekaUXQhTonByxoIbhc0f+WIL60BJ1U&XP!L$(?`(LmjZ z&3?*%!r!~6zkpEkg5aRKD zhI=rZo^u*O^W_8f<-cCg%9bNA06O4IC=v+Zk7#jd+I5QVf}RTdC(dm z7dP$Luz^YQ_>e`?2c_Dc9weBgfl0)IYne82t&~XoF8qERM`sm0fWS_t(_6=k38&&t z#1YUVoYkLAQkGoSeN;2Nv377{ZSasde5hmaP{+R+p8!D`NS}DU^%J87l+*OqJ02vd zo?fzyyJT|3Xg@IU5Jn%Gcc4o3>H<~)LJF6-)QjtnUQJQfjbtVhX{@HK9#|PtIl$~t z)UKkbS%n2LU{0u~q6fn{Z6~j)J&Ga_u+u;#Os(n!(+gY8;I+$7;X4S5j9x`KE1LS$ zm;ZXf6rNc3fh{)N_I3+QL5BxRGK4)s*g^PMkbG{?<2d{OscRFTmvE-SM}6KGF4t4+n~EMNUQuDtnPa zolgurbG_$TP_+=;CGN-FB_$)9;O4dL6Z6PMyBl z^%OvP{f8&UfANgOUI@H>{n8HrM5^8%Q9Rfm;F6Xp)=kA0x)Z9XKwNq%0&hOP;<66u zx3nE`w~DV}ghI6etY`5@sqv^hP01cfw?bayRAD8Om=w=LzEiL)JU@j8(0ev;#^fF$ z7?UB3-HZAP`jSb}29mllXxubv%;>EO8jD7a7DYQZmUCqH3cWi4ktOkKx~j$)-cJ&z z_ow$;Nc_T}ZqcO1pf7{MO^L(C)InovZ+icNcVmZ_R1Gev3U1#wyxl#x-TkrgAl3@n zz$9t?$HoovCL(L3lGFu(t%21fdC9PG>7a2bs=eyV?s$+}@zD%w+21Qt3=K`H)^`%sGWc zb^W=YPSCl<7K*&0=9WC}9RJ!$=RCNxh13y$mx0ZlIBC+C!q9tto{kvlK}5`|m={ao zcJm4Z)oY8jA*1Mi%;)xaJ>s(v6*99-pl%|#K@)_|Fe9!f)a?(MnT`i8EienfTS$xX zP>AOUSUtPLS|RfMleI$7vZ$67F8&y5`X0_dOT(!;kBqd`~%h_j6njrtAowLNFin1yG6N{Z;3O%!2yygR54n z7bn;RdxNO~G?j(;PA#40&%iX-!4ZXvf?iRzmUTh`{7byxkdGQVy^V67@LhkL(?(GKDCf27ot$3eFB<0aL6Qv0U>2OraE>7ytPo1Fv0^03 zfZ5hjGz1V0F(2}a1sD`!Fb#tu2to#?d>BeT*4Qev9jmQtZ3N3Mv|96k)ulVc&k3c2 z;a97NdZM`N=MfE zeh4;6zsOjS{7P+GL2^MQIkXGMUw!BLYhA1lpqa(mjtnrKLp#=zuw`9I<)-R2+khzT zD2noquy9D`EI$iEM}I z!?hwekBxYl^A2Bf^S|Jdnsno5KLRalXcCHWBzs6|*9aIR>zA9a{TwZvoyD;X2UPGq zO2&yu9fx#HZH=wsDty#?j(~xnD8jS&RS1fW^ym_QfMM)b*0x#aXq@8#=eyP83FgeBgiAG6Nu40GBv)QxDOFy<6ZIl z$pJSp9=t1l582m1jE5#o`V>Rg=E*oNDY+?w))UOy6lKAOC zUGZlcUA&>I5_r4u>4@5_?AC!!Ymd-d*>^0U3p9|2wv!#ZNnB00b~HZYO!n#QK5d|4 zzu8%F{vbJeepZAT9*E$0E-~CEJ94sm$)0UC+Cs$`uX$YS*grL^+zlB@L

ecECVSYilZr}|VfG2?LCVKgK`49uN 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 literal 0 HcmV?d00001 diff --git a/__pycache__/permissions.cpython-312.pyc b/__pycache__/permissions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b2c27bad47235b1e099e34601599bdb3aa264e3 GIT binary patch literal 10776 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 L5h5Ma2@C%J7YOw8 literal 0 HcmV?d00001 diff --git a/actions.py b/actions.py new file mode 100644 index 0000000..d70f38e --- /dev/null +++ b/actions.py @@ -0,0 +1,302 @@ +import logging +import os +import time +import subprocess +from tkinter import E +from appium import webdriver +from appium.options.android import UiAutomator2Options +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +from page_objects.download_tabbar_page import DownloadTabbarPage +from page_objects.measure_tabbar_page import MeasureTabbarPage +from page_objects.section_mileage_config_page import SectionMileageConfigPage +from page_objects.upload_config_page import UploadConfigPage +from page_objects.more_download_page import MoreDownloadPage +from page_objects.screenshot_page import ScreenshotPage +import globals.driver_utils as driver_utils # 导入驱动工具模块 +import globals.global_variable as global_variable +from page_objects.login_page import LoginPage +import globals.apis as apis +import globals.create_link as create_link + + +class DeviceAutomation: + def __init__(self, device_id=None): + # 如果没有提供设备ID,则自动获取 + if device_id is None: + self.device_id = self.get_device_id() + else: + self.device_id = device_id + + # 初始化权限 + if driver_utils.grant_appium_permissions(self.device_id): + logging.info(f"设备 {self.device_id} 授予Appium权限成功") + else: + logging.warning(f"设备 {self.device_id} 授予Appium权限失败") + + # 确保Appium服务器正在运行,不在运行则启动 + if not driver_utils.check_server_status(4723): + driver_utils.start_appium_server() + + # 初始化Appium驱动和页面对象 + self.init_driver() + # 创建测试结果目录 + self.results_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_results') + + @staticmethod + def get_device_id() -> str: + """ + 获取设备ID,优先使用已连接设备,否则使用全局配置 + """ + try: + # 检查已连接设备 + result = subprocess.run( + ["adb", "devices"], + capture_output=True, + text=True, + timeout=10 + ) + + # # 解析设备列表 + # for line in result.stdout.strip().split('\n')[1:]: + # if line.strip() and "device" in line and "offline" not in line: + # device_id = line.split('\t')[0] + # logging.info(f"使用已连接设备: {device_id}") + # global_variable.GLOBAL_DEVICE_ID = device_id + # return device_id + + target_port = "4723" + for line in result.stdout.strip().split('\n')[1:]: + if line.strip() and "device" in line and "offline" not in line: + device_id = line.split('\t')[0] + + # 检查是否为无线设备且端口为4723 + if ':' in device_id: + ip_port = device_id.split(':') + if len(ip_port) == 2 and ip_port[1] == target_port: + logging.info(f"找到目标无线设备(端口{target_port}): {device_id}") + global_variable.GLOBAL_DEVICE_ID = device_id + return device_id + + # 如果没有找到端口4723的设备,找其他无线设备 + for line in result.stdout.strip().split('\n')[1:]: + if line.strip() and "device" in line and "offline" not in line: + device_id = line.split('\t')[0] + + # 检查是否为无线设备(任何端口) + if ':' in device_id and device_id.split(':')[-1].isdigit(): + logging.info(f"未找到端口{target_port}的设备,使用其他无线设备: {device_id}") + global_variable.GLOBAL_DEVICE_ID = device_id + return device_id + + # 如果没有任何无线设备,找有线设备 + for line in result.stdout.strip().split('\n')[1:]: + if line.strip() and "device" in line and "offline" not in line: + device_id = line.split('\t')[0] + logging.info(f"未找到无线设备,使用有线设备: {device_id}") + global_variable.GLOBAL_DEVICE_ID = device_id + return device_id + + logging.error("未找到任何可用设备") + return None + + except Exception as e: + logging.warning(f"设备检测失败: {e}") + + # 使用全局配置 + device_id = global_variable.GLOBAL_DEVICE_ID + logging.info(f"使用全局配置设备: {device_id}") + return device_id + + + def init_driver(self): + """初始化Appium驱动""" + try: + # 使用全局函数初始化驱动 + self.driver, self.wait = driver_utils.init_appium_driver(self.device_id) + # 初始化页面对象 + logging.info(f"设备 {self.device_id} 开始初始化页面对象") + self.login_page = LoginPage(self.driver, self.wait) + self.download_tabbar_page = DownloadTabbarPage(self.driver, self.wait, self.device_id) + self.measure_tabbar_page = MeasureTabbarPage(self.driver, self.wait,self.device_id) + self.section_mileage_config_page = SectionMileageConfigPage(self.driver, self.wait, self.device_id) + self.upload_config_page = UploadConfigPage(self.driver, self.wait, self.device_id) + self.more_download_page = MoreDownloadPage(self.driver, self.wait,self.device_id) + self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id) + logging.info(f"设备 {self.device_id} 所有页面对象初始化完成") + + # 检查应用是否成功启动 + if driver_utils.is_app_launched(self.driver): + logging.info(f"设备 {self.device_id} 沉降观测App已成功启动") + else: + logging.warning(f"设备 {self.device_id} 应用可能未正确启动(init_driver)") + driver_utils.launch_app_manually(self.driver) + + except Exception as e: + logging.error(f"设备 {self.device_id} 初始化驱动失败: {str(e)}") + raise + + def run_automation(self): + """根据当前应用状态处理相应的操作""" + try: + max_retry = 3 # 限制最大重试次数 + retry_count = 0 + while retry_count < max_retry: + login_btn_exists = self.login_page.is_login_page() + if not login_btn_exists: + logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面") + if self.login_page.navigate_to_login_page(self.driver, self.device_id): + logging.info(f"设备 {self.device_id} 成功跳转到登录页面") + else: + logging.error(f"设备 {self.device_id} 跳转到登录页面失败") + retry_count += 1 + continue + # 处理登录页面状态 + logging.info(f"设备 {self.device_id} 检测到登录页面,执行登录操作") + max_retries_login = 3 + login_success = False + + for attempt in range(max_retries_login + 1): + if self.login_page.login("wangshun"): + login_success = True + break + else: + if attempt < max_retries_login: + logging.warning(f"设备 {self.device_id} 登录失败,准备重试 ({attempt + 1}/{max_retries_login})") + time.sleep(2) # 等待2秒后重试 + else: + logging.error(f"设备 {self.device_id} 登录失败,已达到最大重试次数") + + if login_success: + break + elif retry_count == max_retry-1: + logging.error(f"设备 {self.device_id} 处理登录页面失败,已达到最大重试次数") + return False + else: + retry_count += 1 + + # login_btn_exists = self.login_page.is_login_page() + # if not login_btn_exists: + # logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面") + # if self.login_page.navigate_to_login_page(self.driver, self.device_id): + # logging.info(f"设备 {self.device_id} 成功跳转到登录页面") + # return self.run_automation() # 递归调用处理登录后的状态 + # else: + # logging.error(f"设备 {self.device_id} 跳转到登录页面失败") + # return False + + # # 处理登录页面状态 + # logging.info(f"设备 {self.device_id} 检测到登录页面,执行登录操作") + # max_retries = 1 + # login_success = False + + # for attempt in range(max_retries + 1): + # if self.login_page.login(): + # login_success = True + # break + # else: + # if attempt < max_retries: + # logging.warning(f"设备 {self.device_id} 登录失败,准备重试 ({attempt + 1}/{max_retries})") + # time.sleep(2) # 等待2秒后重试 + # else: + # logging.error(f"设备 {self.device_id} 登录失败,已达到最大重试次数") + + # if not login_success: + # return False + + logging.info(f"设备 {self.device_id} 登录成功,继续执行更新操作") + time.sleep(1) + + + # 执行更新操作 + if not self.download_tabbar_page.download_tabbar_page_manager(): + logging.error(f"设备 {self.device_id} 更新操作执行失败") + return False + + task_count = 0 + max_tasks = 1 # 最大任务数量,防止无限循环 + + while task_count < max_tasks: + # 获取测量任务 + logging.info(f"设备 {self.device_id} 获取测量任务 (第{task_count + 1}次)") + # task_data = apis.get_measurement_task() + # logging.info(f"设备 {self.device_id} 获取到的测量任务: {task_data}") + task_data = { + "id": 39, + "user_name": "czsczq115ykl", + "name": "czsczq115ykl", + "line_num": "L179451", + "line_name": "CDWZQ-2标-资阳沱江特大桥-23-35-山区", + "remaining": "0", + "status": 1 + } + if not task_data: + logging.info(f"设备 {self.device_id} 未获取到状态为1的测量任务,等待后重试") + time.sleep(1) # 等待1秒后重试 + break + # continue + + # 设置全局变量 + global_variable.GLOBAL_CURRENT_PROJECT_NAME = task_data.get('line_name', '') + global_variable.GLOBAL_LINE_NUM = task_data.get('line_num', '') + logging.info(f"设备 {self.device_id} 当前要处理的项目名称:{global_variable.GLOBAL_CURRENT_PROJECT_NAME}") + + # 执行测量操作 + # logging.info(f"设备 {self.device_id} 开始执行测量操作") + if not self.measure_tabbar_page.measure_tabbar_page_manager(): + logging.error(f"设备 {self.device_id} 测量操作执行失败") + + # # 返回到测量页面 + # self.driver.back() + # self.check_and_click_confirm_popup_appium() + + continue # 继续下一个任务 + + logging.info(f"设备 {self.device_id} 测量页面操作执行成功") + + # 在测量操作完成后执行断面里程配置 + logging.info(f"设备 {self.device_id} 开始执行断面里程配置") + if not self.section_mileage_config_page.section_mileage_config_page_manager(): + logging.error(f"设备 {self.device_id} 断面里程配置执行失败") + continue # 继续下一个任务 + + # 任务完成后短暂等待 + logging.info(f"设备 {self.device_id} 第{task_count}个任务完成") + task_count += 1 + + logging.info(f"设备 {self.device_id} 已完成{task_count}个任务,结束打数据流程") + if task_count == 0: + logging.error(f"没有完成打数据的线路,结束任务") + return False + + + # GLOBAL_TESTED_BREAKPOINT_LIST 把已打完的写入日志文件 + with open(os.path.join(self.results_dir, "打数据完成线路.txt"), "w", encoding='utf-8') as f: + for bp in global_variable.GLOBAL_TESTED_BREAKPOINT_LIST: + f.write(f"{bp}\n") + + return task_count > 0 + + except Exception as e: + logging.error(f"设备 {self.device_id} 处理应用状态时出错: {str(e)}") + return False + +# 主执行逻辑 +if __name__ == "__main__": + create_link.setup_adb_wireless() + automation = None + try: + automation = DeviceAutomation() + success = automation.run_automation() + + if success: + logging.info(f"设备 {automation.device_id} 自动化流程执行成功") + else: + logging.error(f"设备 {automation.device_id} 自动化流程执行失败") + except Exception as e: + logging.error(f"设备执行出错: {str(e)}") + finally: + if automation: + driver_utils.safe_quit_driver(automation.driver, automation.device_id) diff --git a/check_station.png b/check_station.png new file mode 100644 index 0000000000000000000000000000000000000000..e054821ab15c022ef8073101629b53d6b4ca7a72 GIT binary patch literal 159914 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 literal 0 HcmV?d00001 diff --git a/check_station.py b/check_station.py new file mode 100644 index 0000000..13ff4a0 --- /dev/null +++ b/check_station.py @@ -0,0 +1,211 @@ +import logging +import time +import requests +import pandas as pd +from io import BytesIO +import subprocess +import globals.global_variable as global_variable +import globals.driver_utils as driver_utils # 导入驱动工具模块 + + + +class CheckStation: + def __init__(self, driver=None, wait=None,device_id=None): + """初始化CheckStation对象""" + if device_id is None: + self.device_id = self.get_device_id() + else: + self.device_id = device_id + if driver is None or wait is None: + self.driver, self.wait = driver_utils.init_appium_driver(self.device_id) + else: + self.driver = driver + self.wait = wait + if driver_utils.grant_appium_permissions(self.device_id): + logging.info(f"设备 {self.device_id} 授予Appium权限成功") + else: + logging.warning(f"设备 {self.device_id} 授予Appium权限失败") + + # 确保Appium服务器正在运行,不在运行则启动 + if not driver_utils.check_server_status(4723): + driver_utils.start_appium_server() + + # 检查应用是否成功启动 + if driver_utils.is_app_launched(self.driver): + logging.info(f"设备 {self.device_id} 沉降观测App已成功启动") + else: + logging.warning(f"设备 {self.device_id} 应用可能未正确启动") + driver_utils.check_app_status(self.driver) + + @staticmethod + def get_device_id() -> str: + """ + 获取设备ID,优先使用已连接设备,否则使用全局配置 + """ + try: + # 检查已连接设备 + result = subprocess.run( + ["adb", "devices"], + capture_output=True, + text=True, + timeout=10 + ) + + # 解析设备列表 + for line in result.stdout.strip().split('\n')[1:]: + if line.strip() and "device" in line and "offline" not in line: + device_id = line.split('\t')[0] + logging.info(f"使用已连接设备: {device_id}") + global_variable.GLOBAL_DEVICE_ID = device_id + return device_id + + except Exception as e: + logging.warning(f"设备检测失败: {e}") + + # 使用全局配置 + device_id = global_variable.GLOBAL_DEVICE_ID + logging.info(f"使用全局配置设备: {device_id}") + return device_id + + def get_measure_data(self): + # 模拟获取测量数据 + pass + + def add_transition_point(self): + # 添加转点逻辑 + print("添加转点") + return True + + def get_excel_from_url(self, url): + """ + 从URL获取Excel文件并解析为字典 + Excel只有一列数据(A列),每行是站点值 + + Args: + url: Excel文件的URL地址 + + Returns: + dict: 解析后的站点数据字典 {行号: 值},失败返回None + """ + try: + print(f"正在从URL获取数据: {url}") + response = requests.get(url, timeout=30) + response.raise_for_status() # 检查请求是否成功 + + # 使用pandas读取Excel数据,指定没有表头,只读第一个sheet + excel_data = pd.read_excel( + BytesIO(response.content), + header=None, # 没有表头 + sheet_name=0, # 只读取第一个sheet + dtype=str # 全部作为字符串读取 + ) + + station_dict = {} + + # 解析Excel数据:使用行号+1作为站点编号,A列的值作为站点值 + print("解析Excel数据(使用行号作为站点编号)...") + for index, row in excel_data.iterrows(): + station_num = index + 1 # 行号从1开始作为站点编号 + station_value = str(row[0]).strip() if pd.notna(row[0]) else "" + + if station_value: # 只保存非空值 + station_dict[station_num] = station_value + + print(f"成功解析Excel,共{len(station_dict)}条数据") + return station_dict + + except requests.exceptions.RequestException as e: + print(f"请求URL失败: {e}") + return None + except Exception as e: + print(f"解析Excel失败: {e}") + return None + + def check_station_exists(self, station_data: dict, station_num: int) -> str: + """ + 根据站点编号检查该站点的值是否以Z开头 + + Args: + station_data: 站点数据字典 {编号: 值} + station_num: 要检查的站点编号 + + Returns: + str: 如果站点存在且以Z开头返回"add",否则返回"pass" + """ + if station_num not in station_data: + print(f"站点{station_num}不存在") + return "error" + + value = station_data[station_num] + str_value = str(value).strip() + is_z = str_value.upper().startswith('Z') + + result = "add" if is_z else "pass" + print(f"站点{station_num}: {value} -> {result}") + return result + + + def run(self): + last_station_num = 0 + + url = f"https://database.yuxindazhineng.com/team-bucket/69378c5b4f42d83d9504560d/前测点表/20260309/CDWZQ-2标-龙家沟左线大桥-0-11号墩-平原.xlsx" + station_data = self.get_excel_from_url(url) + print(station_data) + station_quantity = len(station_data) + over_station_num = 0 + over_station_list = [] + while over_station_num < station_quantity: + try: + # 键盘输出线路编号 + station_num_input = input("请输入线路编号:") + if not station_num_input.isdigit(): # 检查输入是否为数字 + print("输入错误:请输入一个整数") + continue + station_num = int(station_num_input) # 转为整数 + + if station_num in over_station_list: + print("已处理该站点,跳过") + continue + + if last_station_num == station_num: + print("输入与上次相同,跳过处理") + continue + last_station_num = station_num + + result = self.check_station_exists(station_data, station_num) + if result == "error": + print("处理错误:站点不存在") + # 错误处理逻辑,比如记录日志、发送警报等 + elif result == "add": + print("执行添加操作") + # 添加转点 + if not self.add_transition_point(): + print("添加转点失败") + # 可以决定是否继续循环 + continue + over_station_num += 1 + else: # result == "pass" + print("跳过处理") + over_station_num += 1 + + over_station_list.append(station_num) + + # 可以添加适当的延时,避免CPU占用过高 + # time.sleep(1) + + except KeyboardInterrupt: + print("程序被用户中断") + break + except Exception as e: + print(f"发生错误: {e}") + time.sleep(20) + # 错误处理,可以继续循环或退出 + print(f"已处理{over_station_num}个站点") + + # 截图 + self.driver.save_screenshot("check_station.png") + return True + +if __name__ == "__main__": + check_station = CheckStation() + check_station.run() \ No newline at end of file diff --git a/ck/.gitignore b/ck/.gitignore new file mode 100644 index 0000000..a8775af --- /dev/null +++ b/ck/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Virtual Environment +venv/ +env/ +ENV/ + +# Test +.pytest_cache/ +.coverage +htmlcov/ diff --git a/ck/README.md b/ck/README.md new file mode 100644 index 0000000..ade52a4 --- /dev/null +++ b/ck/README.md @@ -0,0 +1,292 @@ +# 串口通信协议 + +一个完整的Python串口通信协议实现,支持两台设备之间可靠的数据传输,可传输结构化数据(用户名、线路名称、站点编号等)。 + +## 特性 + +- ✅ **完整的协议帧结构** - 包含帧头、长度、命令、数据、CRC校验、帧尾 +- ✅ **CRC16校验** - 使用CRC-16/MODBUS算法确保数据完整性 +- ✅ **异步接收** - 多线程接收,不阻塞主程序 +- ✅ **多种命令类型** - 心跳、数据传输、控制命令、应答等 +- ✅ **结构化数据** - 支持JSON格式的站点数据传输(用户名、线路名称、站点编号) +- ✅ **易于使用** - 简洁的API,开箱即用 + +## 协议帧格式 + +``` ++------+------+--------+------+--------+------+ +| HEAD | LEN | CMD | DATA | CRC | TAIL | ++------+------+--------+------+--------+------+ +| 0xAA | 2B | 1B | N字节 | 2B | 0x55 | ++------+------+--------+------+--------+------+ +``` + +### 字段说明 + +- **HEAD**: 帧头标识 (1字节, 0xAA) +- **LEN**: 数据长度 (2字节, 大端序, 包含CMD+DATA) +- **CMD**: 命令字 (1字节) +- **DATA**: 数据内容 (N字节) +- **CRC**: CRC16校验 (2字节, 大端序) +- **TAIL**: 帧尾标识 (1字节, 0x55) + +## 安装 + +```bash +pip install -r requirements.txt +``` + +## 命令类型 + +| 命令 | 值 | 说明 | +| ----------- | ---- | -------------- | +| HEARTBEAT | 0x01 | 心跳包 | +| DATA_QUERY | 0x02 | 数据查询 | +| DATA_RESPONSE | 0x03 | 数据响应 | +| CONTROL | 0x04 | 控制命令 | +| ACK | 0x05 | 应答 | +| NACK | 0x06 | 否定应答 | + +## 站点数据模型 + +### StationData 类 + +用于传输站点信息的数据模型: + +```python +from data_models import StationData + +# 创建站点数据 +station = StationData( + username="张三", # 用户名 + line_name="1号线", # 线路名称 + station_no=5 # 第几站 +) + +# 序列化为字节(用于发送) +data_bytes = station.to_bytes() + +# 从字节反序列化(接收后解析) +restored = StationData.from_bytes(data_bytes) +``` + +## 使用方法 + +### 基本示例 + +```python +from serial_protocol import SerialProtocol, Command + +# 创建协议实例 +device = SerialProtocol(port='COM1', baudrate=115200) + +# 打开串口 +if device.open(): + # 发送数据 + device.send_data(b"Hello, World!") + + # 发送心跳 + device.send_heartbeat() + + # 发送控制命令 + device.send_control(0x10, b"\x01\x02") + + # 关闭串口 + device.close() +``` + +### 异步接收数据 + +```python +def on_receive(cmd, data): + print(f"收到命令: 0x{cmd:02X}, 数据: {data.hex()}") + +device = SerialProtocol(port='COM1', baudrate=115200) +device.open() + +# 启动接收线程 +device.start_receive(on_receive) + +# ... 主程序运行 ... + +device.close() +``` + +## 完整示例 + +### 设备A (发送端) + +运行 `device_a.py`,设备A会定期发送: +- 心跳包 +- **站点数据**(包含用户名、线路名称、站点编号) +- **接收设备B的字典响应** + +输出示例: +``` +============================================================ +循环 1 +============================================================ +✓ 发送心跳包 + +准备发送站点数据: + 用户: 李四, 线路: 2号线, 站点: 第1站 +✓ 站点数据发送成功 + +[设备A] 收到数据: + 命令: 0x03 (DATA_RESPONSE) + + 📥 收到设备B的响应字典: + 1: "李四" + 2: "2号线" + 3: "1" + 4: "已接收" + 5: "设备B确认" +``` + +运行命令: +```bash +python device_a.py +``` + +### 设备B (接收端) + +运行 `device_b.py`,设备B会: +- 接收站点数据并解析显示 +- 自动应答心跳包 +- **返回统一格式的响应字典** `{1:"xxx", 2:"xxx", 3:"xxx", ...}` + +输出示例: +``` +============================================================ +[设备B] 收到数据 +============================================================ +命令类型: 0x03 (DATA_RESPONSE) + +📍 站点数据详情: + 用户名称: 李四 + 线路名称: 2号线 + 站点编号: 第1站 + +📤 设备B统一响应格式: + 1: "李四" + 2: "2号线" + 3: "1" + 4: "已接收" + 5: "设备B确认" + +<<< 已发送响应字典 +``` + +运行命令: +```bash +python device_b.py +``` + +## 设备B统一响应格式 + +设备B接收到站点数据后,会返回统一格式的字典响应: + +```python +{ + 1: "用户名", + 2: "线路名称", + 3: "站点编号", + 4: "已接收", + 5: "设备B确认" +} +``` + +**字段说明:** +- `1`: 接收到的用户名 +- `2`: 接收到的线路名称 +- `3`: 接收到的站点编号(字符串) +- `4`: 固定值 "已接收" +- `5`: 固定值 "设备B确认" + +## 硬件连接 + +### 方案1: 使用两个USB转串口模块 + +``` +设备A (COM1) <---RX/TX交叉---> 设备B (COM2) + TX ----------------------> RX + RX <---------------------- TX + GND <---------------------> GND +``` + +### 方案2: 使用虚拟串口 (测试用) + +**Windows**: 使用 com0com 或 Virtual Serial Port Driver +**Linux**: 使用 socat + +```bash +# Linux创建虚拟串口对 +socat -d -d pty,raw,echo=0 pty,raw,echo=0 +# 会创建 /dev/pts/X 和 /dev/pts/Y +``` + +## API文档 + +### SerialProtocol 类 + +#### 初始化 + +```python +SerialProtocol(port: str, baudrate: int = 115200, timeout: float = 1.0) +``` + +#### 方法 + +- `open() -> bool` - 打开串口 +- `close()` - 关闭串口 +- `send_frame(cmd: int, data: bytes) -> bool` - 发送数据帧 +- `receive_frame() -> Optional[dict]` - 接收数据帧(阻塞) +- `start_receive(callback)` - 启动异步接收 +- `stop_receive()` - 停止异步接收 +- `send_heartbeat() -> bool` - 发送心跳包 +- `send_data(data: bytes) -> bool` - 发送数据 +- `send_control(code: int, params: bytes) -> bool` - 发送控制命令 +- `send_ack() -> bool` - 发送应答 +- `send_nack() -> bool` - 发送否定应答 + +#### 静态方法 + +- `calc_crc16(data: bytes) -> int` - 计算CRC16校验值 +- `build_frame(cmd: int, data: bytes) -> bytes` - 构建数据帧 +- `parse_frame(frame: bytes) -> Optional[dict]` - 解析数据帧 + +## 常见问题 + +### 1. 如何修改串口号? + +编辑 `device_a.py` 和 `device_b.py`,修改 `port` 参数: +- Windows: `'COM1'`, `'COM2'`, etc. +- Linux: `'/dev/ttyUSB0'`, `'/dev/ttyS0'`, etc. + +### 2. 如何修改波特率? + +修改 `baudrate` 参数,常用值:9600, 19200, 38400, 57600, 115200 + +### 3. 数据长度限制? + +理论最大65535字节,但建议单帧数据不超过1024字节以提高可靠性。 + +### 4. 如何处理超时? + +设置 `timeout` 参数控制读取超时时间。 + +## 应用场景 + +- 🤖 机器人通信 +- 📡 传感器数据采集 +- 🎮 设备控制 +- 📊 工业自动化 +- 🔌 嵌入式系统互联 + +## 许可证 + +MIT License + +## 作者 + +Created with ❤️ for reliable serial communication diff --git a/ck/data_models.py b/ck/data_models.py new file mode 100644 index 0000000..8dd72fd --- /dev/null +++ b/ck/data_models.py @@ -0,0 +1,82 @@ +""" +数据模型定义 +定义设备间传输的数据结构 +""" + +import json +from typing import Optional, Dict, Any +from dataclasses import dataclass, asdict + + +@dataclass +class StationData: + """站点数据模型""" + username: str # 用户名 + line_name: str # 线路名称 + station_no: int # 第几站 + + def to_bytes(self) -> bytes: + """转换为字节数据""" + data_dict = asdict(self) + json_str = json.dumps(data_dict, ensure_ascii=False) + return json_str.encode('utf-8') + + @staticmethod + def from_bytes(data: bytes) -> Optional['StationData']: + """从字节数据解析""" + try: + json_str = data.decode('utf-8') + data_dict = json.loads(json_str) + return StationData( + username=data_dict['username'], + line_name=data_dict['line_name'], + station_no=data_dict['station_no'] + ) + except Exception as e: + print(f"解析数据失败: {e}") + return None + + def __str__(self): + """字符串表示""" + return (f"用户: {self.username}, " + f"线路: {self.line_name}, " + f"站点: 第{self.station_no}站") + + +class ResponseData: + """设备B统一响应数据格式 {1:"xxxx", 2:"xxxx", 3:"xxxx", ...}""" + + @staticmethod + def create_response(station_data: StationData) -> Dict[int, str]: + """ + 根据站点数据创建响应字典 + + Args: + station_data: 接收到的站点数据 + + Returns: + 格式化的响应字典 {1: "用户名", 2: "线路名", 3: "站点号", ...} + """ + return { + 1: station_data.username, + 2: station_data.line_name, + 3: str(station_data.station_no), + 4: "已接收", + 5: "设备B确认" + } + + @staticmethod + def to_bytes(response_dict: Dict[int, str]) -> bytes: + """将响应字典转换为字节数据""" + json_str = json.dumps(response_dict, ensure_ascii=False) + return json_str.encode('utf-8') + + @staticmethod + def from_bytes(data: bytes) -> Optional[Dict[int, Any]]: + """从字节数据解析响应字典""" + try: + json_str = data.decode('utf-8') + return json.loads(json_str) + except Exception as e: + print(f"解析响应数据失败: {e}") + return None diff --git a/ck/device_a.py b/ck/device_a.py new file mode 100644 index 0000000..6e25f06 --- /dev/null +++ b/ck/device_a.py @@ -0,0 +1,103 @@ +""" +设备A示例 - 发送端 +模拟第一台设备,定期发送站点数据(用户名、线路名称、站点编号) +""" + +from serial_protocol import SerialProtocol, Command +from data_models import StationData, ResponseData +import time + + +def on_receive(cmd: int, data: bytes): + """接收数据回调函数""" + print("\n[设备A] 收到数据:") + cmd_name = (Command(cmd).name if cmd in Command._value2member_map_ + else 'UNKNOWN') + print(f" 命令: 0x{cmd:02X} ({cmd_name})") + + if cmd == Command.ACK: + print(" >>> 对方已确认接收") + elif cmd == Command.DATA_RESPONSE: + # 优先尝试解析字典响应 + response_dict = ResponseData.from_bytes(data) + if response_dict and isinstance(response_dict, dict): + print("\n 📥 收到设备B的响应字典:") + for key, value in response_dict.items(): + print(f" {key}: \"{value}\"") + else: + # 尝试解析站点数据 + station_data = StationData.from_bytes(data) + if station_data: + print(f" >>> 对方发来站点数据: {station_data}") + else: + print(f" >>> 对方发来数据: " + f"{data.decode('utf-8', errors='ignore')}") + + +def main(): + # 创建串口协议实例 + # Windows: 'COM1', 'COM2', etc. + # Linux: '/dev/ttyUSB0', '/dev/ttyS0', etc. + device = SerialProtocol(port='COM1', baudrate=115200) + + print("=" * 60) + print("设备A - 发送端") + print("=" * 60) + + # 打开串口 + if not device.open(): + print("❌ 无法打开串口") + return + + print(f"✓ 串口已打开: {device.port} @ {device.baudrate}") + + # 启动接收线程 + device.start_receive(on_receive) + print("✓ 接收线程已启动") + + try: + # 模拟线路数据 + lines = ["1号线", "2号线", "3号线", "环线"] + users = ["张三", "李四", "王五", "赵六"] + + # 模拟设备运行 + counter = 0 + while True: + counter += 1 + print(f"\n{'='*60}") + print(f"循环 {counter}") + print('='*60) + + # 1. 发送心跳包 + if device.send_heartbeat(): + print("✓ 发送心跳包") + else: + print("✗ 发送心跳包失败") + + time.sleep(2) + + # 2. 发送站点数据 + station_data = StationData( + username=users[counter % len(users)], + line_name=lines[counter % len(lines)], + station_no=counter + ) + print("\n准备发送站点数据:") + print(f" {station_data}") + + if device.send_data(station_data.to_bytes()): + print("✓ 站点数据发送成功") + else: + print("✗ 站点数据发送失败") + + time.sleep(5) + + except KeyboardInterrupt: + print("\n\n用户中断") + finally: + device.close() + print("串口已关闭") + + +if __name__ == '__main__': + main() diff --git a/ck/device_b.py b/ck/device_b.py new file mode 100644 index 0000000..a761684 --- /dev/null +++ b/ck/device_b.py @@ -0,0 +1,117 @@ +""" +设备B示例 - 接收端 +模拟第二台设备,接收站点数据(用户名、线路名称、站点编号)并应答 +""" + +from serial_protocol import SerialProtocol, Command +from data_models import StationData, ResponseData +import time +import subprocess + + +def on_receive(cmd: int, data: bytes): + """接收数据回调函数""" + print("\n" + "="*60) + print("[设备B] 收到数据") + print("="*60) + cmd_name = (Command(cmd).name if cmd in Command._value2member_map_ + else 'UNKNOWN') + print(f"命令类型: 0x{cmd:02X} ({cmd_name})") + + # 根据不同命令进行处理 + if cmd == Command.HEARTBEAT: + print(">>> 收到心跳包") + # 可以回复ACK + device.send_ack() + print("<<< 已发送ACK应答\n") + + elif cmd == Command.DATA_RESPONSE: + # 尝试解析站点数据 + station_data = StationData.from_bytes(data) + if station_data: + print("\n📍 站点数据详情:") + print(f" 用户名称: {station_data.username}") + print(f" 线路名称: {station_data.line_name}") + print(f" 站点编号: 第{station_data.station_no}站") + + # 根据站点编号执行不同逻辑 + if station_data.station_no == 0: + print("\n🚀 站点编号为0,启动 actions.py") + # 启动 actions.py + + subprocess.Popen(["python", "actions.py"], cwd="d:\\Projects\\cjgc_data") + + if station_data.station_no > 0: + print(f"\n🚀 站点编号为{station_data.station_no},启动 check_station.py") + # 启动 check_station.py + + subprocess.Popen(["python", "check_station.py"], cwd="d:\\Projects\\cjgc_data") + + # 创建统一格式的响应字典 {1:"xxx", 2:"xxx", 3:"xxx", ...} + response_dict = ResponseData.create_response(station_data) + print("\n📤 设备B统一响应格式:") + for key, value in response_dict.items(): + print(f" {key}: \"{value}\"") + + # 发送响应 + device.send_data(ResponseData.to_bytes(response_dict)) + print("\n<<< 已发送响应字典\n") + else: + # 如果不是站点数据,按普通消息处理 + message = data.decode('utf-8', errors='ignore') + print(f">>> 收到普通消息: {message}") + device.send_ack() + print("<<< 已发送ACK\n") + + elif cmd == Command.CONTROL: + if len(data) >= 1: + control_code = data[0] + params = data[1:] + print(f">>> 收到控制命令: 0x{control_code:02X}, " + f"参数: {params.hex()}") + # 执行控制逻辑... + device.send_ack() + print("<<< 已发送ACK\n") + + +# 全局变量,用于在回调中访问 +device = None + + +def main(): + global device + + # 创建串口协议实例 + # 注意: 设备B应该使用另一个串口,或者通过虚拟串口对连接 + # Windows: 'COM2', Linux: '/dev/ttyUSB1' + device = SerialProtocol(port='COM2', baudrate=115200) + + print("=" * 60) + print("设备B - 接收端") + print("=" * 60) + + # 打开串口 + if not device.open(): + print("❌ 无法打开串口") + return + + print(f"✓ 串口已打开: {device.port} @ {device.baudrate}") + + # 启动接收线程 + device.start_receive(on_receive) + print("✓ 接收线程已启动,等待接收数据...") + + try: + # 保持运行 + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n\n用户中断") + finally: + device.close() + print("串口已关闭") + + +if __name__ == '__main__': + main() diff --git a/ck/requirements.txt b/ck/requirements.txt new file mode 100644 index 0000000..7ad05ef --- /dev/null +++ b/ck/requirements.txt @@ -0,0 +1 @@ +pyserial>=3.5 diff --git a/ck/serial_protocol.py b/ck/serial_protocol.py new file mode 100644 index 0000000..4af333c --- /dev/null +++ b/ck/serial_protocol.py @@ -0,0 +1,298 @@ +""" +串口通信协议实现 +支持两台设备之间可靠的数据传输 + +协议帧格式: ++------+------+--------+------+--------+------+ +| HEAD | LEN | CMD | DATA | CRC | TAIL | ++------+------+--------+------+--------+------+ +| 0xAA | 2B | 1B | N字节 | 2B | 0x55 | ++------+------+--------+------+--------+------+ + +HEAD: 帧头标识 (1字节, 0xAA) +LEN: 数据长度 (2字节, 大端序, 包含CMD+DATA) +CMD: 命令字 (1字节) +DATA: 数据内容 (N字节) +CRC: CRC16校验 (2字节, 大端序) +TAIL: 帧尾标识 (1字节, 0x55) +""" + +import serial +import struct +from enum import IntEnum +from typing import Optional, Callable +import threading + + +class Command(IntEnum): + """命令类型定义""" + HEARTBEAT = 0x01 # 心跳包 + DATA_QUERY = 0x02 # 数据查询 + DATA_RESPONSE = 0x03 # 数据响应 + CONTROL = 0x04 # 控制命令 + ACK = 0x05 # 应答 + NACK = 0x06 # 否定应答 + + +class SerialProtocol: + """串口协议类""" + + # 协议常量 + FRAME_HEAD = 0xAA + FRAME_TAIL = 0x55 + # 最小帧长度: HEAD(1) + LEN(2) + CMD(1) + CRC(2) + TAIL(1) + MIN_FRAME_LEN = 7 + + def __init__(self, port: str, baudrate: int = 115200, + timeout: float = 1.0): + """ + 初始化串口协议 + + Args: + port: 串口名称 (如 'COM1', '/dev/ttyUSB0') + baudrate: 波特率 + timeout: 超时时间(秒) + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.serial = None + self.running = False + self.receive_thread = None + self.receive_callback: Optional[Callable] = None + + def open(self) -> bool: + """打开串口""" + try: + self.serial = serial.Serial( + port=self.port, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=self.timeout + ) + return True + except Exception as e: + print(f"打开串口失败: {e}") + return False + + def close(self): + """关闭串口""" + self.stop_receive() + if self.serial and self.serial.is_open: + self.serial.close() + + @staticmethod + def calc_crc16(data: bytes) -> int: + """ + 计算CRC16校验值 (CRC-16/MODBUS) + + Args: + data: 需要计算校验的数据 + + Returns: + CRC16校验值 + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + return crc + + def build_frame(self, cmd: int, data: bytes = b'') -> bytes: + """ + 构建数据帧 + + Args: + cmd: 命令字 + data: 数据内容 + + Returns: + 完整的数据帧 + """ + # 计算长度 (CMD + DATA) + length = 1 + len(data) + + # 构建帧体 (不含CRC和TAIL) + frame_body = (struct.pack('>BHB', self.FRAME_HEAD, length, cmd) + + data) + + # 计算CRC (对HEAD+LEN+CMD+DATA进行校验) + crc = self.calc_crc16(frame_body) + + # 添加CRC和TAIL + frame = frame_body + struct.pack('>HB', crc, self.FRAME_TAIL) + + return frame + + def parse_frame(self, frame: bytes) -> Optional[dict]: + """ + 解析数据帧 + + Args: + frame: 接收到的数据帧 + + Returns: + 解析结果字典 {'cmd': 命令字, 'data': 数据}, 失败返回None + """ + if len(frame) < self.MIN_FRAME_LEN: + return None + + # 检查帧头和帧尾 + if frame[0] != self.FRAME_HEAD or frame[-1] != self.FRAME_TAIL: + return None + + try: + # 解析长度 + length = struct.unpack('>H', frame[1:3])[0] + + # 检查帧长度是否匹配 + # HEAD + LEN + (CMD+DATA) + CRC + TAIL + expected_len = 1 + 2 + length + 2 + 1 + if len(frame) != expected_len: + return None + + # 解析命令字 + cmd = frame[3] + + # 提取数据 + data = frame[4:4+length-1] + + # 提取CRC + received_crc = struct.unpack('>H', frame[-3:-1])[0] + + # 计算CRC并校验 + calc_crc = self.calc_crc16(frame[:-3]) + if received_crc != calc_crc: + print(f"CRC校验失败: 接收={received_crc:04X}, " + f"计算={calc_crc:04X}") + return None + + return {'cmd': cmd, 'data': data} + + except Exception as e: + print(f"解析帧失败: {e}") + return None + + def send_frame(self, cmd: int, data: bytes = b'') -> bool: + """ + 发送数据帧 + + Args: + cmd: 命令字 + data: 数据内容 + + Returns: + 发送是否成功 + """ + if not self.serial or not self.serial.is_open: + print("串口未打开") + return False + + try: + frame = self.build_frame(cmd, data) + self.serial.write(frame) + return True + except Exception as e: + print(f"发送失败: {e}") + return False + + def receive_frame(self) -> Optional[dict]: + """ + 接收一帧数据 (阻塞式) + + Returns: + 解析结果字典或None + """ + if not self.serial or not self.serial.is_open: + return None + + try: + # 等待帧头 + while True: + byte = self.serial.read(1) + if not byte: + return None + if byte[0] == self.FRAME_HEAD: + break + + # 读取长度字段 + len_bytes = self.serial.read(2) + if len(len_bytes) != 2: + return None + + length = struct.unpack('>H', len_bytes)[0] + + # 读取剩余数据: CMD + DATA + CRC + TAIL + remaining = self.serial.read(length + 3) + if len(remaining) != length + 3: + return None + + # 重组完整帧 + frame = bytes([self.FRAME_HEAD]) + len_bytes + remaining + + # 解析帧 + return self.parse_frame(frame) + + except Exception as e: + print(f"接收失败: {e}") + return None + + def start_receive(self, callback: Callable[[int, bytes], None]): + """ + 启动接收线程 + + Args: + callback: 回调函数 callback(cmd, data) + """ + if self.running: + return + + self.receive_callback = callback + self.running = True + self.receive_thread = threading.Thread( + target=self._receive_loop, daemon=True) + self.receive_thread.start() + + def stop_receive(self): + """停止接收线程""" + self.running = False + if self.receive_thread: + self.receive_thread.join(timeout=2.0) + + def _receive_loop(self): + """接收循环""" + while self.running: + result = self.receive_frame() + if result and self.receive_callback: + try: + self.receive_callback(result['cmd'], result['data']) + except Exception as e: + print(f"回调函数执行失败: {e}") + + def send_heartbeat(self) -> bool: + """发送心跳包""" + return self.send_frame(Command.HEARTBEAT) + + def send_data(self, data: bytes) -> bool: + """发送数据""" + return self.send_frame(Command.DATA_RESPONSE, data) + + def send_control(self, control_code: int, + params: bytes = b'') -> bool: + """发送控制命令""" + data = bytes([control_code]) + params + return self.send_frame(Command.CONTROL, data) + + def send_ack(self) -> bool: + """发送应答""" + return self.send_frame(Command.ACK) + + def send_nack(self) -> bool: + """发送否定应答""" + return self.send_frame(Command.NACK) diff --git a/globals/__pycache__/apis.cpython-312.pyc b/globals/__pycache__/apis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b2fb5bcc7977e01fe63f5dee6aae6697626d212 GIT binary patch literal 20668 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 iO4$~P#N1msM&a38-BltZ0 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 E0jpn;vH$=8 literal 0 HcmV?d00001 diff --git a/globals/__pycache__/driver_utils.cpython-312.pyc b/globals/__pycache__/driver_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bbd2559082a34e7ebe309cf6b093a01f6f440d7 GIT binary patch literal 37967 zcmeIbdt6lKxiGrc+?Zizm;nYD?!z6tfcFbtBB&^MSJa3af~*liZdo&q(}Y;$r;N=*vO zXc#xsv`lJ%a);Oz%A54WnJz}ZuAy&GP8MhWI$M*@Wpfs|XK*>374Df_%-5M37iWVo zi_7Kg5SPux!dniP$HlpGxp;RT=Wyp8HE;=E*Vp7jnnXxb;5IbvlxkA$O}mw6>8WY2 z^rYNh7aswxd@c#fn+tF`;XV)GOosb>u7FE{dojGH!o7qm%=OFX>>5=G?-vw->P?(@FOcj&D5S=HbOy*52OeYbo!5XDfZ)#-6V4;aeLbyU;VL-&F&U=t13RZvh_q=b0fFM?eRb*m7J0#POeOj>nzMG ztn<`+q4Xk;YrZS5u-SdW-P~G-@1^kC0>#udz)O)Pq$j_H3`g5|Xh*$P>1@UV&-i}3 z$$(Sc3lpARP2%*B-to?{e&)L{sbA8*E~aA{uU<+a4P2L&)B3fwaxH!a-gHtbKMQYi z9=`_Opk{h;I(U;7AH(V4%_#qdZzlQ80B>XgV+^UmEdA!!HYH2X%AJeijLMXsA&rSs zEejLVz;Nd8vt2sQ(xrz#gI~9SIlPNux}r?8>nW>0>buNKI#q9_N)R=rNl(h%ALZ9x zjFF)1($8u%Y}4ytnO0WE`1PJ@RsLM17U@YG&G@j~K%I!But%4S6q-U59 znNkX|{Dc|u$i4Ce>n)J-`1M?(+MNN3{o-lk7Ao zin}bcf*@a2w?(Quyk|(Qmd7opzX^51ea1R})MZrF8EI9}U9nJAr`+D~I`30Xk$0)o z<9@k>NQXLFLL6bGkBjE{b^zGXF1RLoUYdCAnTgZirbq9s3vW(5-GAdd zU!Qogf8x#ViKpJ1d|`kjq^Zw50q>>cnRDYIUE9c&x^BMl%ZXQ?s@y>0>)P9E+w1C& z!}(Y0nt12=8_)MooPKhm_u2n^_w=pHeK+5Db$sxh?E1Er;v-E>CyVQw8tSvJx8#Z~T|f%yY>Rh*QwWBB*fUM^8+C@4Vbi6R*B`^XEUD=y`GS>F-WF_0Gh*PfQFB zlG^@?XD96mmk-Yu*TlQs*td~-I22XZ%JFTDT*#tEajF};+ZiU5>1(W$(H_Kx-2B!$ zUu*p_5udsB@P9z6IKXvU0qX&Lr4~^UKy+?h39`%uZD-msueZIlq-5cu<;4r&-@?)* z%NH#!ImwkA^Bw6-i1@3VskyDbuK8G-$E(h?{jazoxh8+|gNgI~H{bu>w9>%cZWr#$bUoj`phSuBOy=i-YLD!tM=~ zo$8)Ee`;DG6XypeUh1A0c*L+EFl3ln=XF2o4MmBA z$YXuJYy@lK?VnN@U|!vL?~RE!&rJN{g~@@z@xf=u-#vTde85Ke{q3a+anYXl~=vE-l-ibF(11OzZ7o@_Gqg{RD&C3(tdqx~g8Z{C4#$=$U z)D<#_IKC?s)!fzqOSB=RYitEH7*WoUwY|B{i#TyDqRk<*xSi0KI%N+DS@1<#>mi%C zFmTe7w8qx9THXzV5lY0LY8&{vR&Q;)n{R0(sJtf>-|9XIbU{l?T`MP#iO$Et;){WN zwJ;cUM;e=9*WnRu4jFd4TiU$tkV(e(Ls2K|_*QIy*5ltCFKcwY-t)Xb%4ddKe-^RINhIl#12st|1-63Nw0l&63Wb*QL_3k6E zV}_zBjZo`;v_51Y%g__jJ>r86%NjDt`z?<&C6DI{pMoHmX+PKc_+}5k7-M3Z>pWg8 zMWlM7l@uY?9n#?*D9==J*45UxKz~Rt(4)R)Z^$aWlNNidP&ghM@^yFqhSEbj`L-t5 zhCGLGE3f71ymf~fn%j=lHG2+8^?DndJ;m)Ep}5A@MsF<)C{TR0^hf?66dDhIo*mHp z-OL>)qqjV@^{ZQZm-nsgn=9m%39;ot-TFHQ#+E%ClheIvG%c@tQ&5*YngTCDUD9X@ zc~2TmEx>o@XvRFer;aA2;@vTtSA_TM(QNX2=4gCc_olCJoiad0Bf69!U5b#pde9xz zZMd#8OA#dli9y|pDf>oNAM=?-mlWM&p0Y4;i9LqV+jn5pp468T*xSEZSXOnlYD%xo zju9*wcZ^I%;mZ%b_>eGf?NzT(^N?_?DVTn|XZxrveZ*EcWGn11yJnj|tbX)2J<>*W2S4 zjH#dB*~BuA^v@WrHF-2EyEg`_k_#Q!ex>xvysJloyQ+n~`-XSz7alw$95^KGIxOsb zNO08#?GJxycYZMy1-ZYtvzoCdf5x!TYV776r*=p^S{>Z`pm5;m@ZJWY`k0W`IAUoU zvNU}$rNy*g+=*c<37;|1`WWn0>nUqrRKV2tSbxsN`hl$1kM=((BrXdYmrFm!yz1>Q z`@zYPg5^U6%Z2^E4mjviO{**;=IksK9{F^YxIZ zUQGlcYXNuL+gBlcN`H>=v)y(tM9?b|1Eh=fv%G=R_*o!uv@dB}Sx(ob@iU%eg$;s? zlDME3F)Khq?$Hk6XW>Oi*5w2RbX46g%9!h>?!)umNI zJ5xy}zZOUW6{Dvk8i+BwR1}bescPm=Q=IgRawia()es$EIt*S!dB_E2oaoYdlcaaQ z%o2#iz>&y{r}bW$iPED#$^y^Q$XPcq((kW}!=mKki>+Y@|L5A1$Q?a_xxDkqhcM z7P;T=>DxHy+xUO7ZyopPm)Z1f!hOn+`&MEY6earJhei@|Xz~Z$lP~{p;=ON8oB~|+ zJ2x-AG}(7)qW4FD(NDh6Lz0kSLQPTObE- zr4*>9`i``Pf})*J|G9M%u3TM?lHZs0zSW+@zH4JTA1mtSiL$aAApvFd-9x zbiHxmoeIG8D5Dt2Umx<^M7sIicP0kT(iQ>@txA&*X+$tczufBn&dv9qLJ(*N4L&_S z_{`+DpDJ}#`{v4N0sRLQ9=gj_I3s_$s9s!hkc)1qhyvcm3SVHiFf8zu{mfz5ZgKlO zr8&WTn?1*}%;{(y)4}{idy>7tZohy;iKci#$k2X_uk!%vOYptg7Egl*H))rvo3DZU zUDN6YcPsGP`L&(-^~c=x$7`iF67G5NYKpY3Etb>$0J3=yP5t~P^LaP(8Dp@S11EY; z^d$#v*?+;ezu(ru&tHp)WsCHkbH(ypjUJb~(R<9zyS#|Xx7Go3-_^#uTHCxXkGGD8 z*UowMZN6rXL;=?QXopKg@?G9G7i4wu1dn%l2m#$0<7ulu?)JKl)wMRbeC?eo@)=Sd z^?9H~M@!wK@Z#kg-B3T^&u-4m`EbvqgGXseiOS@WB=*gNKqen4KVba5Z@|R7`L*|e zPJ>xb^U{$7>IsXx@!r`Rm(Pb67dDXCCuAVY;XyT!iPAtesfQiX;3e(C@*q2KC|Y4&grdafP?R_hp(vnbnj4QSCVfSF>Be^lC!YKn?O*7` z9H^ddG2*f!MC^^{1}2_-Yy9%JrF2DcAsZz|YERVhjde$w-68v?tvkxgw$^UgxUX{k z#@fma{1PaP$Bqs~9jo)ydA&Sei*I_)eZhf`TtZ5nH)Hw@7~rwQug9PX z0$>ufc7$x}WxmWtBosr5O2lqD(P+Fw9S@W!iRH_npdteyjTo0_hD5i zx8sjO;6ZGM5M%4e`V7TKSd{!@kmxIzIs{V&*5H(rF~$l}&M`;Mh-1l+V@Z$cletSM zxfR$o6k9Nu-CgzX=2%2WZKrH~rG4`dMRg*II%;!F>E`I2@!6Mg=mZQek|$dZlM zmTZ)$lA6A)Lg6MMWpmI{IU4W0p5huwSum8cpvNYmw<&!VA-C%4147k7A@NYqczDzn z|M{3X_cKPLPY6r2LrB~iH17Ju<`^>rdT-MM%t?Lhw<^wUJ-hW=+j~qvtXOQfaTl3l z42z=g;1dKEGvMP^2cew^JgWx}T=8Dj4%P~*w+SiRgO(j*@y>CPx*{onHh4&=trs4y z7xr>OwOdFz8niTw#V36+W=RdEFAgrP6wFokg1F+{_`zM569ZDs zB_P4tV;VQd+{WcF#b`3BC+;(#R#%*k$5u zep9VHV?fkxs+DOi5TBcB}Y@FqjW+2JigDvyhWH@OeUn@o3caqyc{N=a%^`qk95 zRQg4^bMc%*n&m7&6fsQ#7kl3{iL*$PG>bG&F8RK7rEsZA8W8QLfoMOPOYbtnpT!?7 ziuSFuoHZGKE6ikh)xvC9F7;0&jT^t!Z@QQ%)!$`S%_DhKr2Me9={#~xuR|2=n?bZM zk9yN;C7<*p<_{Aq-zP_&7n#ZA@~cIrYC+>FDTP>m{tS8KUIEem15zHpnJbXnZ&N`t zbNdU`>j=b-=B9O0Y0ACJKC7~d{Pyti^4tBI<1l7?!L0JnQN!Xze2ZLuGpew>j%O2G zFh0q%Ds669X&Y&7*bU5nM(QIzb5r16FfXlGFE5;uKB70@7&J0Nw&UG$ZHKSsr zoe|VCav~@Vq<6(U9s^1PW3YbVNf9njgQB=Ac2*F~Q`H?S)g3-(fUA<;75jMX=@7lo zSm(=KF{(OeUZLuqAexdrMH(i}%s0n(>9lG@XsP9Mpl zrM^5sx1y22xb@Vtliwbolvo)N-%ySRa4>+zrN2%PSv{5blT0KC1{oLE|6XiEa32;Q-dmoi9s0PQGzFSY7nNIPRx(JWU zhe}qWnu2`Y#Kg$m}?h4~6G z9i23?vcTl|AK!ex_vS07vCUMLPi7B~xE7BnR&}EtxaXm06zWpt2jsdPZ*1S&3QVTs zp+tP9x)GJFz!7Q&aWV*nLr#p}Kqgux2pbzgtFx2*!sABB;~vN(tURt!9?`JvJfdGb zVqH8UoqQSwxF`vOhe!S%@4^5PL>@W0d=3OY%;usDJlYu4n;{(qql8MJZCC$9n1CwrKT)`Vc|+@gSpijrrAN?QH_3dU5Q&w(|H|Z7FUfiU*acy0 znQ9mK@-QA|`$W%oCoaDOWlKyIX<80^1BMhdE_~QwFwE)!mP(}+`| zwcbs5Ga+rWyA@>gz}9Q$J(#QulW7}((YFL&k@dqPrXA9Q_CSL>l;&>rpne8yBq*t- z9K>3X=%W$|5OR)arAjSvw7U6`862eF2Ey3M$`=Wb@+s9nR1e_ z^At{#xVMGkn~B~J0#Xb53qF|U9fYA1vU}=|x@%E6K-@Zu;zf3mvU?~1h3v2&Lhf35 z;|V$BEviEmn+P0G(5C=S7pfN30x!(n((d(yY)TyGZm#PHMTtKV9Rz+iwqr2{doZZR zU@r#yFn9(6(BtrW>*|ly0{anoVkXo##@C3ZcXea%@o6I(QMUxhHqOn|87n=Ul5qn-x^F=GnzAhB&T#Jr*tG|?NH9z z!4tzd6@ta}i7jU|XSrZ^iDG9V`>4=xQaJjk;OYq4I{^^8>(wfuI)f}f-Yq=F{yjUc)U|cE*Xwr zaGPoZ-Z|J)1^z=l5(1TCzs7;U#Fv9L*;0 znWH%+`0g6bUxN3X(aapYr;W~Ei1#_8bBgg^Fq)d%v*|QyY*-y5mh2%*c3|~DLeR2& z)DkbhJA#&F^1G0;Y7hV}S4FHD^ad>(RAmS`8?Ly6maW&NLWG>+v z&|!%wJr$$rSv}h!Tjm_l_pqk-MuWabZ2FiZ6`7EM<^3!B=L$>r2-Ob>duoNOhl7r~ zvGn|r^!Y>S^ZT2w6a>?&dMmyx^(T&G(D<;XgT{v=1v}Py)*6WFHw7LO7S#wD2ZQ!Q zQf$mc&%oN@l5(MVy^ygXXx})Nm?>*c1os^fYK{%>YZUf239jSAiOqt&8FVuoS%Jj< zxZ(JDJsWV&$ipkluM(2CK>I<-!+M;(o?SSSy<#YPMXwF3Q|Yo)UM&$SYlL)!1p2{| z6F7bira*WHv!lqiG_K4u9ACBe(9ryX278Z5|P!NetAI;0}-HyQTIJ+az(BIg9a4_ad=apt* zH`VZ{8QyzPIP|b^_+epholt#5$gK}LxKABvs38)Y{>7aY462&2&}9g|tY!kbD`W(Y z4)1Fa_8t>ljU)D^Av@6!fwW(MUK47TnCT*uwR-xwf#rj{-dr^>UwB|&aDR<(@X+x7 z!@|CYgp}H#<>66VBCOMatfAz^*KCVN)8_V{9E`p2*ihQ4UQ<}_4I~XF51NJb2SuGD zaVf6+*q(>IsG3vNLdM>pec!*w!yL{H)C?pC9jiv|34gURvH4$2nE}ANp2uN-{V<^{ zX;-x7H}ltP;C6LEnRSO%_xm+vYvJvWtIOu?v}^vgun=DUM|>WHf3li(TBH7yIB%yh zO3)Y}Mlk9jMzCt}*>2hur5BQOcWLy(8a>1eY4i{?6otPIMVt04(htR#JNC@i56{=b zOPDYWk6AqEZ~qd4FgqD|pMH7eh)7IQ)vsz6x0pvFjg}H=X_L&9k&7f&I7el+3}r*t zYdEmFQCQE&0LWn$G9LCMFar)SEd1-xQl0uDoeyzLD!RJy`lX38KcI5)o8NzZ;@JS; z?RQNJC9PdTOKiWO1KmL)y9BM_irUKU8>>U+Jv-KKsohh(dt=!)S(XMGe_(Hr9Xe-+~a2-fAz#j z!OEe6mBXgeDTdWA8>}0(+0Xb-`TGv`=L%^h!?pzjWka^5f78N`f5Q~89Q4cXddyJP z&l1bxwI9Um%hIAE58?BW=o~XU6P}keoaW01?8|?>sFe&wU;uQ$nZSueF5r)7acDYp zvhISO2>T~r>YjY=)Qz8AzH#~Go9}=3M*lONc5j{M`0>W(W>-mttE6(9tGY;9ecP8dki~wqc^ZGM{q!m|mAJvR(s~y@_8$8C39P1o9*7KJ+Lywa)8+(t6fD>vr+^xV1TG6q6foE+AdaAdjv%wV2FZBm|E2d zrWdxF!fThFlsgEDj9yteE0X%um;ZXvq)e>)z!t5vy~6@i&>=wqQ!u+rqsom0>9mUj z24{H7fMH|l(mbx|u+Y201S_)o7D0B(g%Rm(7bUonVgl8ap0_4XKQ-~jcW%CYY2ue) z;qmUo8^4(Be$9siMYkdsr3B@@NT$vw2cNvr`wdXF;M~RTN8QE6#dksRn!fP4kpFHp zrIlRs+g+VmuKDwifF8;G_B!t|muPkXaX`Jaot$6a#(OBcfEW{m0Sx*t(&G9)luA~Z zZwWk=5lYM@@|e!PH-7n5)Q&lG_Ez^-0m>UcIW_UiCq?$cz?(NN{}@0d+U#M)gSi1N zX^CRpUSuIVp^6H`rKckBkSHX}I;7v(*6wcQe}EBX7;Jz5VErE4fJ;FMMbM!pWRIv@ zA+2$uuo6y8@+f2Kg=OJ+3m!o2*}xbRd%0j#sw8$V2;>G5g@Ubub6e23eax8JR~0lC zj2SJma&9!^$mrwx_5mU*#BS`a8mD-_5MS7z(r*!Bmj-pqrZfh9DHLvsA2B+IjLyE4 z{w425kF2N~T2U3;b9iKrduWgQW8+b*6_kO6ge@N%w@RCcq>(B(mkjJ0ST7{57%{FK zGOk3eSAFSS53)=?$SsR5FJXRFVp^Y~{nZNR`ULH-6ZG&Lw#NXeMo;iwcmShrVzu>> zri}=408m9(CKnsD5ss&L4tY)GBsR1#pxN`tChUeL|%dg{f7xmJqjl$cAyOV(k zi{UKph&sI(RAP+OLhAHN@k}=PP^F$S=VTVuji=w5By)={6lp~*EPm8A@y*k&MR2DJ zsWbK-dz!m3;-oKuq4)Ycol)Wg-whQNu{^YtxpftS>WU(5$jG}N@wq)-58ne(Av4Vc z>L#2UG(pG=Gvaze75|W#>UiMN0_y<0g|rwCg?QS*KH9Blg-G*H(h5OCqFP!wkJIi) zIRDzp1xRUNI*X^mFF;V*19v#dhQ|xF$dT&*qx9_MB zm)Em?EGhGx|Exc-ejqlOw76&Us4Z#4mNR6_33#sA=6sTrI;CaobEiy@f6C5SVo^oe z6#I$APBDevd1b8T16EU}*HgxY8GE4-xh`N{V-PJT)Ps&&KS2XOhfsY`MVcBBkEAc1 zOzLBb)8X;M8PIT<1H)v_++dm(XNeGJy(cb)vvKwYQv+x!bFp1oGR>caX|97K3Ks>r zB5Ez`xH$M1f6*ZwHDr1lr98@=izU;0fnUodbb>AfXc&KaI}^F2NIDjF~<< zp3+;;%KugUn&ppblF<&OMlM@w0hfb2W_Z7zcB|SS0af90|EW@Y@7u3=bc>Z>15E-J zG+(regvl?HIjT5W9)>y_IPdnrRt+-{PD3Pm0Krp6Gtw+c{by#$0XQj7pBFdleBWlM zIvRO}ZPAm@^^CvMGx^fjZoSwGk|Qb(0n5c3ue<@Q?a6Qd z0;~chm0>CeL6Qv0paz@NPL3iRv=BnF(PBi(fSS?~Gz1V0J`eKq`4|wo0J??Y(HK5t zpvs4##FLG!T-(Xoy4FT;0)fVB9x$?Whu8(76tMql^$?#G_xzkh)3@=poR3%KzDr;HC@T`tPu2Z7IF`~eC3?cy&I79_nAQ&x~%pmcq9 zYiJS*b42q;V#Wv?JwnFum5?u_Yiet3rRt%2PdnHN@{Hg&mx0#Invr8bqPHM_1qLxaBd-#ta?Vn(sd6vMy1MX;?W-_-un4q_0Jh_4?c)IvN}#Mx<{wS*9-1rg0bVmJiQaB^0d|(${=!Un{W&=k)Kn^x%aD z2kHm22D1kmhUc#p<~<;|)&=clpHJDKe%YQ-h%F52iayimVh!Duz}t;YLDXh#j}CNN zd%3>Kz{vsKK!fnW9%1hRA*QBBI~JRIF5_%QKs&Hu(0g@vFm@*x|CwV)%xOdBv;cd} zoHc4m?%OqF$>`Z8a#n@Zx&0e1Rb8kW@VvfbuzUzWnY&I%F8kP0E@_)q1-!3yzS236 z^MkJp@IwWqLjEctW%b9FHB&Z#=8i)eviJo9=>s+)c3n_c_U}<~qqekH;{&zBSxev5 z4pv@{8Oo{{vTYKKo37i^`W}HU6pXIVrnX7E6RYJhsR`hvnQQ0fNWfB|>{$ZjH+pOp?VKnorMv_PXALP{<^NmY9!h*ipf z$gfk>N+YR}fzY<*lIWKV>K*auZ zGoJXxTUae2-~aR7uaZ-YN6mF=5szYdO6N;0rGoQBh|Uk^!}1yluM$^lNJu- z4kQko6cW%F6g1}P=Dy>rpd&x0e@TD7u%J?KR0VZg#&k)2x}Yu{L^bgV-CM@2alNaC zt(mylZDo7dF>^+rH)zh4cC6&}AQk&0K51NGER32hJ&&AP+FSNasgx>p`5-&sfjxH1 z;F_zfkX-e#WeaXtTiH8txMOYm>NX+ncu?0oYPN&ul3~J(J#qSw{R9V6qW;m7m3pN% zVYX=(b}U{%(kQDU(%RLMokK@fls-(kNGA`gzruB+7~92T*fHL)|z6S-g5oI--CNDsx|f z4>}jTZ7Jg$Y1dP!tomgo@o+*4IHUG1{p0%6etK6pqZb^$PXH`JGKfqb!Uv_h8;}Ha zyP%vOx)0nq|1$2LRHF^HCD;m{?EiM$Ml#2Th%}7gArxBeefKD4YCwcWX zkwv$4Ep-DP?wGLb4~49WcyNk?vk46lI@ouqZR26wfXEPoTlR1rSi0 zhBo`&c1oM=X$n2iKt7~k5kFSJOtfqOUa}(t!;1eoq#-=4kO5dqjYm89Hy{Q@A;|5b z+jSPigjGm%-{?+{99ts$^5Nu{H~nhjsATf|MbPMp0P*=BOW=NII{}lXk#}}k>F66z zsw)<2LTYsU^0T0MMoYP1p}RG5mx=NZe+i#@Fc^RU@P1EQE9LbPgnvE$BJb@NC}WQH zf{Ak2kPT;tJ^(ozjldbe{|n~Z3&A~%xfGhJE=p0u-t;aysMb!5Y;@4l~)+{c1fi=*SwF=vWS z6+JUoa25_l7xpi|7QJXnV>NAI$0-MI)R8{wOdd^2yA!2z#`J6k-DdUl#~yZlwVmfT(VWzx$9cVE)h=`GS^*kTrpf}6w-GF?YqG51_=2TClYsZ7_%)L zg5l^zcM8RGqjyJOQ(&bqZ^M=9D-~Dg332;_x(DGH`TR~ik(g@`P-?0rq^rXj zCtfiMDU~CZsv%3&7gKsnarZW1M#q(9Z_qFwYD^oJX+Jb)ZCI%NaG@Ta!;T}|jLC6S z4&guVdEh?$yL-G2%+gbVK`qGYVATk);-|n)cO7oA_DzlKXEO{!^2E z%Hje7klO8w?$=bX_%h%(Ab>z)8cuWyB^>`0Q0i-d--+4r;1pvo{o>}EuTVEvvX(Ze z@=iSYHk5+umz19f3^wGg5l*3~$EvE>RY>Dpl&1L~kdprsCP!YISdrxFi-P6D<5M&o zfBUC5E}wy;pQ?xh6u01PCZ&@Gse4=Yjoun%pY*Dc*rZ9WiU4xb*JLr+$9(U%o#1PVeN;zCQWxnW9)l|5imEd7wRl~lE)-%H1$B!><7kkV_C@#lg`6@W zp*(0@KWdBts>~AKyR2_n(3~v^a*6#k=yrix*^%M>^&|V6hxRuM`&tBNYtYyRnXWsN z1ziGIBn!!NhU4dc#>DB5v#638E4m>xFtw zNCqXwqobC%Gh0q=IW2LbX!_Mc@|vJ!?I)JFznHPm&r$oX2=w4g@pz=lj2Jq~9;isx|2A0RqbSr&wE+WQ(V10BgPQZ`2r0cs@aGM^7eR)p<1BK;;GnOG}0ErLxR4lLko zfzu*Gm#!5kNDx$+p;_dL=-vj5bw$kG zz(3nXyDUsJfG=o6B+VD_G@3jM$YRyU1JIiHZ%bTQj(CvsGkhiVsYB`o<=zzq##Jn# zeA8x8vV-`%O;8_(vw|owLcVxJApmJm+9M+Z3Vdit%_XVZ1FB^4ve#T0K>*ZYFLKwSJ|!e8exI9M(~5`J*TtytGJ#%Z(erOdJiV(9^c0^1Hb`=y zBZ&oWe27(xq!EE;K`2^z@8Ydj{^iz7-TEK)i5CMGD7 z%DPN#O++UEk*3|-QW^DtCZi%+7)n48fi8%p^z{;aT8hCY3^rr%eGHz#pd5o~cmdEI z48I)$V8Jei8)#HgAH$*-P|4hn%Ht4A;ICrvdkjdah#Yv3-9XU-2t^AMnFyCuV{SsC zc0>~b6islJ=o3aAPH>ZAbB;L+Mw}~#oGW@N0SkD(q3>v*HJG?`)R{5joIB*4JK`)E za+VAjhMmiMDnD7gS~R7A6S3q<_Nl7t$*Db6<5bs*;ttCPEd!5JkA`Tnk^pR5#o3~x z&l9NV-wK`<<70Z(gPbqXb*~idx-GTu#LHbTb`9C)i4-}WwkUZ@FALhsfgVR`8EEvA zL-+w9eO1uD8pwTA{7?TRKKXk7ypjCXL;0(FcZ??G_s_eQv~0|o`Lg9jOMi60FDwST zf=1y$lhD*6dZN5!!*b?Vi<}Vt+ETp1$^3d%Duf?8Q#LHoez=H*_z#z8Fbhrh7UdTpKOsy@^85cs8#1XzZAU9^q||dZ^#Crx zCHq_ZQWtE#9B86?i=mC8`;Tg)*mQ67i3$JX$ers#+`zn#6Z~sbnuE>0A2ky zP>cuuTP?M-b94NEz|R+P@#CfhG(w_5pKx7qVgzFCz`P+_v0yBwT9S&-BEh-p-St=4 z%N2v*&vQf2xRFd!JtVm6aX?{D*yEXRp$_)>tTK)E1C1UPv2m_Rml+<&+`9-N(UF&a ziKzStsO5BImHnrXRawtRL{1PAD!H_catp#xFy!4%y9f#vtwzBV|1C|~(-Olg_#c7Y953MRqyU!)k8g6p zntcZlXr+B|(l5%L)MoZ3NHJ<|4rfs#o`9hHI4c(e2s&u~aj_r(QMC_Hb%WwRvPni1 zrE)vi!xyiC#pM3_^c1#nkdP?X{OBV-}_;N+=i;EM&KHw9Eu z6P}Wa;MILd6bJ5`fqa_*5MEqX$gCoc1;kt%%4woHJJ@=AOq=V=u_$-Fs!IyO$v*c+H= z7Y2xbhoTz6hMWg`Zv_#b0SD(#V%!kpc~J^O_H4-JE*@a~6&haVNv zI)WBrA)b8Qk=(Z=kUZ*0?W+zf2XdqDP~Z0ctp3gYr9#TGsVH4iT(1^eH_F8W>|J(H3{x*=>QEz*b?w zrmKcOS$}W6@)aR_|HqC8MO8X#T>D?2Pvs!kUqFM(HT+laYhft*5eJ!#4yP+%F5!wJ zdWa+!_+*o@63+&eQbi=(o|XzmYs$$7kRaJf%ToE@IRub)=|P)a4_^4hb274-i|F$_ zN?qy`#N-pX})iVqBZ5)id{B!t|Lz)(5ss zUR~hTQ|Z5%=UAe;|L)T(CS|pNgmGL|{>JxpoF>g{QDuU8+9I3>){E`z)2n%+u6#?*XRu{|f8>ky-YCIw>nd)ec!- z0NSBi?P@`l>Wup8tokYJjEcNd^6EWz=9QkQUg;5L#_oS&W@t~}P_<&_eh=HFmARzK zJM&C&so|FLwi>6;!w4-}sw#ixe#s&^VKTu^ovof3v+s}AbX83=_d*Wg*yO6~=rFf_ zXW9ySZkF>;tAac4{4LzD2O{5DK1(y}%LM_~2zK`~s!*8&CXV;;G9Mmwtir7*KZs?TDTq-#l}E;@KA_ zPoKK=%{NfWGWa9v2yOB_8sh&`A~V@{m+}b=0+hGz$Ppm4j`+M@{1hN~s0XIOJwOoC zx2Hh4lH^foq)Ym#B7C5Fss7~Sy?k9t{D!HgM%GIudZ5(64v}f}6-X6e=x63o$AnPm zlc!!ob$bHw3LLS5~e|+P) zOLT_NE|aFjX)rE;iKI?Xq=EOgg~CkiE?oDPs;76P0ud8-QElc$Y84_J2sCVW-TdV*p$w`)te#a+Sj0-`_X(k2>8MHT zY5Fn(&JH4TVq2iK)0ibcgz2vJi@Ierok|p`KxiZGB2NcyzW3g(b8p=GexI*E zX(XL2bOK;4#Lb1+>A0Mz9v6%~trxP!2U;|t0rk`Htb&x%zsoW6K*EcuT zALo%oE{ah!y+U#KZfm@NnZQqgm+ugbDx>k^j`T}{A-0xkfPIFk?6|{Wz2H>`Y%txP zkX?DBnfWP9Xbr0;WWfR@$UQNT0)<7&Ma4#`4Q=3$5u7Oz;A_K*;QtFUT?5vHrxLb3 z5Fw>7@ec5%5S`Fx9*)kL(sZzxK2TLh{2wOWk;Ue&9rZ zM=*Y+BtR{opUA5fTn`8BbyFHkY~Fb8l7RzP48ysb&Ti|i3b3Q;1@}0c+bm>N1|3yX ziHs|MBy-tN<}%THpvn{6TH%2m!rGle@~)s|_jOwVcugF#trCUF(l-sug!J-%v#%Fr z$x`e|A^p*yz2kaLo?v&4=Hxp;|?Y2Y!Td@p?Z zFzDKU-I?*S`9<@2>!=gHC^+Pt5Blt5&a{`KUyKecJddBjo406WZu!vMa`FQaU05s(+`Dw=3w}tpfKG zrwfJX))%e)Q3Iy_$Ar@3LVk15*@DvZ=v=Fh9fk37Ego_${`uy&x4gMUDBCmas_wO3 zx2KQTbB63WL3`e`5rVHL9uxM08h%sIejNFf3B(`cLik3YQ6BwX%d}iuu5P`$R!B$J zde^~e$37N)b=(2TBznw?ip|4@Bkyoie`WtFVcC|C)`Rx{A1kh|5VEU-j=iH1+Aa^u zr_jpG+%LdXEH>|pJDXW1Ef08#Fc+Zdwij*v1p`F`slw`-;6eEM%F*G24dB91C~OQm zn?7}>BmXcVgJg5S*I`&|GjX}6@?{6Z`mnGKOdT49jHZ9HA0LZP1MNxZJ8WX4bgjWQ zUhsH_+k8Un2_fTT(Ecd&lXGtWvVoPsq&3sVxL(NRf)4jsQbE6IU|BG!blRvM6!H!Q zorkYGa{`YHISc#DqpA7*sn=4MjHS6=u6nU5@W}aXpQhzb8|Q68{`R1A$5>`ARGpBf zI3g5Q@ItO9=f$3{uYl?z|6;(Aqe#n!6_uWl0{bq1G2%CuXuwSP>)0{)mz!fZYK@W%oz#>_Eog?pVI%-8-Y7Yq8+oMdF-$#a$-pY!#Q zRG3?q0Wa6grs{0%HBectPS*z0_3#qrA_)&f!p}xo%Tt=jX4g+^zy}HoK2TIH(RwuS z9Y)R9jehptZw2lF8&qEw6iAgRsFDb{%AlUyC!hJ|jmt05&zTh&h}Qef_s@faEI@9N zPI(v7y-xMO$KUzJ_+a<=AYikA*qs7mf)+tC-FW`Z@pqo1{-|%h@yd-i&Oz=QuU(q_ z-dAZAGU7)w@f`qBC~mv^r1AwU`thsd@B`lrL0H#dx1cJ=Kk!vM^1(Cy2xJ0tH4bhB zRgpey2duso<2O22@1Ev78s?(y}Rg0`%on6kg~ z$FN=v6{npbFhI@n=a^%s*Wf{CgwhAu)=>;2F)}c`0a8AU)fZy*;JVQs-&^^unC{BY zG*QV0&?CPCzLspmF=ax@x?oJ%u(?domGRdgaafnidQL7V@BaQ6>8t~1T^jJIh{FLF zaK>aANhHN+HLsgG^FxPUD%u^9@o5)0=F?m>NLnoStwH9Cflh~J8n+`0jEfP2Cl|Q; zleJG2zc#>4lXYLv8zk60p=-TpdRKUYMP_QK@zk>*nN^z>!e{S`bRnbUAhJ_aa6HLTF)>sWQ&Jb_ycuRg3(hV?_5pR(+4Kpb z=gDhHtKm!91CGDK7mt^#ti=KEO3M#$fqg>qydL=cah&LAE~DT3v2h6;aiS&QqSG<- zLvP@pk|<4C1^ayY^T;ve^T?>xM-`?}g#SdDt1O54Ajed0)qXH9sobdjl~E7RVG|kI zdt}?hDL!o?L(KHcD@R0f8m9UcI$`=HP0QJOheaj9f80YX^-b5I6Wtx@sZA_V!=5(v zM3uku{nSAL-v-?wX;wYlf|B;cIrL%qv-=35n+pojnv-HJ}r+mkuW^ z1Gg^m;1=cVs=t9@X6D~86%4p3^NT41t51M$`Q0U3*At7%vYDSd%M!I8SWNKvL887a zGwOp>Ek0+GFk4fWKZADW|8^t*10-IOgesyT%KvsGAaxVDbN?+z0(=k(;YTp|1cH0) zxf94o4UU4lE@@ZAEta>zsyYR$ivJXIqV{f)=B^d?=HeqwO(%=%n;PoxCMg`pt!)ho z%aMM#RSO1=PzWCjK@oe)1G;uaYz3SzaS(9}_4MFW!4z7aa@OqxfPOlmNEYqt?T zyMc&7C{aw+){Y*}JjJ5dFY?ulP`U>=L9MX)s0#7;+R?85@iQVG zehl+Ji@{9{{sV*47$5_QAbR9H=5bdhlZfE61eYU+_+8`bmNw4U>|V>~Koa=Rc>Zq) zz&k6;{+WsSGh_ZU6Z>bz{%6MeXU6nr#`Y(zHMez5EE{wCLG4=BdAnRwtzq5lXB7r5yX>=Q z18co)cd(_O&9kzxf7#70Vzc_SQw-b&bII-MlH2%p`xxtFW2aUytZ9Ua9b#ffnB*ZQ zd4!oW#LNjY^KR?1+1T5g*&_BhdwUsl5;Ucz-z0Dc7XxP}#4(ZHnIeR+= Qf5?ww6Yj)8F47tQ596BNe*gdg literal 0 HcmV?d00001 diff --git a/globals/__pycache__/ex_apis.cpython-312.pyc b/globals/__pycache__/ex_apis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d8b6e3bdd83013a2384bea03ea5a115f281a332b GIT binary patch literal 12070 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 literal 0 HcmV?d00001 diff --git a/globals/__pycache__/ids.cpython-312.pyc b/globals/__pycache__/ids.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8c901c95349c9b9fa688fdaa02d34727d1757ef GIT binary patch literal 2614 zcmaJ@TTk0a6b_}O+;R_r6ndcrT4;f#)po14+g&G_mZ*snIR>^Pq>;&tsHqc2_5_Nb zvcF<~!%F*4_Jv2wulv5Lv`^cY-7`)c1vw=T)|~Hr=YGcVpV8=a1b%-%`0wMvR3!43 z5Q5)W|M1TraQH1kBRE3G@EE;@uhDTlPOszZ^aj2`C-4NliEq+LJV~eU6rIM?bOz7R zSv*UlI7(wUM&me66F5PWI7w4DMbkJhwss4ybQkX)BE@VeSlZ!3SOnFc#W>%hx8$SL?7Yx^Y!%&=XS$v zZXKPRoNYBujvFs^y3IK0*vuj=t;w9FZ>rn&nR6?oJ>o55n8$j{SskhCw5i4!Inr#_ zHuV;F?p;MQ^cHjGLks|#I(Jf9i&~~mMbZtk#TmFu4r3jgo6Qjw>dj-aLk#WQ?C@`> zp9Sj2q3AXuthMNAyEfKn&gH&Yh4WowH^E^45T3IoXh&>)c>YE`-|< zUKYa%t3>;cw8kfAIc&wvHfb`=?pV-&-Wrjt9}w&~rQbcOYp^DMXHIo?<|KwFoPFjy&iYlZAxE>xxTQA;aZI0s4C$ri!H$ z_tP>VAcJ=UF)bo`6@Iz&+TUCJaQeOdY-_s3D530BZ!p`r8&3Z+A-o+yWflugn$hdU zR>+yw;pX7l2wG6G*iw0Y!;6)x?@BV+t0P60DkzX^QoVjqRf^tJPoGz1FR@oEN-83N zzbARAj=W?aDWiQZ$Z+uu-lTsZka=|xNw}Hk7m=j9^!-I72UudIJL@J_ zx-mFs0ZX}=XP_76KY?Xxs~dN-V3Bs`w=W_YH?;|X|H%B%<7K`RUgv%kwy77O^%ZXJ z@kJ!-W+h>oL*ktJxf=(+vDNOhJG0iEbmLDTc4D&|bz_gaGj3`FjFX#wD)VoIT^d3p n-0Y4xFTTBqEVzkRz)ozzBl$v9_Y@w9*C0+j|8?BjgzEnXQHyye literal 0 HcmV?d00001 diff --git a/globals/apis.py b/globals/apis.py new file mode 100644 index 0000000..6864abc --- /dev/null +++ b/globals/apis.py @@ -0,0 +1,520 @@ +import requests +import json +import logging +import socket +from typing import Optional, Dict, Any +import globals.global_variable as global_variable + +def send_tcp_command(command="StartMultiple", host="127.0.0.1", port=8888, timeout=10): + """ + 使用TCP协议发送命令到指定地址和端口 + + 参数: + command: 要发送的命令字符串(默认:"StartMultiple") + host: 目标主机地址(默认:"127.0.0.1") + port: 目标端口(默认:8888) + timeout: 连接超时时间(秒,默认:10) + + 返回: + 成功返回服务器响应(字符串),失败返回None + """ + # 创建TCP套接字 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + # 设置超时时间 + sock.settimeout(timeout) + + # 连接到目标服务器 + sock.connect((host, port)) + logging.info(f"已成功连接到 {host}:{port}") + + # 发送命令(注意:需要根据服务器要求的编码格式发送,这里用UTF-8) + sock.sendall(command.encode('utf-8')) + logging.info(f"已发送命令: {command}") + + # 接收服务器响应(缓冲区大小1024字节,可根据实际情况调整) + response = sock.recv(1024) + if response: + response_str = response.decode('utf-8') + logging.info(f"收到响应: {response_str}") + return response_str + else: + logging.info("未收到服务器响应") + return None + + except ConnectionRefusedError: + logging.info(f"连接被拒绝,请检查 {host}:{port} 是否开启服务") + return None + except socket.timeout: + logging.info(f"连接超时({timeout}秒)") + return None + except Exception as e: + logging.info(f"发送命令时发生错误: {str(e)}") + return None + +def get_breakpoint_list(): + """ + 获取需要处理的断点列表 + """ + # 请求参数 + params = { + 'user_name': global_variable.GLOBAL_USERNAME + } + + # 请求地址 + url = "https://engineering.yuxindazhineng.com/index/index/get_name_all" + + try: + # 发送GET请求 + response = requests.get(url, params=params, timeout=30) + + # 检查请求是否成功 + if response.status_code == 200: + result = response.json() + + # 检查接口返回状态 + if result.get('code') == 0: + data = result.get('data', []) + logging.info("成功获取断点列表,数据条数:", len(data)) + + # 打印断点信息 + # for item in data: + # logging.info(f"线路编码: {item.get('line_num')}, " + # f"线路名称: {item.get('line_name')}, " + # f"状态: {item.get('status')}, " + # f"用户: {item.get('name')}") + + return data + else: + logging.info(f"接口返回错误: {result.get('code')}") + return [{"id": 37, + "user_name": "wangshun", + "name": "wangshun", + "line_num": "L193588", + "line_name": "CDWZQ-2标-155号路基左线-461221-461570-155左-平原", + "status": 3 + }] + else: + logging.info(f"请求失败,状态码: {response.status_code}") + return [] + + except requests.exceptions.RequestException as e: + logging.info(f"请求异常: {e}") + return [] + except ValueError as e: + logging.info(f"JSON解析错误: {e}") + return [] + +def filter_breakpoint_list_by_status(status_codes): + """ + 根据状态码过滤断点列表,只保留line_name + + Args: + status_codes: 状态码列表,如 [0, 1] 或 [0, 1, 2, 3] + + Returns: + list: 包含line_name的列表 + """ + data = get_breakpoint_list() + + if not data: + logging.info("获取断点列表失败或列表为空") + return [] + + # 根据状态码过滤数据 + if status_codes: + filtered_data = [item for item in data if item.get('status') in status_codes] + logging.info(f"过滤后的断点数量 (状态{status_codes}): {len(filtered_data)}") + + # 按状态分组显示 + for status in status_codes: + status_count = len([item for item in filtered_data if item.get('status') == status]) + logging.info(f"状态{status}的断点数量: {status_count}") + else: + # 如果没有指定状态码,返回所有数据 + filtered_data = data + logging.info("未指定状态码,返回所有数据") + return filtered_data + +def get_measurement_task(): + """ + 获取测量任务 + 返回: 如果有状态为1的数据返回任务信息,否则返回None + """ + try: + url = "https://engineering.yuxindazhineng.com/index/index/getOne" + + # 获取用户名 + user_name = global_variable.GLOBAL_USERNAME + if not user_name: + logging.error("未设置用户名,无法获取测量任务") + return None + + # 构造请求参数 + data = { + "user_name": user_name + } + + logging.info(f"请求参数: user_name={user_name}") + response = requests.post(url, data=data, timeout=10) + response.raise_for_status() + + data = response.json() + logging.info(f"接口返回数据: {data}") + + if data.get('code') == 0 and data.get('data'): + task_data = data['data'] + if task_data.get('status') == 1: + logging.info(f"获取到测量任务: {task_data}") + return task_data + else: + logging.info("获取到的任务状态不为1,不执行测量") + return None + else: + logging.warning("未获取到有效任务数据") + return None + + except Exception as e: + logging.error(f"获取测量任务失败: {str(e)}") + return None + +def get_end_with_num(): + """ + 根据线路编码获取测量任务 + 返回: 如果有状态为1的数据返回任务信息,否则返回None + """ + try: + url = "https://engineering.yuxindazhineng.com/index/index/getOne3" + + # 获取用户名 + user_name = global_variable.GLOBAL_USERNAME + line_num = global_variable.GLOBAL_LINE_NUM + if not line_num: + logging.error("未设置线路编码,无法获取测量任务") + return None + if not user_name: + logging.error("未设置用户名,无法获取测量任务") + return None + + # 构造请求参数 + data = { + "user_name": user_name, + "line_num": line_num + } + + # logging.info(f"请求参数: user_name={user_name}, line_num={line_num}") + response = requests.post(url, data=data, timeout=10) + response.raise_for_status() + + data = response.json() + logging.info(f"接口返回数据: {data}") + + if data.get('code') == 0 and data.get('data'): + task_data = data['data'] + if task_data.get('status') == 3: + logging.info(f"获取到测量任务: {task_data}") + return task_data + else: + logging.info("获取到的任务状态不为3,不执行测量") + return None + else: + # logging.warning("未获取到有效任务数据") + return None + + except Exception as e: + logging.error(f"获取测量任务失败: {str(e)}") + return None + + + +def change_breakpoint_status(user_name, line_num, status): + """ + 修改断点状态 + + Args: + user_name: 登录账号名 + line_num: 线路编码 + status: 当前工作状态,0未开始 1操作中 2操作完成 + + Returns: + bool: 操作是否成功 + """ + try: + url = "https://engineering.yuxindazhineng.com/index/index/change" + data = { + "user_name": user_name, + "line_num": line_num, + "status": status + } + + response = requests.post(url, data=data, timeout=10) + result = response.json() + + if result.get("code") == 0: + logging.info(f"修改断点状态成功: 线路{line_num} 状态{status} - {result.get('msg')}") + return True + else: + logging.error(f"修改断点状态失败: 线路{line_num} 状态{status} - {result.get('msg')}") + return False + + except Exception as e: + logging.error(f"修改断点状态请求异常: {str(e)}") + return False + + +def get_one_addr(user_name): + """ + 根据用户名获取一个地址信息 + + Args: + user_name (str): 登录用户名 + + Returns: + dict: API的原始响应数据 + """ + # 请求地址 + url = "https://engineering.yuxindazhineng.com/index/index/getOneAddr" + + # 请求参数 + data = { + "user_name": user_name + } + + # 请求头 + headers = { + 'Content-Type': 'application/json' + } + + try: + # 发送POST请求 + response = requests.post(url,data=data,timeout=10) + + # 检查请求是否成功 + response.raise_for_status() + + # 解析返回数据的地址 + addr = response.json().get('data').get('addr') + if addr: + logging.info(f"获取到地址: {addr}") + else: + logging.warning("返回数据中未包含地址信息") + + # 直接返回API的响应 + return addr + + except requests.exceptions.RequestException as e: + # 返回错误信息 + return False + except json.JSONDecodeError as e: + return False + +def get_work_conditions_by_linecode(linecode: str) -> Optional[Dict[str, Dict]]: + """ + 通过线路编码获取工况信息 + + Args: + linecode: 线路编码,如 "L118134" + + Returns: + 返回字典,格式为 {point_id: {"sjName": "", "workinfoname": "", "work_type": ""}} + 如果请求失败返回None + """ + url="http://www.yuxindazhineng.com:3002/api/comprehensive_data/get_settlement_by_linecode" + max_retries = 3 # 最大重试次数 + retry_count = 0 # 当前重试计数 + while retry_count < max_retries: + try: + # 准备请求参数 + payload = {"linecode": linecode} + headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + logging.info(f"发送POST请求到: {url}") + logging.info(f"请求参数: {payload}") + + # 发送POST请求 + response = requests.post( + url, + json=payload, + headers=headers, + timeout=30 + ) + + # 检查响应状态 + if response.status_code != 200: + logging.error(f"HTTP请求失败,状态码: {response.status_code}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + continue # 继续重试 + + # 解析响应数据 + try: + result = response.json() + except json.JSONDecodeError as e: + logging.error(f"JSON解析失败: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + continue # 继续重试 + + + # 检查API返回码 + if result.get('code') != 0: + logging.error(f"API返回错误: {result.get('message', '未知错误')}") + return None + + # 提取数据 + data_list = result.get('data', []) + if not data_list: + logging.warning("未找到工况数据") + return {} + + # 处理数据,提取所需字段 + work_conditions = {} + for item in data_list: + point_id = item.get('aname') + if point_id: + work_conditions[point_id] = { + "sjName": item.get('sjName', ''), + "workinfoname": item.get('workinfoname', ''), + "work_type": item.get('work_type', '') + } + + logging.info(f"成功提取 {len(work_conditions)} 个测点的工况信息") + return work_conditions + + except requests.exceptions.RequestException as e: + logging.error(f"网络请求异常: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + except json.JSONDecodeError as e: + logging.error(f"JSON解析失败: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + except Exception as e: + logging.error(f"获取工况信息时发生未知错误: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + # 达到最大重试次数仍失败 + logging.error(f"已达到最大重试次数 ({max_retries} 次),请求失败") + return None + +def get_user_max_variation(username: str) -> Optional[int]: + """ + 调用POST接口根据用户名获取用户的max_variation信息 + + Args: + username: 目标用户名,如 "chzq02-02guoyu" + + Returns: + 成功:返回用户的max_variation整数值 + 失败:返回None + """ + # 接口基础配置 + api_url = "http://www.yuxindazhineng.com:3002/api/accounts/get" + timeout = 30 # 超时时间(避免请求长时间阻塞) + + # 1. 准备请求参数与头部 + # 接口要求的POST参数(JSON格式) + payload = {"username": username} + # 请求头部:指定JSON格式,模拟浏览器UA避免被接口拦截 + headers = { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + try: + # 2. 发送POST请求 + logging.info(f"向接口 {api_url} 发送请求,查询用户名:{username}") + response = requests.post( + url=api_url, + json=payload, # 自动将字典转为JSON字符串,无需手动json.dumps() + headers=headers, + timeout=timeout + ) + + # 3. 检查HTTP响应状态(200表示请求成功到达服务器) + response.raise_for_status() # 若状态码非200(如404、500),直接抛出HTTPError + logging.info(f"接口请求成功,HTTP状态码:{response.status_code}") + + # 4. 解析JSON响应(处理文档中提到的"网页解析失败"风险) + try: + response_data = response.json() + except json.JSONDecodeError as e: + logging.error(f"接口返回数据非JSON格式,解析失败:{str(e)}") + logging.error(f"接口原始返回内容:{response.text[:500]}") # 打印前500字符便于排查 + return None + + # 5. 检查接口业务逻辑是否成功(按需求中"code=0表示查询成功") + if response_data.get("code") != 0: + logging.error(f"接口查询失败,业务错误信息:{response_data.get('message', '未知错误')}") + return None + + # 6. 验证返回数据结构并提取max_variation + data_list = response_data.get("data", []) + if not data_list: + logging.warning(f"查询到用户名 {username},但未返回账号数据") + return None + + # 检查第一条数据是否包含max_variation + first_user = data_list[0] + if "max_variation" not in first_user: + logging.warning(f"用户 {username} 的返回数据中缺少 max_variation 字段") + return None + + max_variation = first_user["max_variation"] + logging.info(f"成功查询到用户 {username} 的 max_variation:{max_variation}") + + # 7. 直接返回max_variation的值 + return max_variation + + # 处理请求过程中的异常(网络问题、超时等) + except requests.exceptions.RequestException as e: + logging.error(f"接口请求异常(网络/超时/服务器不可达):{str(e)}") + # 若为连接错误,提示检查文档中提到的"不支持的网页类型"或域名有效性 + if "ConnectionRefusedError" in str(e) or "Failed to establish a new connection" in str(e): + logging.error(f"建议排查:1. 接口域名 {api_url} 是否可访问;2. 服务器是否正常运行;3. 端口3002是否开放") + return None + + # 处理其他未知异常 + except Exception as e: + logging.error(f"获取用户 {username} 的 max_variation 时发生未知错误:{str(e)}") + return None + +def get_accounts_from_server(yh_id): + """从服务器获取账户信息""" + url = "http://www.yuxindazhineng.com:3002/api/accounts/get_uplaod_data" + headers = { + "Content-Type": "application/json" + } + data = { + "yh_id": yh_id + } + + try: + print(f"🔍 查询服务器账户信息,用户ID: {yh_id}") + response = requests.post(url, headers=headers, json=data, timeout=10) + + if response.status_code == 200: + result = response.json() + if result.get("code") == 0: + print(f"✅ 查询成功,找到 {result.get('total', 0)} 个账户") + return result.get("data", []) + else: + print(f"❌ 查询失败: {result.get('message', '未知错误')}") + return [] + else: + print(f"❌ 服务器响应错误: {response.status_code}") + return [] + except requests.exceptions.RequestException as e: + print(f"❌ 网络请求失败: {e}") + return [] + except json.JSONDecodeError as e: + print(f"❌ JSON解析失败: {e}") + return [] \ No newline at end of file diff --git a/globals/create_link.py b/globals/create_link.py new file mode 100644 index 0000000..f9d915b --- /dev/null +++ b/globals/create_link.py @@ -0,0 +1,273 @@ +import subprocess +import re +import time +import requests +import json +from appium import webdriver +from appium.webdriver.common.appiumby import AppiumBy +from appium.options.android import UiAutomator2Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from urllib3.connection import port_by_scheme + + +# ======================= +# 基础工具函数 +# ======================= + +def run_command(command): + """执行系统命令并返回输出""" + result = subprocess.run(command, shell=True, capture_output=True, text=True) + return result.stdout.strip() + + +# ======================= +# 无线ADB连接管理 +# ======================= + +def check_wireless_connections(target_port=4723): + """ + 检查当前无线ADB连接状态 + 返回: (list) 当前无线连接的设备列表,每个元素为(device_id, ip, port) + """ + devices_output = run_command("adb devices") + lines = devices_output.splitlines()[1:] + + wireless_connections = [] + + for line in lines: + if not line.strip(): + continue + + parts = line.split() + if len(parts) < 2: + continue + + device_id = parts[0] + status = parts[1] + + # 检查是否为无线连接(包含:端口) + if ":" in device_id and status == "device": + # 解析IP和端口 + ip_port = device_id.split(":") + if len(ip_port) == 2: + ip, port = ip_port[0], ip_port[1] + wireless_connections.append((device_id, ip, int(port))) + + return wireless_connections + +def disconnect_wireless_connection(connection_id): + """断开指定的无线ADB连接""" + print(f" 断开连接: {connection_id}") + result = run_command(f"adb disconnect {connection_id}") + return result + +def cleanup_wireless_connections(target_device_ip=None, target_port=4723): + """ + 清理无线ADB连接 + - 如果target_device_ip为None:断开所有端口为4723的连接 + - 如果target_device_ip有值:断开所有端口为4723且IP不是目标设备的连接 + 返回: (bool) 是否需要建立新连接 + """ + print("\n🔍 检查无线ADB连接状态...") + + # 获取当前所有无线连接 + wireless_connections = check_wireless_connections(target_port) + + if not wireless_connections: + print("📡 当前没有无线ADB连接") + return True # 需要建立新连接 + + print(f"📡 发现 {len(wireless_connections)} 个无线连接:") + for conn_id, ip, port in wireless_connections: + print(f" - {conn_id} (IP: {ip}, 端口: {port})") + + need_new_connection = True + connections_to_disconnect = [] + + for conn_id, ip, port in wireless_connections: + # 检查端口是否为4723 + if port != target_port: + print(f" ⚠️ 连接 {conn_id} 端口不是 {target_port},保持不动") + continue + + # 如果没有指定目标IP,断开所有4723端口的连接 + if target_device_ip is None: + connections_to_disconnect.append(conn_id) + continue + + # 如果指定了目标IP,检查IP是否匹配 + if ip == target_device_ip: + print(f" ✅ 发现目标设备的连接: {conn_id}") + need_new_connection = False # 已有正确连接,不需要新建 + else: + print(f" ⚠️ 发现其他设备的4723端口连接: {conn_id}") + connections_to_disconnect.append(conn_id) + + # 断开需要清理的连接 + for conn_id in connections_to_disconnect: + disconnect_wireless_connection(conn_id) + time.sleep(1) # 等待断开完成 + + # 如果断开了一些连接,重新检查状态 + if connections_to_disconnect: + print("🔄 重新检查连接状态...") + time.sleep(2) + remaining = check_wireless_connections(target_port) + if remaining: + for conn_id, ip, port in remaining: + if ip == target_device_ip and port == target_port: + print(f" ✅ 目标设备连接仍然存在: {conn_id}") + need_new_connection = False + break + + return need_new_connection + + +# ======================= +# Appium 启动 +# ======================= + +def start_appium(): + appium_port = 4723 + print(f"🚀 启动 Appium Server(端口 {appium_port})...") + subprocess.Popen( + ["appium.cmd", "-a", "127.0.0.1", "-p", str(appium_port)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + # 检查端口是否就绪(替代固定sleep) + max_wait = 30 # 最大等待30秒 + start_time = time.time() + while time.time() - start_time < max_wait: + try: + # 尝试连接Appium端口,验证是否就绪 + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + result = sock.connect_ex(("127.0.0.1", appium_port)) + sock.close() + if result == 0: # 端口就绪 + print(f"✅ Appium Server 启动成功(端口 {appium_port})") + return True + except Exception: + pass + time.sleep(1) + + print(f"❌ Appium Server 启动超时({max_wait}秒)") + return False + + +# ======================= +# 无线 ADB 建链主流程 +# ======================= + +def setup_adb_wireless(): + target_port = 4723 + print(f"🚀 开始无线 ADB 建链(端口 {target_port})") + + # 获取USB连接的设备 + devices_output = run_command("adb devices") + lines = devices_output.splitlines()[1:] + + usb_devices = [] + + for line in lines: + if not line.strip(): + continue + + parts = line.split() + if len(parts) < 2 or parts[1] != "device": + continue + + device_id = parts[0] + + # 跳过已经是无线的 + if ":" in device_id: + continue + + usb_devices.append(device_id) + + if not usb_devices: + print("❌ 未检测到 USB 设备") + return + + for serial in usb_devices: + print(f"\n🔎 处理设备: {serial}") + + # 获取WLAN IP + ip_info = run_command(f"adb -s {serial} shell ip addr show wlan0") + ip_match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', ip_info) + + if not ip_match: + print("⚠️ 获取 IP 失败,请确认已连接 WiFi") + continue + + device_ip = ip_match.group(1) + print(f"📍 设备 IP: {device_ip}") + + # ===== 清理现有无线连接 ===== + need_new_connection = cleanup_wireless_connections( + target_device_ip=device_ip, + target_port=target_port + ) + + # ===== 建立新连接(如果需要) ===== + if need_new_connection: + print(f"\n🔌 建立新的无线连接: {device_ip}:{target_port}") + + # 切 TCP 模式 + print(f" 设置设备 {serial} 为 TCP 模式,端口 {target_port}...") + run_command(f"adb -s {serial} tcpip {target_port}") + time.sleep(3) # 等待模式切换 + + # 无线连接 + connect_result = run_command(f"adb connect {device_ip}:{target_port}") + time.sleep(2) # 等待连接稳定 + + if "connected" not in connect_result.lower(): + print(f"❌ 无线连接失败: {connect_result}") + continue + else: + print(f"✅ 无线连接成功: {device_ip}:{target_port}") + else: + print(f"✅ 已存在目标设备的有效连接,跳过新建") + + # 验证连接 + wireless_id = f"{device_ip}:{target_port}" + verify_result = run_command("adb devices") + if wireless_id in verify_result and "device" in verify_result.split(wireless_id)[1][:10]: + print(f"✅ 连接验证通过: {wireless_id}") + else: + print(f"❌ 连接验证失败,请检查") + continue + + # ===== 后续自动化 ===== + if not start_appium(): + print("❌ Appium启动失败,跳过后续操作") + continue + + # driver, app_started = start_settlement_app(wireless_id, device_ip, target_port) + + # if not app_started: + # print("⚠️ App启动失败,跳过后续操作") + # continue + + + print(f"🎉 所有操作完成! 设备 {serial} 已就绪") + + # # 关闭Appium连接 + # if driver: + # print("🔄 关闭Appium连接...") + # driver.quit() + + break # 处理完第一个设备后退出,如需处理多个设备可移除此行 + + +# ======================= +# 程序入口 +# ======================= + +if __name__ == "__main__": + # 配置参数 + setup_adb_wireless() \ No newline at end of file diff --git a/globals/driver_utils.py b/globals/driver_utils.py new file mode 100644 index 0000000..89b1a12 --- /dev/null +++ b/globals/driver_utils.py @@ -0,0 +1,1195 @@ +import logging +import time +import subprocess +import traceback +import socket +import os +import requests +from appium import webdriver +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.appium_service import AppiumService +from appium.options.android import UiAutomator2Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException, InvalidSessionIdException, WebDriverException +import globals.global_variable as global_variable + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s") + +def init_appium_driver(device_id, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"): + """ + 初始化Appium驱动的全局函数 + + 参数: + device_id: 设备ID + app_package: 应用包名,默认为"com.bjjw.cjgc" + app_activity: 应用启动Activity,默认为".activity.LoginActivity" + + 返回: + (driver, wait): (WebDriver实例, WebDriverWait实例),如果初始化失败则抛出异常 + """ + logging.info(f"设备 {device_id} 开始初始化Appium驱动") + + # 创建并配置Appium选项 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = device_id + options.app_package = app_package + options.app_activity = app_activity + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 28800 + options.udid = device_id + + # 增加uiautomator2服务器启动超时时间 + options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒 + # 增加连接超时设置 + options.set_capability('connection_timeout', 120000) # 120秒 + + # # 添加额外的能力,避免初始化错误 + # options.set_capability('skipDeviceInitialization', True) # 跳过设备初始化,避免修改系统设置 + # options.set_capability('disableHiddenApiPolicy', True) # 禁用隐藏API策略,避免权限问题 + # options.set_capability('skipServerInstallation', True) # 跳过服务器安装,使用已有的UiAutomator2服务器 + # options.set_capability('skipUnlock', True) # 跳过解锁屏幕,避免干扰 + + try: + # driver_url = "http://127.0.0.1:4723/wd/hub" + # # 连接Appium服务器 + # logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + # driver = webdriver.Remote(driver_url, options=options) + # logging.info(f"设备 {device_id} Appium服务器连接成功") + driver_urls = [ + "http://127.0.0.1:4723/wd/hub", # 标准路径 + "http://127.0.0.1:4723", # 简化路径 + "http://localhost:4723/wd/hub", # localhost + ] + + driver = None + last_exception = None + + # 尝试多个URL + for driver_url in driver_urls: + try: + logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + driver = webdriver.Remote(driver_url, options=options) + logging.info(f"设备 {device_id} Appium服务器连接成功: {driver_url}") + break # 连接成功,跳出循环 + except Exception as e: + last_exception = e + logging.warning(f"设备 {device_id} 连接失败 {driver_url}: {str(e)[:100]}") + continue + + # 检查是否连接成功 + if not driver: + logging.error(f"设备 {device_id} 所有Appium服务器地址尝试失败") + logging.error(f"最后错误: {str(last_exception)}") + raise Exception(f"设备 {device_id} 无法连接到Appium服务器: {str(last_exception)}") + + # 初始化等待对象 + wait = WebDriverWait(driver, 20) + logging.info(f"设备 {device_id} WebDriverWait初始化成功") + + # 等待应用稳定 + time.sleep(2) + + # 设置屏幕永不休眠 + try: + # 使用ADB命令设置屏幕永不休眠 + screen_timeout_cmd = [ + "adb", "-s", device_id, + "shell", "settings", "put", "system", "screen_off_timeout", "86400000" + ] + timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15) + if timeout_result.returncode == 0: + logging.info(f"设备 {device_id} 已成功设置屏幕永不休眠") + else: + logging.warning(f"设备 {device_id} 设置屏幕永不休眠失败: {timeout_result.stderr}") + except Exception as timeout_error: + logging.warning(f"设备 {device_id} 设置屏幕永不休眠时出错: {str(timeout_error)}") + + + logging.info(f"设备 {device_id} Appium驱动初始化完成") + return driver, wait + + except Exception as e: + logging.error(f"设备 {device_id} : {str(e)}") + logging.error(f"错误类型: {type(e).__name__}") + logging.error(f"错误堆栈: {traceback.format_exc()}") + # 如果驱动已创建,尝试关闭 + if 'driver' in locals() and driver: + try: + driver.quit() + except: + pass + raise + +def check_session_valid(driver, device_id=None): + """ + 检查当前会话是否有效 + + 参数: + driver: WebDriver实例 + device_id: 设备ID(可选) + + 返回: + bool: 会话有效返回True,否则返回False + """ + # 从全局变量获取设备ID + if device_id is None: + device_id = global_variable.GLOBAL_DEVICE_ID + device_str = f"设备 {device_id} " if device_id else "" + + if not driver: + logging.warning(f"{device_str}驱动实例为空") + return False + + try: + # 首先检查driver是否有session_id属性 + if not hasattr(driver, 'session_id') or not driver.session_id: + logging.debug(f"{device_str}驱动缺少有效的session_id") + return False + # 尝试获取当前上下文,如果会话无效会抛出异常 + current_context = driver.current_context + logging.debug(f"{device_str}会话检查通过,当前上下文: {current_context}") + return True + except InvalidSessionIdException: + logging.error(f"{device_str}会话已失效 (InvalidSessionIdException)") + return False + except WebDriverException as e: + error_msg = str(e).lower() + + # 明确的会话失效错误 + if any(phrase in error_msg for phrase in [ + "session is either terminated or not started", + "could not proxy command to the remote server", + "socket hang up", + "connection refused", + "max retries exceeded" + ]): + logging.debug(f"{device_str}会话连接错误: {error_msg[:100]}") + return False + else: + logging.debug(f"{device_str}WebDriver异常但可能不是会话失效: {error_msg[:100]}") + return True + except (ConnectionError, ConnectionRefusedError, ConnectionResetError) as e: + logging.debug(f"{device_str}网络连接错误: {str(e)}") + return False + + except Exception as e: + error_msg = str(e) + # 检查是否是连接相关错误 + if any(phrase in error_msg.lower() for phrase in [ + "10054", "10061", "connection", "connect", "refused", "urllib3" + ]): + logging.debug(f"{device_str}连接相关异常: {error_msg[:100]}") + return False + else: + logging.debug(f"{device_str}检查会话时出现其他异常: {error_msg[:100]}") + return True # 对于真正的未知异常,保守返回True + + +def reconnect_driver(device_id, old_driver=None, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"): + """ + 重新连接Appium驱动,不重新启动应用 + + 参数: + device_id: 设备ID + old_driver: 旧的WebDriver实例(可选) + app_package: 应用包名 + app_activity: 应用启动Activity + + 返回: + (driver, wait): 新的WebDriver和WebDriverWait实例 + """ + # 使用传入的device_id或从全局变量获取 + if not device_id: + device_id = global_variable.GLOBAL_DEVICE_ID + + # 修复device_id参数类型问题并使用全局设备ID作为备用 + actual_device_id = device_id + + # 检查device_id是否为有效的字符串格式 + if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)): + # 尝试从old_driver获取设备ID + if old_driver and hasattr(old_driver, 'capabilities'): + capability_device_id = old_driver.capabilities.get('udid') + if capability_device_id: + actual_device_id = capability_device_id + logging.warning(f"检测到device_id参数无效,已从old_driver中提取设备ID: {actual_device_id}") + + # 如果仍然没有有效的设备ID,使用全局变量 + if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)): + actual_device_id = global_variable.GLOBAL_DEVICE_ID + logging.warning(f"无法获取有效设备ID,使用全局变量GLOBAL_DEVICE_ID: {actual_device_id}") + + device_id = actual_device_id # 使用修正后的设备ID + logging.info(f"设备 {device_id} 开始重新连接驱动(不重启应用)") + + # # 首先安全关闭旧驱动 + # if old_driver: + # safe_quit_driver(old_driver, device_id) + + max_reconnect_attempts = 3 + reconnect_delay = 5 # 秒 + + for attempt in range(max_reconnect_attempts): + try: + logging.info(f"设备 {device_id} 第{attempt + 1}次尝试重新连接") + + # 确保Appium服务器运行 + if not ensure_appium_server_running(): + logging.warning(f"设备 {device_id} Appium服务器未运行,尝试启动") + time.sleep(reconnect_delay) + continue + + # 创建并配置Appium选项 - 重点:设置 autoLaunch=False 不自动启动应用 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = device_id + options.app_package = app_package + options.app_activity = app_activity + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 3600 + options.udid = device_id + + # 关键设置:不自动启动应用 + options.set_capability('autoLaunch', False) + options.set_capability('skipUnlock', True) + options.set_capability('skipServerInstallation', True) + options.set_capability('skipDeviceInitialization', True) + + # 增加uiautomator2服务器启动超时时间 + options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒 + # 增加连接超时设置 + options.set_capability('connection_timeout', 120000) # 120秒 + + # 连接Appium服务器 + driver_urls = [ + "http://127.0.0.1:4723/wd/hub", # 标准路径 + "http://127.0.0.1:4723", # 简化路径 + "http://localhost:4723/wd/hub", # localhost + ] + + driver = None + last_exception = None + + # 尝试多个URL + for driver_url in driver_urls: + try: + logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + driver = webdriver.Remote(driver_url, options=options) + logging.info(f"设备 {device_id} Appium服务器连接成功: {driver_url}") + break # 连接成功,跳出循环 + except Exception as e: + last_exception = e + logging.warning(f"设备 {device_id} 连接失败 {driver_url}: {str(e)[:100]}") + continue + + # 检查是否连接成功 + if not driver: + logging.error(f"设备 {device_id} 所有Appium服务器地址尝试失败") + logging.error(f"最后错误: {str(last_exception)}") + raise Exception(f"设备 {device_id} 无法连接到Appium服务器: {str(last_exception)}") + + + # 初始化等待对象 + wait = WebDriverWait(driver, 20) + logging.info(f"设备 {device_id} WebDriverWait初始化成功") + + # 不启动应用,直接附加到当前运行的应用 + try: + # 获取当前运行的应用 + current_package = driver.current_package + logging.info(f"设备 {device_id} 当前运行的应用: {current_package}") + + # 如果当前运行的不是目标应用,尝试切换到目标应用 + if current_package != app_package: + logging.info(f"设备 {device_id} 当前应用不是目标应用,尝试启动目标应用") + launch_app_manually(driver, app_package, app_activity) + else: + logging.info(f"设备 {device_id} 已成功连接到运行中的目标应用") + except Exception as attach_error: + logging.warning(f"设备 {device_id} 获取当前应用信息失败: {str(attach_error)}") + # 即使获取当前应用失败,也继续使用连接 + + # 验证新会话是否有效 + if check_session_valid(driver, device_id): + logging.info(f"设备 {device_id} 重新连接成功") + return driver, wait + else: + logging.warning(f"设备 {device_id} 新创建的会话无效,将重试") + safe_quit_driver(driver, device_id) + + except Exception as e: + logging.error(f"设备 {device_id} 第{attempt + 1}次重新连接失败: {str(e)}") + if attempt < max_reconnect_attempts - 1: + wait_time = reconnect_delay * (attempt + 1) + logging.info(f"设备 {device_id} 将在{wait_time}秒后重试重新连接") + time.sleep(wait_time) + else: + logging.error(f"设备 {device_id} 所有重新连接尝试均失败") + # 首先安全关闭旧驱动 + if old_driver: + safe_quit_driver(old_driver, device_id) + raise + + # 所有尝试都失败 + raise Exception(f"设备 {device_id} 重新连接失败,已尝试{max_reconnect_attempts}次") + + + +# def ensure_appium_server_running(port=4723): +# """使用完整的环境变量启动Appium""" +# try: +# # 获取当前用户的环境变量 +# env = os.environ.copy() + +# # 添加常见的Node.js路径(macOS常见问题) +# additional_paths = [ +# "/usr/local/bin", +# "/opt/homebrew/bin", # Apple Silicon Mac +# "/usr/bin", +# "/bin", +# os.path.expanduser("~/.npm-global/bin"), +# os.path.expanduser("~/node_modules/.bin"), +# # Android基础路径 +# "/Users/{}/Library/Android/sdk/platform-tools".format(os.getenv('USER')), +# ] + +# # 更新PATH环境变量 +# current_path = env.get('PATH', '') +# new_path = current_path + os.pathsep + os.pathsep.join(additional_paths) +# env['PATH'] = new_path + +# # 构建启动命令 +# appium_cmd = f"appium -p {port} --log-level error" + +# # 使用完整环境启动 +# process = subprocess.Popen( +# appium_cmd, +# shell=True, +# env=env, +# stdout=subprocess.PIPE, +# stderr=subprocess.PIPE, +# text=True +# ) + +# logging.info(f"Appium启动进程已创建,PID: {process.pid}") +# return wait_for_appium_start(port) + +# except Exception as e: +# logging.error(f"使用完整环境启动Appium时出错: {str(e)}") +# return False + +def is_port_in_use(port): + """检查端口是否被占用""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(('127.0.0.1', port)) == 0 + +def kill_system_process(process_name): + """杀掉系统进程""" + try: + # Windows + subprocess.run(f"taskkill /F /IM {process_name}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + except Exception: + pass + +def start_appium_server(port=4723): + """启动 Appium 服务,强制指定路径兼容性""" + # 1. 先尝试清理可能占用的 node 进程 + if is_port_in_use(port): + logging.warning(f"端口 {port} 被占用,尝试清理 node.exe...") + kill_system_process("node.exe") + time.sleep(2) + + # 2. 构造启动命令 + # 注意:这里增加了 --base-path /wd/hub 解决 404 问题 + # --allow-cors 允许跨域,有时候能解决连接问题 + appium_cmd = f"appium -p {port} --base-path /wd/hub --allow-cors" + + logging.info(f"正在启动 Appium: {appium_cmd}") + try: + # 使用 shell=True 在 Windows 上更稳定 + # creationflags=subprocess.CREATE_NEW_CONSOLE 可以让它在后台运行不弹出窗口 + subprocess.Popen(appium_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logging.info("Appium 启动命令已发送,等待服务就绪...") + except Exception as e: + logging.error(f"启动 Appium 进程失败: {e}") + +def check_server_status(port): + """检测服务器状态,兼容 Appium 1.x 和 2.x 路径""" + base_url = f"http://127.0.0.1:{port}" + check_paths = ["/wd/hub/status", "/status"] # 优先检查 /wd/hub + + for path in check_paths: + try: + url = f"{base_url}{path}" + response = requests.get(url, timeout=1) + if response.status_code == 200: + return True + except: + pass + return False + +# def ensure_appium_server_running(port=4723): +# """确保 Appium 服务器正在运行,如果没运行则启动它""" + +# # 1. 第一次快速检测 +# if check_server_status(port): +# logging.info(f"Appium 服务已在端口 {port} 运行") +# return True + +# # 2. 如果没运行,启动它 +# logging.warning(f"Appium 未在端口 {port} 运行,准备启动...") +# start_appium_server(port) + +# # 3. 循环等待启动成功(最多等待 20 秒) +# max_retries = 20 +# for i in range(max_retries): +# if check_server_status(port): +# logging.info("Appium 服务启动成功并已就绪!") +# return True + +# time.sleep(1) +# if i % 5 == 0: +# logging.info(f"等待 Appium 启动中... ({i}/{max_retries})") + +# logging.error("Appium 服务启动超时!请检查 appium 命令是否在命令行可直接运行。") +# return False + +def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool: + """ + 修复版:为 Appium 授予权限(使用正确的方法) + """ + logging.info(f"设备 {device_id}:开始设置Appium权限") + + # 1. 使用系统设置命令(替代原来的pm grant尝试) + logging.info("使用系统设置命令...") + system_commands = [ + ["adb", "-s", device_id, "shell", "settings", "put", "global", "window_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "transition_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "animator_duration_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "system", "screen_off_timeout", "86400000"], + ] + + success_count = 0 + for cmd in system_commands: + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功: {' '.join(cmd[3:])}") + else: + logging.warning(f" 失败: {' '.join(cmd[3:])}") + except: + logging.warning(f" 异常: {' '.join(cmd[3:])}") + + # 2. 授予可自动授予的权限 + logging.info("授予基础权限...") + grantable = [ + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.ACCESS_WIFI_STATE", + ] + + for perm in grantable: + cmd = ["adb", "-s", device_id, "shell", "pm", "grant", "io.appium.settings", perm] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功授予: {perm.split('.')[-1]}") + else: + logging.debug(f" 跳过: {perm.split('.')[-1]}") + + # 3. 返回结果 + logging.info(f"设置完成,成功项数: {success_count}") + + if require_all: + return success_count == (len(system_commands) + len(grantable)) + else: + return success_count > 0 # 只要有成功项就返回True + +def ensure_appium_server_running(port=4723): + """确保 Appium 服务器正在运行,如果没运行则启动它""" + + # 1. 第一次快速检测 + if check_server_status(port): + logging.info(f"Appium 服务已在端口 {port} 运行") + return True + + # 2. 如果没运行,启动它 + logging.warning(f"Appium 未在端口 {port} 运行,准备启动...") + start_appium_server(port) + + # 3. 循环等待启动成功(最多等待 20 秒) + max_retries = 20 + for i in range(max_retries): + if check_server_status(port): + logging.info("Appium 服务启动成功并已就绪!") + return True + + time.sleep(1) + if i % 5 == 0: + logging.info(f"等待 Appium 启动中... ({i}/{max_retries})") + + logging.error("Appium 服务启动超时!请检查 appium 命令是否在命令行可直接运行。") + return False + + +# 禁用requests的警告(访问本地接口无需SSL验证,避免控制台刷屏) +requests.packages.urllib3.disable_warnings() + +def check_appium_server_status(port=4723, timeout=30): + """ + 检测指定端口的Appium服务是否真正启动并可用 + :param port: Appium服务端口 + :param timeout: 最大等待时间(秒) + :return: 服务就绪返回True,超时/失败返回False + """ + # Appium官方状态查询接口 + check_url = f"http://localhost:{port}/wd/hub/status" + # 检测开始时间 + start_check_time = time.time() + logging.info(f"开始检测Appium服务是否就绪,端口:{port},最大等待{timeout}秒") + + while time.time() - start_check_time < timeout: + try: + # 发送HTTP请求,超时1秒(避免单次检测卡太久) + response = requests.get( + url=check_url, + timeout=1, + verify=False # 本地接口,禁用SSL验证 + ) + # 接口返回200(HTTP成功状态码),且JSON中status=0(Appium服务正常) + if response.status_code == 200 and response.json().get("status") == 0: + logging.info(f"Appium服务检测成功,端口{port}已就绪") + return True + except Exception as e: + # 捕获所有异常(连接拒绝、超时、JSON解析失败等),说明服务未就绪 + logging.debug(f"本次检测Appium服务未就绪:{str(e)}") # 调试日志,不刷屏 + + # 检测失败,休眠1秒后重试 + time.sleep(1) + + # 循环结束→超时 + logging.error(f"检测超时!{timeout}秒内Appium服务端口{port}仍未就绪") + return False + +def safe_quit_driver(driver, device_id=None): + """ + 安全关闭驱动的全局函数 + + 参数: + driver: WebDriver实例 + device_id: 设备ID(可选) + """ + if device_id is None: + device_id = global_variable.GLOBAL_DEVICE_ID + device_str = f"设备 {device_id} " if device_id else "" + logging.info(f"{device_str}开始关闭驱动") + + if not driver: + logging.info(f"{device_str}没有可关闭的驱动实例") + return + + # 检查driver是否为WebDriver实例或是否有quit方法 + if not hasattr(driver, 'quit'): + logging.warning(f"{device_str}驱动对象类型无效,不具有quit方法: {type(driver).__name__}") + return + + max_quit_attempts = 3 + for attempt in range(max_quit_attempts): + try: + logging.info(f"{device_str}尝试关闭驱动 (尝试 {attempt + 1}/{max_quit_attempts})") + driver.quit() + logging.info(f"{device_str}驱动已成功关闭") + return + except InvalidSessionIdException: + # 会话已经失效,不需要重试 + logging.info(f"{device_str}会话已经失效,无需关闭") + return + except Exception as e: + logging.error(f"{device_str}关闭驱动时出错 (尝试 {attempt + 1}/{max_quit_attempts}): {str(e)}") + if attempt < max_quit_attempts - 1: + # 等待一段时间后重试 + wait_time = 2 + logging.info(f"{device_str}将在 {wait_time} 秒后重试") + time.sleep(wait_time) + else: + logging.critical(f"{device_str}尝试多次关闭驱动失败,可能导致资源泄漏") + +def check_app_status(driver, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"): + """ + 检查应用状态(不跳转页面) + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + activity: 应用启动Activity,默认为".activity.LoginActivity" + + 返回: + bool: 应用是否正在运行 + """ + try: + device_id = None + if not device_id: + device_id = global_variable.GLOBAL_DEVICE_ID + # 尝试从driver获取设备ID + if driver and hasattr(driver, 'capabilities'): + device_id = driver.capabilities.get('udid') + device_str = f"设备 {device_id} " if device_id else "" + else: + device_str = "" + + logging.info(f"{device_str}检查应用状态: {package_name}") + + # 检查应用是否在运行 + if device_id: + # 使用ADB命令检查应用进程 + cmd = [ + "adb", "-s", device_id, + "shell", "ps", "|", "grep", package_name + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + if package_name in result.stdout: + logging.info(f"{device_str}应用正在运行中") + return True + else: + logging.info(f"{device_str}应用未运行") + return False + else: + # 尝试使用Appium API检查应用状态 + try: + if driver: + # 获取当前包名 + current_package = driver.current_package + if current_package == package_name: + logging.info(f"{device_str}应用正在运行中(当前包名: {current_package})") + return True + else: + logging.info(f"{device_str}应用未运行(当前包名: {current_package})") + return False + except: + logging.warning(f"{device_str}无法获取当前应用状态") + return False + + except Exception as e: + logging.error(f"检查应用状态时出错: {str(e)}") + logging.error(f"错误堆栈: {traceback.format_exc()}") + return False +def is_app_launched(driver, package_name="com.bjjw.cjgc"): + """ + 检查应用是否已启动 + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + + 返回: + bool: 如果应用已启动则返回True,否则返回False + """ + try: + # 通过检查当前活动的包名来确认应用是否已启动 + current_package = driver.current_package + return current_package == package_name + except Exception as e: + logging.error(f"检查应用启动状态时出错: {str(e)}") + return False + + +def launch_app_manually(driver, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"): + """ + 手动启动应用 + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + activity: 应用启动Activity,默认为".activity.LoginActivity" + """ + try: + device_id = global_variable.GLOBAL_DEVICE_ID + # 尝试从driver获取设备ID + if driver and hasattr(driver, 'capabilities'): + device_id = driver.capabilities.get('udid') + device_str = f"设备 {device_id} " if device_id else "" + else: + device_str = "" + + logging.info(f"{device_str}尝试手动启动应用: {package_name}/{activity}") + + # 首先尝试使用driver的execute_script方法启动应用 + try: + if driver: + driver.execute_script("mobile: startActivity", { + "intent": f"{package_name}/{activity}" + }) + logging.info(f"{device_str}已使用Appium startActivity命令启动应用") + except Exception as inner_e: + logging.warning(f"{device_str}使用Appium startActivity命令失败: {str(inner_e)},尝试使用ADB命令") + + # 如果device_id可用,使用ADB命令启动应用 + if device_id: + cmd = [ + "adb", "-s", device_id, + "shell", "am", "start", + "-n", f"{package_name}/{activity}" + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + + if result.returncode == 0: + logging.info(f"{device_str}已使用ADB命令成功启动应用") + else: + logging.error(f"{device_str}ADB启动应用失败: {result.stderr}") + else: + logging.warning("无法获取设备ID,无法使用ADB命令启动应用") + + # 等待应用启动 + time.sleep(5) + except Exception as e: + logging.error(f"手动启动应用时出错: {str(e)}") + logging.error(f"错误堆栈: {traceback.format_exc()}") + +# # 跳转到主页面并点击对应的导航菜单按钮 +# def go_main_click_tabber_button(driver, device_id, tabber_button_text): +# """ +# 跳转到主页面并点击对应的导航菜单按钮 + +# 参数: +# driver: WebDriver实例 +# device_id: 设备ID +# tabber_button_text: 导航菜单按钮的文本 + +# 返回: +# bool: 成功返回True,失败返回False +# """ +# try: +# # 检查当前是否已经在主页面 +# current_activity = driver.current_activity +# logging.info(f"设备 {device_id} 当前Activity: {current_activity}") + +# if ".activity.MainActivity" in current_activity: +# logging.info(f"设备 {device_id} 已在主页面") +# else: +# logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面") + +# max_back_presses = 10 # 最大返回键次数 +# back_press_count = 0 + +# while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses: +# try: +# # 点击返回按钮 +# # driver.press_keycode(4) # 4 是返回按钮的 keycode +# driver.back() +# back_press_count += 1 +# time.sleep(1) + +# # 更新当前Activity +# current_activity = driver.current_activity +# logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后,当前Activity: {current_activity}") + +# except Exception as inner_e: +# logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}") +# break + +# # 检查是否成功回到主页面 +# if ".activity.MainActivity" not in current_activity: +# logging.error(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}") +# return False + + +# try: + +# tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text) +# # 点击按钮 +# tabber_button.click() +# logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}") + +# # 等待页面加载 +# time.sleep(2) + +# return True + +# except TimeoutException: +# logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时") +# return False +# except Exception as e: +# logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}") +# return False + +# except Exception as e: +# logging.error(f"设备 {device_id} 跳转到主页面并点击菜单按钮时出错: {str(e)}") +# return False + +# def go_main_click_tabber_button(driver, device_id, tabber_button_text, max_retries=3): +# """ +# 跳转到主页面并点击对应的导航菜单按钮(带重试机制) + +# 参数: +# driver: WebDriver实例 +# device_id: 设备ID +# tabber_button_text: 导航菜单按钮的文本 +# max_retries: 最大重试次数 + +# 返回: +# bool: 成功返回True,失败返回False +# """ +# retry_count = 0 + +# while retry_count < max_retries: +# try: +# logging.info(f"设备 {device_id} 第 {retry_count + 1} 次尝试执行导航操作") + +# # 确保Appium服务器正在运行 +# if not ensure_appium_server_running(): +# logging.error(f"设备 {device_id} Appium服务器未运行,启动失败") +# retry_count += 1 +# if retry_count < max_retries: +# logging.info(f"设备 {device_id} 等待2秒后重试...") +# time.sleep(2) +# continue +# else: +# logging.error(f"设备 {device_id} 达到最大重试次数,Appium服务器启动失败") +# return False + +# # 检查会话有效性 +# if not check_session_valid(driver, device_id): +# logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") +# if not reconnect_driver(device_id, driver): +# logging.error(f"设备 {device_id} 驱动重连失败") +# retry_count += 1 +# if retry_count < max_retries: +# logging.info(f"设备 {device_id} 等待2秒后重试...") +# time.sleep(2) +# continue +# else: +# logging.error(f"设备 {device_id} 达到最大重试次数,驱动重连失败") +# return False + +# # 检查当前是否已经在主页面 +# current_activity = driver.current_activity +# logging.info(f"设备 {device_id} 当前Activity: {current_activity}") + +# if ".activity.MainActivity" in current_activity: +# logging.info(f"设备 {device_id} 已在主页面") +# else: +# logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面") + +# max_back_presses = 10 # 最大返回键次数 +# back_press_count = 0 + +# while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses: +# try: +# if not check_session_valid(driver, device_id): +# logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") +# if not reconnect_driver(device_id, driver): +# logging.error(f"设备 {device_id} 驱动重连失败") +# # 点击返回按钮 +# driver.back() +# back_press_count += 1 +# time.sleep(1) + +# # 更新当前Activity +# current_activity = driver.current_activity +# logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后,当前Activity: {current_activity}") + +# except Exception as inner_e: +# logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}") +# break + +# # 检查是否成功回到主页面 +# if ".activity.MainActivity" not in current_activity: +# logging.warning(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}") +# # 不立即返回,继续重试逻辑 +# retry_count += 1 +# if retry_count < max_retries: +# logging.info(f"设备 {device_id} 等待2秒后重试...") +# time.sleep(2) +# continue +# else: +# logging.error(f"设备 {device_id} 达到最大重试次数,无法回到主页面") +# return False + +# # 现在已经在主页面,点击指定的导航菜单按钮 +# # logging.info(f"设备 {device_id} 已在主页面,尝试点击导航菜单按钮: {tabber_button_text}") + +# try: +# tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text) +# # 点击按钮 +# tabber_button.click() +# logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}") + +# # 等待页面加载 +# time.sleep(2) + +# # 验证操作是否成功 +# # 可以添加一些验证逻辑,比如检查是否跳转到目标页面 +# new_activity = driver.current_activity +# logging.info(f"设备 {device_id} 点击后当前Activity: {new_activity}") + +# return True + +# except TimeoutException: +# logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时") +# except Exception as e: +# logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}") + +# # 检查会话有效性并尝试重连 +# if not check_session_valid(driver, device_id): +# logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") +# if reconnect_driver(device_id, driver): +# logging.info(f"设备 {device_id} 驱动重连成功,继续重试") +# # 重连成功后继续循环 +# retry_count += 1 +# if retry_count < max_retries: +# time.sleep(2) +# continue +# else: +# logging.error(f"设备 {device_id} 驱动重连失败") +# return False +# else: +# # 会话有效但点击失败,可能是页面元素问题 +# logging.warning(f"设备 {device_id} 会话有效但点击失败,可能是页面加载问题") + +# # 如果点击按钮失败,增加重试计数 +# retry_count += 1 +# if retry_count < max_retries: +# logging.info(f"设备 {device_id} 点击按钮失败,等待2秒后第 {retry_count + 1} 次重试...") +# time.sleep(2) +# else: +# logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败") +# return False + +# except Exception as e: +# logging.error(f"设备 {device_id} 第 {retry_count + 1} 次尝试时出错: {str(e)}") +# retry_count += 1 +# if retry_count < max_retries: +# logging.info(f"设备 {device_id} 等待2秒后重试...") +# time.sleep(2) +# else: +# logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败") +# return False + +# return False + +def go_main_click_tabber_button(driver, device_id, tabber_button_text, max_retries=3): + """ + 跳转到主页面并点击对应的导航菜单按钮(带重试机制) + + 参数: + driver: WebDriver实例 + device_id: 设备ID + tabber_button_text: 导航菜单按钮的文本 + max_retries: 最大重试次数 + + 返回: + bool: 成功返回True,失败返回False + """ + retry_count = 0 + + while retry_count < max_retries: + try: + logging.info(f"设备 {device_id} 第 {retry_count + 1} 次尝试执行导航操作") + + if not check_session_valid(driver, device_id): + logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") + try: + # 重新连接,获取新的driver + new_driver, _ = reconnect_driver(device_id, driver) + driver = new_driver # 更新driver引用 + logging.info(f"设备 {device_id} 驱动重连成功") + except Exception as e: + logging.error(f"设备 {device_id} 驱动重连失败: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + time.sleep(2) + continue + else: + return False + + # 检查当前是否已经在主页面 + current_activity = driver.current_activity + logging.info(f"设备 {device_id} 当前Activity: {current_activity}") + + if ".activity.MainActivity" in current_activity: + logging.info(f"设备 {device_id} 已在主页面") + else: + logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面") + + max_back_presses = 10 # 最大返回键次数 + back_press_count = 0 + + while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses: + try: + if not check_session_valid(driver, device_id): + logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(device_id, driver): + logging.error(f"设备 {device_id} 驱动重连失败") + # 点击返回按钮 + driver.back() + back_press_count += 1 + time.sleep(1) + + # 更新当前Activity + current_activity = driver.current_activity + logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后,当前Activity: {current_activity}") + + except Exception as inner_e: + logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}") + break + + # 检查是否成功回到主页面 + if ".activity.MainActivity" not in current_activity: + logging.warning(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}") + # 不立即返回,继续重试逻辑 + retry_count += 1 + if retry_count < max_retries: + logging.info(f"设备 {device_id} 等待2秒后重试...") + time.sleep(2) + continue + else: + logging.error(f"设备 {device_id} 达到最大重试次数,无法回到主页面") + return False + + # 现在已经在主页面,点击指定的导航菜单按钮 + # logging.info(f"设备 {device_id} 已在主页面,尝试点击导航菜单按钮: {tabber_button_text}") + + try: + tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text) + # 点击按钮 + tabber_button.click() + logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}") + + # 等待页面加载 + time.sleep(2) + + # 验证操作是否成功 + # 可以添加一些验证逻辑,比如检查是否跳转到目标页面 + new_activity = driver.current_activity + logging.info(f"设备 {device_id} 点击后当前Activity: {new_activity}") + + return True + + except TimeoutException: + logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时") + except Exception as e: + logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}") + + # 检查会话有效性并尝试重连 + if not check_session_valid(driver, device_id): + logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") + if reconnect_driver(device_id, driver): + logging.info(f"设备 {device_id} 驱动重连成功,继续重试") + # 重连成功后继续循环 + retry_count += 1 + if retry_count < max_retries: + time.sleep(2) + continue + else: + logging.error(f"设备 {device_id} 驱动重连失败") + return False + else: + # 会话有效但点击失败,可能是页面元素问题 + logging.warning(f"设备 {device_id} 会话有效但点击失败,可能是页面加载问题") + + # 如果点击按钮失败,增加重试计数 + retry_count += 1 + if retry_count < max_retries: + logging.info(f"设备 {device_id} 点击按钮失败,等待2秒后第 {retry_count + 1} 次重试...") + time.sleep(2) + else: + logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败") + return False + + except Exception as e: + logging.error(f"设备 {device_id} 第 {retry_count + 1} 次尝试时出错: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"设备 {device_id} 等待2秒后重试...") + time.sleep(2) + else: + logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败") + return False + + return False + + +def check_connection_error(exception): + """检查是否为连接拒绝错误""" + error_str = str(exception) + connection_errors = [ + '远程主机强迫关闭了一个现有的连接', + '由于目标计算机积极拒绝,无法连接', + 'ConnectionResetError', + 'NewConnectionError', + '10054', + '10061' + ] + return any(error in error_str for error in connection_errors) + +def restart_appium_server(port=4723): + """重启Appium服务器""" + try: + # 杀死可能存在的Appium进程 + subprocess.run(['taskkill', '/f', '/im', 'node.exe'], + capture_output=True, shell=True) + time.sleep(2) + + # 启动Appium服务器 + appium_command = f'appium -p {port}' + subprocess.Popen(appium_command, shell=True) + + # 等待Appium启动 + time.sleep(10) + return True + except Exception as e: + print(f"重启Appium服务器失败: {str(e)}") + return False + +def is_appium_running(port=4723): + """检查Appium服务器是否在运行""" + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + result = s.connect_ex(('127.0.0.1', port)) + return result == 0 + except: + return False + +def wait_for_appium_start(port, timeout=10): + """ + 检测指定端口的Appium服务是否真正启动并可用 + :param port: Appium服务端口 + :param timeout: 最大等待时间(秒) + :return: 服务就绪返回True,超时/失败返回False + """ + # Appium官方状态查询接口 + check_url = f"http://localhost:{port}/wd/hub/status" + # 检测开始时间 + start_check_time = time.time() + logging.info(f"开始检测Appium服务是否就绪,端口:{port},最大等待{timeout}秒") + + while time.time() - start_check_time < timeout: + try: + # 发送HTTP请求,超时1秒(避免单次检测卡太久) + response = requests.get( + url=check_url, + timeout=1, + verify=False # 本地接口,禁用SSL验证 + ) + # 接口返回200(HTTP成功状态码),且JSON中status=0(Appium服务正常) + if response.status_code == 200 and response.json().get("status") == 0: + logging.info(f"Appium服务检测成功,端口{port}已就绪") + return True + except Exception as e: + # 捕获所有异常(连接拒绝、超时、JSON解析失败等),说明服务未就绪 + logging.debug(f"本次检测Appium服务未就绪:{str(e)}") # 调试日志,不刷屏 + + # 检测失败,休眠1秒后重试 + time.sleep(1) + + # 循环结束→超时 + logging.error(f"检测超时!{timeout}秒内Appium服务端口{port}仍未就绪") + return False diff --git a/globals/ex_apis.py b/globals/ex_apis.py new file mode 100644 index 0000000..ff5632b --- /dev/null +++ b/globals/ex_apis.py @@ -0,0 +1,298 @@ +# external_apis.py +import requests +import json +import logging +import random +from typing import Dict, Tuple, Optional +import time + +class WeatherAPI: + def __init__(self): + self.logger = logging.getLogger(__name__) + # 使用腾讯天气API + self.base_url = "https://wis.qq.com/weather/common" + + def parse_city(self, city_string: str) -> Tuple[str, str, str]: + """ + 解析城市字符串,返回省份、城市、区县 + + 参数: + city_string: 完整的地址字符串 + + 返回: + (province, city, county) + """ + # 匹配省份或自治区 + province_regex = r"(.*?)(省|自治区)" + # 匹配城市或州 + city_regex = r"(.*?省|.*?自治区)(.*?市|.*?州)" + # 匹配区、县或镇 + county_regex = r"(.*?市|.*?州)(.*?)(区|县|镇)" + + province = "" + city = "" + county = "" + + import re + + # 先尝试匹配省份或自治区 + province_match = re.search(province_regex, city_string) + if province_match: + province = province_match.group(1).strip() + + # 然后尝试匹配城市或州 + city_match = re.search(city_regex, city_string) + if city_match: + city = city_match.group(2).strip() + else: + # 如果没有匹配到城市,则可能是直辖市或者直接是区/县 + city = city_string + + # 最后尝试匹配区、县或镇 + county_match = re.search(county_regex, city_string) + if county_match: + county = county_match.group(2).strip() + # 如果有区、县或镇,那么前面的城市部分需要重新解析 + if city_match: + city = city_match.group(2).strip() + + # 特殊情况处理,去除重复的省市名称 + if city and province and city.startswith(province): + city = city.replace(province, "").strip() + if county and city and county.startswith(city): + county = county.replace(city, "").strip() + + # 去除后缀 + city = city.rstrip('市州') + if county: + county = county.rstrip('区县镇') + + # self.logger.info(f"解析结果 - 省份: {province}, 城市: {city}, 区县: {county}") + return province, city, county + + def get_weather_by_qq_api(self, province: str, city: str, county: str) -> Optional[Dict]: + """ + 使用腾讯天气API获取天气信息 + + 参数: + province: 省份 + city: 城市 + county: 区县 + + 返回: + 天气信息字典 or None + """ + try: + params = { + 'source': 'pc', + 'weather_type': 'observe', + 'province': province, + 'city': city, + 'county': county + } + + response = requests.get(self.base_url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + + if data.get('status') == 200: + observe_data = data.get('data', {}).get('observe', {}) + + return { + 'weather': observe_data.get('weather', ''), + 'temperature': observe_data.get('degree', ''), + 'pressure': observe_data.get('pressure', '1013') + } + else: + self.logger.error(f"腾讯天气API错误: {data.get('message')}") + return None + + except Exception as e: + self.logger.error(f"腾讯天气API调用失败: {str(e)}") + return None + + def normalize_weather_text(self, weather_text: str) -> str: + """ + 将天气描述标准化为: 晴;阴;雨;雪;风;其他 + + 参数: + weather_text: 原始天气描述 + + 返回: + 标准化后的天气文本 + """ + if not weather_text: + return '其他' + + weather_text_lower = weather_text.lower() + + # 晴 + if any(word in weather_text_lower for word in ['晴', 'sunny', 'clear']): + return '晴' + # 阴 + elif any(word in weather_text_lower for word in ['阴', '多云', 'cloudy', 'overcast']): + return '阴' + # 雨 + elif any(word in weather_text_lower for word in ['雨', 'rain', 'drizzle', 'shower']): + return '雨' + # 雪 + elif any(word in weather_text_lower for word in ['雪', 'snow']): + return '雪' + # 风 + elif any(word in weather_text_lower for word in ['风', 'wind']): + return '风' + # 其他 + else: + return '其他' + + def adjust_pressure(self, pressure: float) -> float: + """ + 调整气压值:低于700时,填700-750之间随机一个;高于700就按实际情况填 + + 参数: + pressure: 原始气压值 + + 返回: + 调整后的气压值 + """ + try: + pressure_float = float(pressure) + if pressure_float < 700: + adjusted_pressure = random.randint(700, 750) + self.logger.info(f"气压值 {pressure_float} 低于700,调整为: {adjusted_pressure:.1f}") + return round(adjusted_pressure, 1) + else: + self.logger.info(f"使用实际气压值: {pressure_float}") + return round(pressure_float, 1) + except (ValueError, TypeError): + self.logger.warning(f"气压值格式错误: {pressure},使用默认值720") + return round(random.randint(700, 750), 1) + + def get_weather_by_address(self, address: str, max_retries: int = 2) -> Optional[Dict]: + """ + 根据地址获取天气信息 + + 参数: + address: 地址字符串 + max_retries: 最大重试次数 + + 返回: + { + 'weather': '晴/阴/雨/雪/风/其他', + 'temperature': 温度值, + 'pressure': 气压值 + } or None + """ + # self.logger.info(f"开始获取地址 '{address}' 的天气信息") + + # 首先解析地址 + province, city, county = self.parse_city(address) + + if not province and not city: + self.logger.error("无法解析地址") + return self.get_fallback_weather() + + # 获取天气信息 + weather_data = None + for attempt in range(max_retries): + try: + # self.logger.info(f"尝试获取天气信息 (第{attempt + 1}次)") + weather_data = self.get_weather_by_qq_api(province, city, county) + if weather_data: + break + time.sleep(1) # 短暂延迟后重试 + except Exception as e: + self.logger.warning(f"第{attempt + 1}次尝试失败: {str(e)}") + time.sleep(1) + + if not weather_data: + self.logger.warning("获取天气信息失败,使用备用数据") + return self.get_fallback_weather() + + # 处理天气数据 + try: + # 标准化天气文本 + normalized_weather = self.normalize_weather_text(weather_data['weather']) + + # 调整气压值 + adjusted_pressure = self.adjust_pressure(weather_data['pressure']) + + # 处理温度 + temperature = float(weather_data['temperature']) + + result = { + 'weather': normalized_weather, + 'temperature': round(temperature, 1), + 'pressure': adjusted_pressure + } + + self.logger.info(f"成功获取天气信息: {result}") + return result + + except Exception as e: + self.logger.error(f"处理天气数据时出错: {str(e)}") + return self.get_fallback_weather() + + def get_fallback_weather(self) -> Dict: + """ + 获取备用天气数据(当所有API都失败时使用) + + 返回: + 默认天气数据 + """ + self.logger.info("使用备用天气数据") + return { + 'weather': '阴', + 'temperature': round(random.randint(15, 30), 1), + 'pressure': round(random.randint(700, 750), 1) + } + + def get_weather_simple(self, address: str) -> Tuple[str, float, float]: + """ + 简化接口:直接返回天气、温度、气压 + + 参数: + address: 地址字符串 + + 返回: + (weather, temperature, pressure) + """ + weather_data = self.get_weather_by_address(address) + if weather_data: + return weather_data['weather'], weather_data['temperature'], weather_data['pressure'] + else: + fallback = self.get_fallback_weather() + return fallback['weather'], fallback['temperature'], fallback['pressure'] + +# 创建全局实例 +weather_api = WeatherAPI() + +# 直接可用的函数 +def get_weather_by_address(address: str) -> Optional[Dict]: + """ + 根据地址获取天气信息(直接调用函数) + + 参数: + address: 地址字符串 + + 返回: + { + 'weather': '晴/阴/雨/雪/风/其他', + 'temperature': 温度值, + 'pressure': 气压值 + } or None + """ + return weather_api.get_weather_by_address(address) + +def get_weather_simple(address: str) -> Tuple[str, float, float]: + """ + 简化接口:直接返回天气、温度、气压 + + 参数: + address: 地址字符串 + + 返回: + (weather, temperature, pressure) + """ + return weather_api.get_weather_simple(address) diff --git a/globals/global_variable.py b/globals/global_variable.py new file mode 100644 index 0000000..86549ce --- /dev/null +++ b/globals/global_variable.py @@ -0,0 +1,17 @@ +# 全局变量 +GLOBAL_DEVICE_ID = "" # 设备ID +GLOBAL_USERNAME = "czyuzongwen" # 用户名 +GLOBAL_CURRENT_PROJECT_NAME = "" # 当前测试项目名称 +GLOBAL_LINE_NUM = "" # 线路编码 +GLOBAL_BREAKPOINT_STATUS_CODES = [0,3] # 要获取的断点状态码列表 +GLOBAL_UPLOAD_BREAKPOINT_LIST = [] +GLOBAL_UPLOAD_BREAKPOINT_DICT = {} +GLOBAL_TESTED_BREAKPOINT_LIST = [] # 测量结束的断点列表 +LINE_TIME_MAPPING_DICT = {} # 存储所有线路编码和对应的时间的全局字典 +GLOBAL_BREAKPOINT_DICT = {} # 存储测量结束的断点名称和对应的线路编码的全局字典 +GLOBAL_NAME_TO_ID_MAP = {} # 存储所有数据员姓名和对应的身份证号的全局字典 +GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = [] # 上传成功的`断点列表 + + + + diff --git a/globals/ids.py b/globals/ids.py new file mode 100644 index 0000000..33cbb60 --- /dev/null +++ b/globals/ids.py @@ -0,0 +1,59 @@ +# ids.py +# 登录界面 +LOGIN_USERNAME = "com.bjjw.cjgc:id/et_user_name" +LOGIN_PASSWORD = "com.bjjw.cjgc:id/et_user_psw" +LOGIN_BTN = "com.bjjw.cjgc:id/btn_login" + +# 更新相关 +UPDATE_WORK_BASE = "com.bjjw.cjgc:id/btn_update_basepoint" +UPDATE_LEVEL_LINE = "com.bjjw.cjgc:id/btn_update_line" +UPDATE_LEVEL_LINE_CONFIRM = "com.bjjw.cjgc:id/commit" + +# 弹窗 & 加载 +ALERT_DIALOG = "android:id/content" +LOADING_DIALOG = "android:id/custom" + +# 底部导航栏 +DOWNLOAD_TABBAR_ID = "com.bjjw.cjgc:id/img_1_layout" +MEASURE_TABBAR_ID = "com.bjjw.cjgc:id/img_3_layout" + +# 测量相关 +MEASURE_BTN_ID = "com.bjjw.cjgc:id/select_point_update_tip_tv" +MEASURE_LIST_ID = "com.bjjw.cjgc:id/line_list" +MEASURE_LISTVIEW_ID = "com.bjjw.cjgc:id/itemContainer" +MEASURE_NAME_TEXT_ID = "com.bjjw.cjgc:id/title" +MEASURE_NAME_ID = "com.bjjw.cjgc:id/sectName" +MEASURE_BACK_ID = "com.bjjw.cjgc:id/btn_back" +MEASURE_BACK_ID_2 = "com.bjjw.cjgc:id/stop_measure_btn" + +# 天气、观测类型 +MEASURE_TITLE_ID = "com.bjjw.cjgc:id/title_bar" +MEASURE_WEATHER_ID = "com.bjjw.cjgc:id/point_list_weather_sp" +MEASURE_TYPE_ID = "com.bjjw.cjgc:id/point_list_mtype_sp" +MEASURE_SELECT_ID = "android:id/select_dialog_listview" +SELECT_DIALOG_TEXT1_ID = "android:id/text1" +MEASURE_PRESSURE_ID = "com.bjjw.cjgc:id/point_list_barometric_et" +MEASURE_TEMPERATURE_ID = "com.bjjw.cjgc:id/point_list_temperature_et" +MEASURE_SAVE_ID = "com.bjjw.cjgc:id/select_point_order_save_btn" + +# 日期选择器 +DATE_START = "com.bjjw.cjgc:id/date" +DATE_END = "com.bjjw.cjgc:id/date_end" +SCRCOLL_YEAR = "com.bjjw.cjgc:id/wheelView1" +SCRCOLL_MONTH = "com.bjjw.cjgc:id/wheelView2" +SCRCOLL_DAY = "com.bjjw.cjgc:id/wheelView3" +SCRCOLL_CONFIRM = "com.bjjw.cjgc:id/okBtn" +SCRCOLL_CANCEL = "com.bjjw.cjgc:id/cancelBtn" + +# 其他 +CONNECT_LEVEL_METER = "com.bjjw.cjgc:id/point_conn_level_btn" +PINGCHAR_PROCESS = "com.bjjw.cjgc:id/point_measure_btn" +LEVEL_METER_MANAGER = "com.bjjw.cjgc:id/btn_back" +LEVEL_METER_MANAGER_LIST = "com.bjjw.cjgc:id/total_station_expandlist" +START_MEASURE = "com.bjjw.cjgc:id/btn_control_begin_or_end" +ALTER_MEASURE = "com.bjjw.cjgc:id/order_title" +MATCH_VISIABLE = "com.bjjw.cjgc:id/title_paired_devices" +BLUETOOTH_PAIR = "com.bjjw.cjgc:id/paired_devices" +REPID_MEASURE = "com.bjjw.cjgc:id/measure_remeasure_all_btn" +ONE = "com.bjjw.cjgc:id/auto_measure_all_station_text" + diff --git a/page_objects/__pycache__/download_tabbar_page.cpython-312.pyc b/page_objects/__pycache__/download_tabbar_page.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..11c1026a6f5a80fba516ce258fc55e5e0db14ec5 GIT binary patch literal 20511 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 literal 0 HcmV?d00001 diff --git a/page_objects/__pycache__/login_page.cpython-312.pyc b/page_objects/__pycache__/login_page.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de856eb1c95130b856e961d876ecde659ba10c94 GIT binary patch literal 7244 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 literal 0 HcmV?d00001 diff --git a/page_objects/__pycache__/more_download_page.cpython-312.pyc b/page_objects/__pycache__/more_download_page.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10752fb2790beb5069e8cbc90a01a0b9282c987a GIT binary patch literal 15172 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 IhCGq~1w`xv?f?J) literal 0 HcmV?d00001 diff --git a/page_objects/__pycache__/screenshot_page.cpython-312.pyc b/page_objects/__pycache__/screenshot_page.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a3d61761321e8fdf7aeb05b4da0f133f00c809d GIT binary patch literal 77843 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 DS5}=5 literal 0 HcmV?d00001 diff --git a/page_objects/__pycache__/section_mileage_config_page.cpython-312.pyc b/page_objects/__pycache__/section_mileage_config_page.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69e759bf183e39d3955fc32f23b106890d912214 GIT binary patch literal 46087 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 Hq}l%)vuEo1 literal 0 HcmV?d00001 diff --git a/page_objects/__pycache__/upload_config_page.cpython-312.pyc b/page_objects/__pycache__/upload_config_page.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..676fc3baf475f15ca487b5383ff5ac6ec851a8b8 GIT binary patch literal 76996 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 bool: + """ + 向指定TCP端口发送指令 + + 参数: + command: 要发送的指令字符串 + host: 目标主机地址(默认127.0.0.1) + port: 目标端口(默认8888) + encoding: 字符串编码格式(默认utf-8) + + 返回: + 发送成功返回True,失败返回False + """ + # 创建TCP socket并自动关闭(with语句确保资源释放) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + # 连接服务器(超时时间5秒,避免无限阻塞) + sock.settimeout(5.0) + sock.connect((host, port)) + + # 发送指令(转换为字节流) + sock.sendall(command.encode(encoding)) + print(f"指令 '{command}' 发送成功") + return True + + except ConnectionRefusedError: + print(f"连接失败:{host}:{port} 未监听或不可达") + except socket.timeout: + print(f"连接超时:超过5秒未连接到 {host}:{port}") + except UnicodeEncodeError: + print(f"编码失败:指令包含{encoding}无法编码的字符") + except Exception as e: + print(f"发送失败:{str(e)}") + + return False + + +# 使用示例 +if __name__ == "__main__": + # 发送StartConnect指令 + send_tcp_command("StartConnect") + + # 也可以发送其他指令,例如: + # send_tcp_command("StopConnect") \ No newline at end of file diff --git a/page_objects/download_tabbar_page.py b/page_objects/download_tabbar_page.py new file mode 100644 index 0000000..ad5b059 --- /dev/null +++ b/page_objects/download_tabbar_page.py @@ -0,0 +1,405 @@ +# 更新基站页面操作 +# page_objects/download_tabbar_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException +import logging +import time +from datetime import datetime + +import globals.ids as ids # 导入元素ID +import globals.global_variable as global_variable # 导入全局变量 +from globals.driver_utils import check_session_valid, reconnect_driver + +class DownloadTabbarPage: + def __init__(self, driver, wait, device_id): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + # 添加默认的目标日期值 + self.target_year = 2022 + self.target_month = 9 + self.target_day = 22 + + def is_download_tabbar_visible(self): + """检查下载标签栏是否可见""" + try: + return self.driver.find_element(AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID).is_displayed() + except NoSuchElementException: + self.logger.warning("下载标签栏元素未找到") + return False + except Exception as e: + self.logger.error(f"检查下载标签栏可见性时发生意外错误: {str(e)}") + return False + + def click_download_tabbar(self): + """点击下载标签栏""" + try: + download_tab = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID)) + ) + download_tab.click() + self.logger.info("已点击下载标签栏") + + # 使用显式等待替代固定等待 + self.wait.until( + lambda driver: self.is_download_tabbar_visible() + ) + return True + except TimeoutException: + self.logger.error("等待下载标签栏可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载标签栏时出错: {str(e)}") + return False + + def update_work_base(self): + """更新工作基点""" + try: + # 点击更新工作基点 + update_work_base = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_WORK_BASE)) + ) + update_work_base.click() + self.logger.info("已点击更新工作基点") + + # 等待更新完成 - 可以添加更具体的等待条件 + # 例如等待某个进度条消失或成功提示出现 + time.sleep(2) # 暂时保留,但建议替换为显式等待 + return True + except TimeoutException: + self.logger.error("等待更新工作基点按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"更新工作基点时出错: {str(e)}") + return False + + def _get_current_date(self): + """获取当前开始日期控件的日期值,支持多种格式解析""" + try: + date_element = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.DATE_START)) + ) + date_text = date_element.text.strip() + self.logger.info(f"获取到当前开始日期: {date_text}") + + # 尝试多种日期格式解析 + date_formats = [ + "%Y-%m-%d", # 匹配 '2025-08-12' 格式 + "%Y年%m月%d日", # 匹配 '2025年08月12日' 格式 + "%Y/%m/%d" # 可选:添加其他可能的格式 + ] + + for fmt in date_formats: + try: + return datetime.strptime(date_text, fmt) + except ValueError: + continue # 尝试下一种格式 + + # 如果所有格式都匹配失败 + self.logger.error(f"日期格式解析错误: 无法识别的格式,日期文本: {date_text}") + return None + + except TimeoutException: + self.logger.error("获取当前日期超时") + return None + except Exception as e: + self.logger.error(f"获取当前日期失败: {str(e)}") + return None + + def update_level_line(self): + """更新水准线路,修改为设置2022年9月22日""" + try: + # 点击更新水准线路 + update_level_line = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE)) + ) + update_level_line.click() + self.logger.info("已点击更新水准线路") + + # 获取原始开始日期 + original_date = self._get_current_date() + if not original_date: + self.logger.error("无法获取原始开始日期,更新水准线路失败") + return False + + # 点击开始日期 + date_start = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.DATE_START)) + ) + date_start.click() + self.logger.info("已点击开始日期控件") + + # # 处理时间选择器,设置为2022年9月22日 + # if not self.handle_time_selector(2022, 9, 22, original_date): + # self.logger.error("处理时间选择失败") + # return False + # 处理时间选择器,滚动选择年份 + if not self.handle_year_selector(): + self.logger.error("时间选择器滑动年份,处理时间失败") + return False + + return True + except TimeoutException: + self.logger.error("等待更新水准线路按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"更新水准线路时出错: {str(e)}") + return False + + def _swipe_year_wheel(self): + """滑动年份选择器的滚轮""" + try: + # 获取年份选择器滚轮元素 + year_wheel = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/wheelView1") + + # 获取滚轮的位置和尺寸 + location = year_wheel.location + size = year_wheel.size + + # 计算滚轮中心点坐标 + center_x = location['x'] + size['width'] // 2 + center_y = location['y'] + size['height'] // 2 + + # 计算滑动距离 - 滚轮高度的1/5 + swipe_distance = size['height'] // 5 + + # 执行滑动操作 - 从中心向上滑动1/5高度 + self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500) + + self.logger.info("已滑动年份选择器") + return True + + except Exception as e: + self.logger.error(f"滑动年份选择器时出错: {str(e)}") + return False + def _scroll_to_value(self, picker_id, target_value, original_value, max_attempts=20): + """滚动选择器到目标值,基于原始值计算滚动次数""" + try: + # 计算需要滚动的次数(绝对值) + scroll_count = abs(int(target_value) - int(original_value)) + self.logger.info(f"需要滚动{scroll_count}次将{picker_id}从{original_value}调整到{target_value}") + + # 确定滚动方向 + direction = "down" if int(target_value) > int(original_value) else "up" + + # 获取选择器元素 + picker = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, picker_id)) + ) + + # 计算滚动坐标 + x = picker.location['x'] + picker.size['width'] // 2 + self.logger.info(f"水平位置x为{x}") + y_center = picker.location['y'] + picker.size['height'] // 2 + self.logger.info(f"垂直位置中点y_center为{y_center}") + # start_y = y_center if direction == "down" else picker.location['y'] + # end_y = picker.location['y'] if direction == "down" else y_center + # 关键修改:计算选择器高度的五分之一(滑动距离) + height_fifth = picker.size['height'] // 5 # 1/5高度 + + # 根据方向计算起点和终点,确保滑动距离为 height_fifth + if direction == "down": + # 向下滚动:从中心点向上滑动1/5高度 + start_y = y_center + end_y = y_center - height_fifth # 终点 = 中心点 - 1/5高度 + self.logger.info(f"down垂直开始位置start_y为{y_center},垂直结束位置end_y为{end_y}") + + else: + # 向上滚动:从中心点向下滑动1/5高度 + start_y = y_center + end_y = y_center + height_fifth # 终点 = 中心点 + 1/5高度 + self.logger.info(f"up垂直开始位置start_y为{y_center},垂直结束位置end_y为{end_y}") + # 执行滚动操作 + for _ in range(scroll_count): + self.driver.swipe(x, start_y, x, end_y, 500) + time.sleep(0.5) # 等待滚动稳定 + return True # 循环scroll_count次后直接返回 + # # 验证当前值 + # current_value = picker.text + # if current_value == str(target_value): + # self.logger.info(f"{picker_id}已达到目标值: {target_value}") + # return True + + # 最终验证 + # final_value = picker.text + # if final_value == str(target_value): + # self.logger.info(f"{picker_id}已达到目标值: {target_value}") + # return True + # else: + # self.logger.error(f"{picker_id}滚动{scroll_count}次后未达到目标值,当前值: {final_value}") + # return False + + except StaleElementReferenceException: + self.logger.warning("元素状态已过期,重新获取") + return False + except Exception as e: + self.logger.error(f"滚动选择器出错: {str(e)}") + return False + + def handle_year_selector(self): + """处理时间选择器,滚动选择年份""" + try: + # 等待时间选择器出现 + self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.ALERT_DIALOG)) + ) + self.logger.info("时间选择对话框已出现") + + # 滚动选择年份 + if not self._swipe_year_wheel(): + self.logger.error("滚动选择年份失败") + return False + + # 点击确认按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.SCRCOLL_CONFIRM)) # 日期选择器确认按钮 + ) + confirm_btn.click() + self.logger.info("已确认时间选择") + + # 点击对话框确认按钮(UPDATE_LEVEL_LINE_CONFIRM) + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE_CONFIRM)) # 选择日期对话框确认按钮 + ) + confirm_btn.click() + self.logger.info("已点击对话框确认按钮commit") + + # 等待加载对话框出现 + custom_dialog = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG)) + ) + self.logger.info("检测到loading对话框出现") + + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {self.device_id} 驱动重连失败") + + # 新增:等待加载对话框消失(表示更新完成) + WebDriverWait(self.driver, 300).until( + EC.invisibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG)) + ) + self.logger.info("loading对话框已消失,更新完成") + + return True + + except TimeoutException as e: + # 明确超时发生在哪个环节 + self.logger.error("处理时间选择时超时:可能是等待对话框出现、日期选择器元素或确认按钮超时", exc_info=True) + return False + except Exception as e: + # 细分不同环节的异常 + self.logger.error(f"处理时间选择对话框时出错:可能是日期滚动、选择器确认或对话框确认环节失败 - {str(e)}", exc_info=True) + return False + + def handle_time_selector(self, target_year, target_month, target_day, original_date=None): + """处理时间选择器,选择起始时间并确认""" + self.logger.info(f"传入handle_time_selector的初始日期: {original_date}") + try: + # 等待时间选择器出现 + self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.ALERT_DIALOG)) + ) + self.logger.info("时间选择对话框已出现") + + # 如果没有提供原始日期,使用当前日期控件的值 + if not original_date: + original_date = self._get_current_date() + if not original_date: + self.logger.error("无法获取原始日期,处理时间选择失败") + return False + + # 滚动选择年份 + if not self._scroll_to_value(ids.SCRCOLL_YEAR, target_year, original_date.year): + self.logger.error("滚动选择年份失败") + return False + + # 滚动选择月份 + if not self._scroll_to_value(ids.SCRCOLL_MONTH, target_month, original_date.month): + self.logger.error("滚动选择月份失败") + return False + + # 滚动选择日期 + if not self._scroll_to_value(ids.SCRCOLL_DAY, target_day, original_date.day): + self.logger.error("滚动选择日期失败") + return False + + # 点击确认按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.SCRCOLL_CONFIRM)) # 日期选择器确认按钮 + ) + confirm_btn.click() + self.logger.info("已确认时间选择") + + # 点击对话框确认按钮(UPDATE_LEVEL_LINE_CONFIRM) + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE_CONFIRM)) # 选择日期对话框确认按钮 + ) + confirm_btn.click() + self.logger.info("已点击对话框确认按钮commit") + + # 新增:等待加载对话框出现 + custom_dialog = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG)) + ) + self.logger.info("检测到loading对话框出现") + + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {self.device_id} 驱动重连失败") + + # 新增:等待加载对话框消失(表示更新完成) + WebDriverWait(self.driver, 300).until( + EC.invisibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG)) + ) + self.logger.info("loading对话框已消失,更新完成") + + '''点击commit确认按钮后,loading弹窗会出现,等待其加载完成后关闭 + 检测导航栏中的测量tabbar是否出现来确定是否返回True + ''' + # measure_tabbar_btn = self.wait.until( + # EC.visibility_of_element_located((AppiumBy.ID, ids.MEASURE_TABBAR_ID)) + # ) + # self.logger.info("检测测量tabbar按钮出现") + + return True + + except TimeoutException as e: + # 明确超时发生在哪个环节 + self.logger.error("处理时间选择时超时:可能是等待对话框出现、日期选择器元素或确认按钮超时", exc_info=True) + return False + except Exception as e: + # 细分不同环节的异常 + self.logger.error(f"处理时间选择对话框时出错:可能是日期滚动、选择器确认或对话框确认环节失败 - {str(e)}", exc_info=True) + return False + + def download_tabbar_page_manager(self): + """执行基础更新操作""" + try: + # 执行基础更新流程 + self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 开始执行更新流程") + + # 点击下载标签栏 + if not self.click_download_tabbar(): + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 点击下载标签栏失败") + return False + + # 更新工作基点 + if not self.update_work_base(): + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新工作基点失败") + return False + + # 更新水准线路 + if not self.update_level_line(): + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新水准线路失败") + return False + + self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新操作执行成功") + return True + except Exception as e: + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 执行更新操作时出错: {str(e)}") + return False \ No newline at end of file diff --git a/page_objects/login_page.py b/page_objects/login_page.py new file mode 100644 index 0000000..7c29a71 --- /dev/null +++ b/page_objects/login_page.py @@ -0,0 +1,179 @@ +# 登录页面操作 +# page_objects/login_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +import logging +import time +import subprocess + +import globals.ids as ids +import globals.global_variable as global_variable # 导入全局变量模块 +import globals.apis as apis + + + +class LoginPage: + def __init__(self, driver, wait): + self.driver = driver + self.wait = wait + + def navigate_to_login_page(self, driver, device_id): + """ + 补充的跳转页面函数:当设备处于未知状态时,尝试跳转到登录页面 + + 参数: + driver: 已初始化的Appium WebDriver对象 + device_id: 设备ID,用于日志记录 + """ + try: + target_package = 'com.bjjw.cjgc' + target_activity = '.activity.LoginActivity' + # 使用ADB命令启动Activity + try: + logging.info(f"尝试使用ADB命令启动LoginActivity: {target_package}/{target_activity}") + adb_command = f"adb -s {device_id} shell am start -n {target_package}/{target_activity}" + result = subprocess.run(adb_command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + logging.info(f"使用ADB命令启动LoginActivity成功") + time.sleep(2) # 等待Activity启动 + return True + else: + logging.warning(f"ADB命令执行失败: {result.stderr}") + except Exception as adb_error: + logging.warning(f"执行ADB命令时出错: {adb_error}") + except Exception as e: + logging.error(f"跳转到登录页面过程中发生未预期错误: {e}") + + # 所有尝试都失败 + return False + + + + def is_login_page(self): + """检查当前是否为登录页面""" + try: + return self.driver.find_element(AppiumBy.ID, ids.LOGIN_BTN).is_displayed() + except NoSuchElementException: + return False + + def login(self, username=None): + """执行登录操作""" + try: + logging.info("正在执行登录操作...") + + # # 获取文本框中已有的用户名 + # username_field = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME)) + # ) + # 获取用户名输入框 + username_field = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME)) + ) + # 填写用户名 + if username: + # 清空用户名输入框 + try: + username_field.clear() + except: + pass + # 填写传入的用户名 + username_field.send_keys(username) + existing_username = username + logging.info(f"已填写用户名: {username}") + else: + # 获取文本框中已有的用户名 + existing_username = username_field.text + # 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解) + if existing_username.strip(): # 去除空格后判断是否有有效内容 + logging.info(f"已获取文本框中的已有用户名: {existing_username}") + else: + logging.info("文本框中未检测到已有用户名(内容为空)") + + # 将用户名写入全局变量中 + global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值 + # global_variable.set_username(existing_username) + + + # # 读取文本框内已有的用户名(.text属性获取元素显示的文本内容) + # existing_username = username_field.text + # # 3. 将获取到的用户名写入全局变量中 + # # global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值 + # global_variable.set_username(existing_username) + + # # 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解) + # if existing_username.strip(): # 去除空格后判断是否有有效内容 + # logging.info(f"已获取文本框中的已有用户名: {existing_username}") + # else: + # logging.info("文本框中未检测到已有用户名(内容为空)") + + # 1. 定位密码输入框 + password_field = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_PASSWORD)) + ) + + # 2. 清空密码框(如果需要) + try: + password_field.clear() + # time.sleep(0.5) # 等待清除完成 + except: + # 如果clear方法不可用,尝试其他方式 + pass + + accounts = apis.get_accounts_from_server("68ef0e02b0138d25e2ac9918") + matches = [acc for acc in accounts if acc.get("username") == existing_username] + if matches: + password = matches[0].get("password") + + password_field.send_keys(password) + + # 4. 可选:隐藏键盘 + try: + self.driver.hide_keyboard() + except: + pass + + # 点击登录按钮 + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + login_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_BTN)) + ) + login_btn.click() + logging.info(f"已点击登录按钮 (尝试 {retry_count + 1}/{max_retries})") + + # 等待登录完成 + time.sleep(3) + + # 检查是否登录成功 + if self.is_login_successful(): + logging.info("登录成功") + return True + else: + logging.warning("登录后未检测到主页面元素,准备重试") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"等待2秒后重新尝试登录...") + time.sleep(2) + + logging.error(f"登录失败,已尝试 {max_retries} 次") + return False + + except Exception as e: + logging.error(f"登录过程中出错: {str(e)}") + return False + + + def is_login_successful(self): + """检查登录是否成功""" + try: + # 等待主页面元素出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID)) + ) + return True + except TimeoutException: + return False \ No newline at end of file diff --git a/page_objects/measure_tabbar_page.py b/page_objects/measure_tabbar_page.py new file mode 100644 index 0000000..fffeb51 --- /dev/null +++ b/page_objects/measure_tabbar_page.py @@ -0,0 +1,403 @@ +# 测量标签栏页面操作 +# page_objects/measure_tabbar_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException +import logging +import time + +import globals.ids as ids +import globals.global_variable as global_variable # 导入全局变量模块 +from globals.driver_utils import check_session_valid, reconnect_driver, go_main_click_tabber_button # 导入会话检查和重连函数 +# import globals.driver_utils as driver_utils # 导入全局变量模块 +class MeasureTabbarPage: + def __init__(self, driver, wait,device_id): + self.driver = driver + self.wait = wait + self.logger = logging.getLogger(__name__) + self.seen_items = set() # 记录已经看到的项目,用于检测是否滚动到底部 + self.all_items = set() # 记录所有看到的项目,用于检测是否已经查看过所有项目 + # 获取设备ID用于重连操作 + self.device_id = device_id + + def is_measure_tabbar_visible(self): + """文件列表是否可见""" + try: + return self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID).is_displayed() + except NoSuchElementException: + self.logger.warning("文件列表未找到") + return False + except Exception as e: + self.logger.error(f"文件列表可见性时发生意外错误: {str(e)}") + return False + + def click_measure_tabbar(self): + """点击测量标签栏""" + try: + measure_tab = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_TABBAR_ID)) + ) + measure_tab.click() + self.logger.info("已点击测量标签栏") + + time.sleep(1) + + # 等待测量页面加载完成 + self.wait.until( + lambda driver: self.is_measure_tabbar_visible() + ) + return True + except TimeoutException: + self.logger.error("等待测量标签栏可点击超时") + return False + except Exception as e: + self.logger.error(f"点击测量标签栏时出错: {str(e)}") + return False + + # def scroll_list(self): + # """滑动列表以加载更多项目""" + # try: + # # 获取列表容器 + # list_container = self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID) + + # # 计算滑动坐标 + # start_x = list_container.location['x'] + list_container.size['width'] // 2 + # start_y = list_container.location['y'] + list_container.size['height'] * 0.8 + # end_y = list_container.location['y'] + list_container.size['height'] * 0.2 + + # # 执行滑动 + # self.driver.swipe(start_x, start_y, start_x, end_y, 1000) + # self.logger.info("已滑动列表") + + # # 等待新内容加载 + # time.sleep(2) + # return True + # except Exception as e: + # self.logger.error(f"滑动列表失败: {str(e)}") + # return False + def scroll_list(self, direction="down"): + """滑动列表以加载更多项目 + + Args: + direction: 滑动方向,"down"表示向下滑动,"up"表示向上滑动 + + Returns: + bool: 滑动是否成功执行,对于向上滑动,如果滑动到顶则返回False + """ + max_retry = 2 + retry_count = 0 + + while retry_count <= max_retry: + # 检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"会话无效,尝试重新连接... (尝试 {retry_count + 1}/{max_retry})") + try: + # 尝试重新连接驱动 + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("驱动重新连接成功") + except Exception as reconnect_error: + self.logger.error(f"驱动重新连接失败: {str(reconnect_error)}") + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,滑动列表失败") + return False + time.sleep(1) + continue + + try: + # 获取列表容器 + list_container = self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID) + + # 计算滑动坐标 + start_x = list_container.location['x'] + list_container.size['width'] // 2 + + if direction == "down": + # 向下滑动 + start_y = list_container.location['y'] + list_container.size['height'] * 0.95 + end_y = list_container.location['y'] + list_container.size['height'] * 0.05 + else: + # 向上滑动 + # 记录滑动前的项目,用于判断是否滑动到顶 + before_scroll_items = self.get_current_items() + start_y = list_container.location['y'] + list_container.size['height'] * 0.05 + end_y = list_container.location['y'] + list_container.size['height'] * 0.95 + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 1000) + + # 等待新内容加载 + time.sleep(1) + + # 向上滑动时,检查是否滑动到顶 + if direction == "up": + after_scroll_items = self.get_current_items() + # 如果滑动后的项目与滑动前的项目相同,说明已经滑动到顶 + if after_scroll_items == before_scroll_items: + self.logger.info("已滑动到列表顶部,列表内容不变") + return False + + return True + except Exception as e: + error_msg = str(e) + self.logger.error(f"滑动列表失败: {error_msg}") + + # 如果是连接相关的错误,尝试重连 + if any(keyword in error_msg.lower() for keyword in ['socket hang up', 'could not proxy command', 'session']): + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,滑动列表失败") + return False + self.logger.warning(f"尝试重新连接后重试... (尝试 {retry_count}/{max_retry})") + time.sleep(1) + else: + # 非连接错误,直接返回False + return False + + + def get_current_items(self): + """获取当前页面中的所有项目文本""" + max_retry = 2 + retry_count = 0 + + while retry_count <= max_retry: + # 检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"会话无效,尝试重新连接... (尝试 {retry_count + 1}/{max_retry})") + try: + # 尝试重新连接驱动 + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("驱动重新连接成功") + except Exception as reconnect_error: + self.logger.error(f"驱动重新连接失败: {str(reconnect_error)}") + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,获取当前项目失败") + return [] + time.sleep(0.1) # 等待1秒后重试 + continue + + try: + items = self.driver.find_elements(AppiumBy.ID, ids.MEASURE_LISTVIEW_ID) + item_texts = [] + + for item in items: + try: + title_element = item.find_element(AppiumBy.ID, ids.MEASURE_NAME_TEXT_ID) + if title_element and title_element.text: + item_texts.append(title_element.text) + except NoSuchElementException: + continue + except Exception as item_error: + self.logger.warning(f"处理项目时出错: {str(item_error)}") + continue + + return item_texts + except Exception as e: + error_msg = str(e) + self.logger.error(f"获取当前项目失败: {error_msg}") + + # 如果是连接相关的错误,尝试重连 + if any(keyword in error_msg.lower() for keyword in ['socket hang up', 'could not proxy command', 'session']): + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,获取当前项目失败") + return [] + self.logger.warning(f"尝试重新连接后重试... (尝试 {retry_count}/{max_retry})") + time.sleep(1) + else: + # 非连接错误,直接返回空列表 + return [] + + def click_item_by_text(self, text): + """点击指定文本的项目""" + max_retry = 2 + retry_count = 0 + + while retry_count <= max_retry: + # 检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"会话无效,尝试重新连接... (尝试 {retry_count + 1}/{max_retry})") + try: + # 尝试重新连接驱动 + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("驱动重新连接成功") + except Exception as reconnect_error: + self.logger.error(f"驱动重新连接失败: {str(reconnect_error)}") + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,点击项目失败") + return False + time.sleep(1) + continue + + try: + # 查找包含指定文本的项目 + items = self.driver.find_elements(AppiumBy.ID, ids.MEASURE_LISTVIEW_ID) + + for item in items: + try: + title_element = item.find_element(AppiumBy.ID, ids.MEASURE_NAME_TEXT_ID) + if title_element and title_element.text == text: + title_element.click() + self.logger.info(f"已点击项目: {text}") + return True + except NoSuchElementException: + continue + except Exception as item_error: + self.logger.warning(f"处理项目时出错: {str(item_error)}") + continue + + self.logger.warning(f"未找到可点击的项目: {text}") + return False + except Exception as e: + error_msg = str(e) + self.logger.error(f"点击项目失败: {error_msg}") + + # 如果是连接相关的错误,尝试重连 + if any(keyword in error_msg.lower() for keyword in ['socket hang up', 'could not proxy command', 'session']): + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,点击项目失败") + return False + self.logger.warning(f"尝试重新连接后重试... (尝试 {retry_count}/{max_retry})") + time.sleep(1) + else: + # 非连接错误,直接返回False + return False + + def find_keyword(self, fixed_filename): + """查找指定关键词并点击,支持向下和向上滑动查找""" + try: + # 等待线路列表容器出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_LIST_ID)) + ) + # self.logger.info("线路列表容器已找到") + + max_scroll_attempts = 50 # 最大滚动尝试次数 + scroll_count = 0 + found_items_count = 0 # 记录已找到的项目数量 + last_items_count = 0 # 记录上一次找到的项目数量 + previous_items = set() # 记录前一次获取的项目集合,用于检测是否到达边界 + + # 首先尝试向下滑动查找 + while scroll_count < max_scroll_attempts: + # 获取当前页面中的所有项目 + current_items = self.get_current_items() + # self.logger.info(f"当前页面找到 {len(current_items)} 个项目: {current_items}") + + # 检查目标文件是否在当前页面中 + if fixed_filename in current_items: + # self.logger.info(f"找到目标文件: {fixed_filename}") + # 点击目标文件 + if self.click_item_by_text(fixed_filename): + return True + else: + self.logger.error(f"点击目标文件失败: {fixed_filename}") + return False + + # 检查是否到达底部:连续两次获取的项目相同 + if current_items == previous_items and len(current_items) > 0: + self.logger.info("连续两次获取的项目相同,已到达列表底部") + break + + # 更新前一次项目集合 + previous_items = current_items.copy() + + # # 记录所有看到的项目 + # self.all_items.update(current_items) + + # # 检查是否已经查看过所有项目 + # if len(current_items) > 0 and found_items_count == len(self.all_items): + # self.logger.info("已向下查看所有项目,未找到目标文件") + # break + # # return False + + # found_items_count = len(self.all_items) + + # 向下滑动列表以加载更多项目 + if not self.scroll_list(direction="down"): + self.logger.error("向下滑动列表失败") + return False + + scroll_count += 1 + self.logger.info(f"第 {scroll_count} 次向下滑动,继续查找...") + + # 如果向下滑动未找到,尝试向上滑动查找 + self.logger.info("向下滑动未找到目标,开始向上滑动查找") + + # 重置滚动计数 + scroll_count = 0 + + while scroll_count < max_scroll_attempts: + # 向上滑动列表 + # 如果返回False,说明已经滑动到顶 + if not self.scroll_list(direction="up"): + # 检查是否是因为滑动到顶而返回False + if "已滑动到列表顶部" in self.logger.handlers[0].buffer[-1].message: + self.logger.info("已滑动到列表顶部,停止向上滑动") + break + else: + self.logger.error("向上滑动列表失败") + return False + + # 获取当前页面中的所有项目 + current_items = self.get_current_items() + # self.logger.info(f"向上滑动后找到 {len(current_items)} 个项目: {current_items}") + + # 检查目标文件是否在当前页面中 + if fixed_filename in current_items: + self.logger.info(f"找到目标文件: {fixed_filename}") + # 点击目标文件 + if self.click_item_by_text(fixed_filename): + return True + else: + self.logger.error(f"点击目标文件失败: {fixed_filename}") + return False + + scroll_count += 1 + self.logger.info(f"第 {scroll_count} 次向上滑动,继续查找...") + + self.logger.warning(f"经过 {max_scroll_attempts * 2} 次滑动仍未找到目标文件") + return False + + except TimeoutException: + self.logger.error("等待线路列表元素超时") + return False + except Exception as e: + self.logger.error(f"查找关键词时出错: {str(e)}") + return False + + def measure_tabbar_page_manager(self): + """执行测量操作""" + try: + # 跳转到测量页面 + if not go_main_click_tabber_button(self.driver, self.device_id, "com.bjjw.cjgc:id/img_3_layout"): + logging.error(f"设备 {self.device_id} 跳转到测量页面失败") + return False + + # # 点击测量标签栏 + # if not self.click_measure_tabbar(): + # self.logger.error("点击测量标签栏失败") + # return False + + # 固定文件名 + fixed_filename = global_variable.GLOBAL_CURRENT_PROJECT_NAME + self.logger.info(f"开始查找测量数据: {fixed_filename}") + + # 重置已看到的项目集合 + self.seen_items = set() + self.all_items = set() + + # 查找并点击测量数据 + if self.find_keyword(fixed_filename): + self.logger.info("成功找到并点击测量数据") + return True + else: + self.logger.warning("未找到测量数据") + return False + + except Exception as e: + self.logger.error(f"执行测量操作时出错: {str(e)}") + return False \ No newline at end of file diff --git a/page_objects/more_download_page.py b/page_objects/more_download_page.py new file mode 100644 index 0000000..43e3939 --- /dev/null +++ b/page_objects/more_download_page.py @@ -0,0 +1,343 @@ +# test_more_download_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +import logging +import time + +class MoreDownloadPage: + def __init__(self, driver, wait,device_id): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + + def is_on_more_download_page(self): + """通过下载历史数据按钮来判断是否在更多下载页面""" + try: + # 使用下载历史数据按钮的resource-id来检查 + download_history_locator = (AppiumBy.ID, "com.bjjw.cjgc:id/download_history") + self.wait.until(EC.presence_of_element_located(download_history_locator)) + self.logger.info("已确认在更多下载页面") + return True + except TimeoutException: + self.logger.warning("未找到下载历史数据按钮,不在更多下载页面") + return False + except Exception as e: + self.logger.error(f"检查更多下载页面时发生意外错误: {str(e)}") + return False + + def click_download_button(self): + """点击下载按钮""" + try: + # 点击下载历史数据按钮 + download_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_history")) + ) + download_button.click() + self.logger.info("已点击下载历史数据按钮") + + # 等待下载操作开始 + # time.sleep(3) + + return True + + except TimeoutException: + self.logger.error("等待下载按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载按钮时出错: {str(e)}") + return False + + def click_download_original_data(self): + """点击下载原始数据按钮并处理日期选择""" + try: + # 点击下载原始数据按钮 + download_original_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_org")) + ) + download_original_btn.click() + self.logger.info("已点击下载原始数据按钮") + + # 等待日期选择弹窗出现 + # time.sleep(2) + + # 点击选择开始日期 + start_date_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date")) + ) + start_date_btn.click() + self.logger.info("已点击选择开始日期") + + # 等待日期选择器出现 + # time.sleep(2) + + # 滑动年份选择器 - 向上滑动1/5的距离 + if not self._swipe_year_wheel(): + self.logger.error("滑动年份选择器失败") + return False + + # 点击日期选择器的确定按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn")) + ) + confirm_btn.click() + self.logger.info("已确认日期选择") + + # 等待日期选择器关闭 + # time.sleep(2) + + # 假设弹窗有确定按钮,点击它开始下载 + try: + # 尝试查找并点击下载弹窗的确定按钮 + download_confirm_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]")) + ) + download_confirm_btn.click() + self.logger.info("已点击下载确认按钮") + except TimeoutException: + self.logger.warning("未找到下载确认按钮,可能不需要确认") + + # 等待下载开始 + # time.sleep(3) + + return True + + except TimeoutException: + self.logger.error("等待下载原始数据按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载原始数据时出错: {str(e)}") + return False + + def click_download_result_data(self): + """点击下载成果数据按钮并处理日期选择""" + try: + # 点击下载成果数据按钮 + download_result_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_result")) + ) + download_result_btn.click() + self.logger.info("已点击下载成果数据按钮") + + # 等待日期选择弹窗出现 + # time.sleep(2) + + # 点击选择开始日期 + start_date_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date")) + ) + start_date_btn.click() + self.logger.info("已点击选择开始日期") + + # 等待日期选择器出现 + # time.sleep(2) + + # 滑动年份选择器 - 向上滑动1/5的距离 + if not self._swipe_year_wheel(): + self.logger.error("滑动年份选择器失败") + return False + + # 点击日期选择器的确定按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn")) + ) + confirm_btn.click() + self.logger.info("已确认日期选择") + + # 等待日期选择器关闭 + # time.sleep(2) + + # 假设弹窗有确定按钮,点击它开始下载 + try: + # 尝试查找并点击下载弹窗的确定按钮 + download_confirm_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]")) + ) + download_confirm_btn.click() + self.logger.info("已点击下载确认按钮") + except TimeoutException: + self.logger.warning("未找到下载确认按钮,可能不需要确认") + + # 等待下载开始 + # time.sleep(3) + + return True + + except TimeoutException: + self.logger.error("等待下载成果数据按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载成果数据时出错: {str(e)}") + return False + + def _swipe_year_wheel(self): + """滑动年份选择器的滚轮""" + try: + # 获取年份选择器滚轮元素 + year_wheel = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/wheelView1") + + # 获取滚轮的位置和尺寸 + location = year_wheel.location + size = year_wheel.size + + # 计算滚轮中心点坐标 + center_x = location['x'] + size['width'] // 2 + center_y = location['y'] + size['height'] // 2 + + # 计算滑动距离 - 滚轮高度的1/5 + swipe_distance = size['height'] // 5 + + # 执行滑动操作 - 从中心向上滑动1/5高度 + self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500) + + self.logger.info("已滑动年份选择器") + return True + + except Exception as e: + self.logger.error(f"滑动年份选择器时出错: {str(e)}") + return False + + def wait_for_loading_dialog(self, timeout=900): + """ + 检查特定结构的加载弹窗的出现和消失 + + 参数: + timeout: 最大等待时间,默认10分钟(600秒) + + 返回: + bool: 如果加载弹窗出现并消失返回True,否则返回False + """ + try: + self.logger.info("开始检查加载弹窗...") + + # 首先检查加载弹窗是否出现 + start_time = time.time() + loading_appeared = False + + # 等待加载弹窗出现(最多等待30秒) + while time.time() - start_time < 30: + try: + # 根据提供的结构查找加载弹窗 + # 查找包含ProgressBar和"loading..."文本的弹窗 + loading_indicators = [ + (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"), + (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"), + (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"), + (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]") + ] + + for by, value in loading_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + loading_appeared = True + self.logger.info("数据下载已开始") + self.logger.info("检测到加载弹窗出现") + break + except: + continue + + if loading_appeared: + break + + except Exception as e: + pass + + time.sleep(1) + + # 如果加载弹窗没有出现,直接返回True + if not loading_appeared: + self.logger.info("未检测到加载弹窗,继续执行") + return True + + # 等待加载弹窗消失 + self.logger.info("等待加载弹窗消失...") + disappearance_start_time = time.time() + + while time.time() - disappearance_start_time < timeout: + try: + # 检查加载弹窗是否还存在 + loading_still_exists = False + + for by, value in loading_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + loading_still_exists = True + break + except: + continue + + if not loading_still_exists: + self.logger.info("加载弹窗已消失") + return True + + # 每1分钟记录一次状态 + if int(time.time() - disappearance_start_time) % 60 == 0: + elapsed_time = int(time.time() - disappearance_start_time) + self.logger.info(f"加载弹窗仍在显示,已等待{elapsed_time//60}分钟") + + except Exception as e: + # 如果出现异常,可能弹窗已经消失 + self.logger.info("加载弹窗可能已消失") + return True + + time.sleep(1) + + # 如果超时,记录错误并返回False + self.logger.error(f"加载弹窗在{timeout}秒后仍未消失") + return False + + except Exception as e: + self.logger.error(f"检查加载弹窗时出错: {str(e)}") + return False + + def more_download_page_manager(self): + """执行更多下载页面管理操作""" + try: + self.logger.info("开始执行更多下载页面操作") + + # 检查是否在更多下载页面 + if not self.is_on_more_download_page(): + self.logger.error("不在更多下载页面") + return False + + # 点击下载历史数据按钮 + if not self.click_download_button(): + self.logger.error("点击下载历史数据按钮失败") + return False + + # 等待下载历史数据页面加载完成 + # time.sleep(3) + + # 点击下载原始数据按钮 + if not self.click_download_original_data(): + self.logger.error("点击下载原始数据按钮失败") + return False + + # 等待下载操作完成 + time.sleep(1) + + # 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失 + if not self.wait_for_loading_dialog(): + self.logger.warning("下载过程中的加载弹窗未在预期时间内消失,但操作已完成") + + # 等待一段时间,确保原始数据下载完成 + time.sleep(1) + + # 点击下载成果数据按钮 + if not self.click_download_result_data(): + self.logger.error("点击下载成果数据按钮失败") + return False + + # 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失 + if not self.wait_for_loading_dialog(): + self.logger.warning("成果数据下载过程中的加载弹窗未在预期时间内消失,但操作已完成") + + self.logger.info("更多下载页面操作执行完成") + return True + except Exception as e: + self.logger.error(f"执行更多下载页面操作时出错: {str(e)}") + return False \ No newline at end of file diff --git a/page_objects/screenshot_page.py b/page_objects/screenshot_page.py new file mode 100644 index 0000000..b75cf4d --- /dev/null +++ b/page_objects/screenshot_page.py @@ -0,0 +1,1907 @@ +# screenshot_page.py +import subprocess +import logging +import time +import re +import os +from datetime import datetime +from appium.webdriver.common.appiumby import AppiumBy +from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import globals.global_variable as global_variable # 导入全局变量模块 +import globals.ids as ids +# 导入全局驱动工具函数 +from globals.driver_utils import check_session_valid, reconnect_driver +import globals.driver_utils as driver_utils +import globals.global_variable as global_variable # 导入全局变量模块 + +class ScreenshotPage: + def __init__(self, driver, wait, device_id=None): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + self.all_items = set() + + # def load_line_time_mapping_dict(self, filename="20251022.1.CZSCZQ-3fhg0410.txt", log_directory="D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + # """ + # 加载指定文件中的线路编码和时间到全局字典 + # 参数: + # filename: 文件名 (例如: "20251022.1.CZSCZQ-3fhg0410.txt") + # log_directory: 文件所在目录 + # """ + + # try: + # # 获取当前年月日(例如:20251023) + # current_date = datetime.now().strftime("%Y%m%d") + # # 拼接格式:年月日.1.用户名.txt + # filename = f"{current_date}.1.{global_variable.GLOBAL_USERNAME}.txt" + # file_path = os.path.join(log_directory, filename) + # if not os.path.exists(file_path): + # self.logger.error(f"文件不存在: {file_path}") + # return + + # self.logger.info(f"正在加载文件: {file_path}") + + # # 临时字典,用于存储当前文件中的线路编码和时间 + # temp_mapping = {} + + # with open(file_path, 'r', encoding='utf-8') as f: + # for line_num, line in enumerate(f, 1): + # line = line.strip() + # # 解析日志行格式: "2025-10-22 16:22:50.171, INFO, 已修改线路时间:L205413, 结束时间:2025-10-22 09:18:17.460" + # if "已修改线路时间" in line and "结束时间" in line: + # # 提取线路编码 + # line_code_match = re.search(r'已修改线路时间[::]\s*([^,]+)', line) + # # 提取结束时间 + # end_time_match = re.search(r'结束时间[::]\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) + + # if line_code_match and end_time_match: + # line_code = line_code_match.group(1).strip() + # end_time_str = end_time_match.group(1) + + # # 解析日期时间 + # try: + # end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S") + # # 存入临时字典(同一个线路编码,后面的会覆盖前面的,实现取最后一条) + # temp_mapping[line_code] = end_time + # print(f"第{line_num}行: 找到线路编码 {line_code} 时间: {end_time}") + # except ValueError as e: + # print(f"第{line_num}行: 解析时间格式错误: {end_time_str}, 错误: {e}") + # continue + # else: + # print(f"第{line_num}行: 无法解析线路编码或结束时间") + + # # 将临时字典的内容更新到全局字典 + # for line_code, end_time in temp_mapping.items(): + # global_variable.LINE_TIME_MAPPING_DICT[line_code] = end_time + # print(f"更新全局字典: {line_code} -> {end_time}") + + # print(f"文件加载完成,共处理 {len(temp_mapping)} 条记录") + # print(f"当前全局字典总数: {len(global_variable.LINE_TIME_MAPPING_DICT)} 条记录") + # return True + + # except Exception as e: + # print(f"加载文件 {filename} 时出错: {str(e)}") + + def load_line_time_mapping_dict(self, filename="20251022.1.CZSCZQ-3fhg0410.txt", log_directory="D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510", poll_interval=120, max_wait_time=18000): + """ + 加载指定文件中的线路编码和时间到全局字典 + 参数: + filename: 文件名 (例如: "20251022.1.CZSCZQ-3fhg0410.txt") + log_directory: 文件所在目录 + poll_interval: 轮询间隔时间(秒),默认2分钟 + max_wait_time: 最大等待时间(秒),默认5小时 + """ + + try: + current_year_month = datetime.now().strftime("%Y%m") + # 将Logs\\后的内容替换为实际年月 + log_directory = log_directory.split("Logs\\")[0] + "Logs\\" + current_year_month + + # 获取当前年月日(例如:20251023) + current_date = datetime.now().strftime("%Y%m%d") + # 拼接格式:年月日.1.用户名.txt + filename = f"{current_date}.1.{global_variable.GLOBAL_USERNAME}.txt" + file_path = os.path.join(log_directory, filename) + + # 轮询等待文件出现 + start_time = time.time() + wait_count = 0 + + while not os.path.exists(file_path): + wait_count += 1 + elapsed_time = time.time() - start_time + + if elapsed_time >= max_wait_time: + self.logger.error(f"等待文件超时 ({max_wait_time}秒),文件仍未出现: {file_path}") + return False + + self.logger.info(f"第{wait_count}次检查 - 文件不存在,等待 {poll_interval} 秒后重试... (已等待 {elapsed_time:.0f} 秒)") + time.sleep(poll_interval) + + # 文件存在,继续处理 + self.logger.info(f"文件已找到,正在加载: {file_path}") + + # 临时字典,用于存储当前文件中的线路编码和时间 + temp_mapping = {} + + with open(file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + # 解析日志行格式: "2025-10-22 16:22:50.171, INFO, 已修改线路时间:L205413, 结束时间:2025-10-22 09:18:17.460" + if "已修改线路时间" in line and "结束时间" in line: + # 提取线路编码 + line_code_match = re.search(r'已修改线路时间[::]\s*([^,]+)', line) + # 提取结束时间 + end_time_match = re.search(r'结束时间[::]\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) + + if line_code_match and end_time_match: + line_code = line_code_match.group(1).strip() + end_time_str = end_time_match.group(1) + + # 解析日期时间 + try: + end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S") + # 存入临时字典(同一个线路编码,后面的会覆盖前面的,实现取最后一条) + temp_mapping[line_code] = end_time + self.logger.info(f"第{line_num}行: 找到线路编码 {line_code} 时间: {end_time}") + except ValueError as e: + self.logger.warning(f"第{line_num}行: 解析时间格式错误: {end_time_str}, 错误: {e}") + continue + else: + self.logger.warning(f"第{line_num}行: 无法解析线路编码或结束时间") + + # 将临时字典的内容更新到全局字典 + for line_code, end_time in temp_mapping.items(): + global_variable.LINE_TIME_MAPPING_DICT[line_code] = end_time + self.logger.info(f"更新全局字典: {line_code} -> {end_time}") + + total_wait_time = time.time() - start_time + self.logger.info(f"文件加载完成,等待时间: {total_wait_time:.0f}秒,共处理 {len(temp_mapping)} 条记录") + self.logger.info(f"当前全局字典总数: {len(global_variable.LINE_TIME_MAPPING_DICT)} 条记录") + return True + + except Exception as e: + self.logger.error(f"加载文件 {filename} 时出错: {str(e)}") + return False + + def scroll_list(self, direction="down"): + """滑动列表以加载更多项目 + + Args: + direction: 滑动方向,"down"表示向下滑动,"up"表示向上滑动 + + Returns: + bool: 滑动是否成功执行,对于向上滑动,如果滑动到顶则返回False + """ + try: + # 获取列表容器 + list_container = self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID) + + # 计算滑动坐标 + start_x = list_container.location['x'] + list_container.size['width'] // 2 + + if direction == "down": + # 向下滑动 + start_y = list_container.location['y'] + list_container.size['height'] * 0.95 + end_y = list_container.location['y'] + list_container.size['height'] * 0.05 + self.logger.info("向下滑动列表") + else: + # 向上滑动 + # 记录滑动前的项目,用于判断是否滑动到顶 + before_scroll_items = self.get_current_items() + start_y = list_container.location['y'] + list_container.size['height'] * 0.05 + end_y = list_container.location['y'] + list_container.size['height'] * 0.95 + self.logger.info("向上滑动列表") + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 1000) + + # 等待新内容加载 + time.sleep(1) + + # 向上滑动时,检查是否滑动到顶 + if direction == "up": + after_scroll_items = self.get_current_items() + # 如果滑动后的项目与滑动前的项目相同,说明已经滑动到顶 + if after_scroll_items == before_scroll_items: + self.logger.info("已滑动到列表顶部,列表内容不变") + return False + + return True + except Exception as e: + self.logger.error(f"滑动列表失败: {str(e)}") + return False + + + def get_current_items(self): + """获取当前页面中的所有项目文本""" + try: + items = self.driver.find_elements(AppiumBy.ID, ids.MEASURE_LISTVIEW_ID) + item_texts = [] + + for item in items: + try: + title_element = item.find_element(AppiumBy.ID, ids.MEASURE_NAME_TEXT_ID) + if title_element and title_element.text: + item_texts.append(title_element.text) + except NoSuchElementException: + continue + + return item_texts + except Exception as e: + self.logger.error(f"获取当前项目失败: {str(e)}") + return [] + + def click_item_by_text(self, text): + """点击指定文本的项目""" + try: + # 查找包含指定文本的项目 + items = self.driver.find_elements(AppiumBy.ID, ids.MEASURE_LISTVIEW_ID) + + for item in items: + try: + title_element = item.find_element(AppiumBy.ID, ids.MEASURE_NAME_TEXT_ID) + if title_element and title_element.text == text: + title_element.click() + self.logger.info(f"已点击项目: {text}") + return True + except NoSuchElementException: + continue + + self.logger.warning(f"未找到可点击的项目: {text}") + return False + except Exception as e: + self.logger.error(f"点击项目失败: {str(e)}") + return False + + + + def find_keyword(self, fixed_filename): + """查找指定关键词并点击,支持向下和向上滑动查找""" + try: + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {self.device_id} 驱动重连失败") + + # 等待线路列表容器出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_LIST_ID)) + ) + self.logger.info("线路列表容器已找到") + + max_scroll_attempts = 100 # 最大滚动尝试次数 + scroll_count = 0 + found_items_count = 0 # 记录已找到的项目数量 + last_items_count = 0 # 记录上一次找到的项目数量 + previous_items = set() # 记录前一次获取的项目集合,用于检测是否到达边界 + + # 首先尝试向下滑动查找 + while scroll_count < max_scroll_attempts: + # 获取当前页面中的所有项目 + current_items = self.get_current_items() + # self.logger.info(f"当前页面找到 {len(current_items)} 个项目: {current_items}") + + # 检查目标文件是否在当前页面中 + if fixed_filename in current_items: + self.logger.info(f"找到目标文件: {fixed_filename}") + # 点击目标文件 + if self.click_item_by_text(fixed_filename): + return True + else: + self.logger.error(f"点击目标文件失败: {fixed_filename}") + return False + + # 检查是否到达底部:连续两次获取的项目相同 + if current_items == previous_items and len(current_items) > 0: + self.logger.info("连续两次获取的项目相同,已到达列表底部") + break + + # 更新前一次项目集合 + previous_items = current_items.copy() + + # # 记录所有看到的项目 + # self.all_items.update(current_items) + + # # 检查是否已经查看过所有项目 + # if len(current_items) > 0 and found_items_count == len(self.all_items): + # self.logger.info("已向下查看所有项目,未找到目标文件") + # break + # # return False + + # found_items_count = len(self.all_items) + + # 向下滑动列表以加载更多项目 + if not self.scroll_list(direction="down"): + self.logger.error("向下滑动列表失败") + return False + + scroll_count += 1 + self.logger.info(f"第 {scroll_count} 次向下滑动,继续查找...") + + # 如果向下滑动未找到,尝试向上滑动查找 + self.logger.info("向下滑动未找到目标,开始向上滑动查找") + + # 重置滚动计数 + scroll_count = 0 + + while scroll_count < max_scroll_attempts: + # 向上滑动列表 + # 如果返回False,说明已经滑动到顶 + if not self.scroll_list(direction="up"): + # 检查是否是因为滑动到顶而返回False + if "已滑动到列表顶部" in self.logger.handlers[0].buffer[-1].message: + self.logger.info("已滑动到列表顶部,停止向上滑动") + break + else: + self.logger.error("向上滑动列表失败") + return False + + # 获取当前页面中的所有项目 + current_items = self.get_current_items() + # self.logger.info(f"向上滑动后找到 {len(current_items)} 个项目: {current_items}") + + # 检查目标文件是否在当前页面中 + if fixed_filename in current_items: + self.logger.info(f"找到目标文件: {fixed_filename}") + # 点击目标文件 + if self.click_item_by_text(fixed_filename): + return True + else: + self.logger.error(f"点击目标文件失败: {fixed_filename}") + return False + + scroll_count += 1 + self.logger.info(f"第 {scroll_count} 次向上滑动,继续查找...") + + self.logger.warning(f"经过 {max_scroll_attempts * 2} 次滑动仍未找到目标文件") + return False + + except TimeoutException: + self.logger.error("等待线路列表元素超时") + return False + except Exception as e: + self.logger.error(f"查找关键词时出错: {str(e)}") + return False + + def handle_measurement_dialog(self): + """处理测量弹窗 - 选择继续测量""" + try: + self.logger.info("检查测量弹窗...") + + # 直接尝试点击"继续测量"按钮 + continue_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/measure_continue_btn")) + ) + continue_btn.click() + self.logger.info("已点击'继续测量'按钮") + return True + + except TimeoutException: + self.logger.info("未找到继续测量按钮,可能没有弹窗") + return False # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"点击继续测量按钮时出错: {str(e)}") + return False + + # 检查有没有平差处理按钮 + def check_apply_btn(self): + """检查是否有平差处理按钮""" + try: + apply_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/point_measure_btn")) + ) + if apply_btn.is_displayed(): + logging.info("进入平差页面") + else: + self.logger.info("没有找到'平差处理'按钮") + return True + except TimeoutException: + self.logger.info("未找到平差处理按钮") + return False # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"点击平差处理按钮时出错: {str(e)}") + return False + + + def get_line_end_time(self, line_code): + """ + 从全局字典中获取线路编码对应的结束时间 + 参数: + line_code: 线路编码 + 返回: + tuple: (date_str, time_str) 日期和时间字符串 + """ + + if line_code in global_variable.LINE_TIME_MAPPING_DICT: + end_time = global_variable.LINE_TIME_MAPPING_DICT[line_code] + date_str = end_time.strftime("%Y-%m-%d") + time_str = end_time.strftime("%H:%M:%S") + return (date_str, time_str) + else: + self.logger.warning(f"未找到线路编码 {line_code} 的结束时间") + return (None, None) + + def show_line_time_mapping(self): + """ + 显示当前全局字典中的所有线路编码和时间 + """ + + self.logger.info("\n当前全局字典内容:") + if global_variable.LINE_TIME_MAPPING_DICT: + for line_code, end_time in sorted(global_variable.LINE_TIME_MAPPING_DICT.items()): + date_str = end_time.strftime("%Y-%m-%d") + time_str = end_time.strftime("%H:%M:%S") + self.logger.info(f" {line_code}: {date_str} {time_str}") + else: + self.logger.info(" 全局字典为空") + self.logger.info(f"总计: {len(global_variable.LINE_TIME_MAPPING_DICT)} 条记录\n") + def clear_line_time_mapping(self): + """ + 清空全局字典 + """ + global_variable.LINE_TIME_MAPPING_DICT.clear() + self.logger.info("已清空全局字典") + + def set_device_time(self, device_id, time_str=None, date_str=None, disable_auto_sync=True): + """ + 通过ADB设置设备时间(带管理员权限) + + 参数: + device_id: 设备ID + time_str: 时间字符串,格式 "HH:MM:SS" (例如: "14:30:00") + date_str: 日期字符串,格式 "YYYY-MM-DD" (例如: "2024-10-15") + disable_auto_sync: 是否禁用自动时间同步(防止设置的时间被网络时间覆盖) + + 返回: + bool: 操作是否成功 + """ + try: + if time_str is None and date_str is None: + return True + + # 首先尝试获取设备的root权限 + self.logger.info(f"尝试获取设备 {device_id} 的root权限...") + root_result = subprocess.run( + ["adb", "-s", device_id, "root"], + capture_output=True, + text=True, + timeout=10 + ) + + # 检查root权限获取是否成功(有些设备可能返回非0但实际已获取权限) + if root_result.returncode != 0: + self.logger.warning(f"获取root权限返回非0状态码,但继续尝试操作: {root_result.stderr.strip()}") + + now = datetime.now() + hour, minute, second = map(int, (time_str or f"{now.hour}:{now.minute}:{now.second}").split(":")) + year, month, day = map(int, (date_str or f"{now.year}-{now.month}-{now.day}").split("-")) + + # 禁用自动同步 + if disable_auto_sync: + # 使用su命令以root权限执行设置 + subprocess.run( + ["adb", "-s", device_id, "shell", "su", "-c", + "settings put global auto_time 0"], + timeout=5 + ) + subprocess.run( + ["adb", "-s", device_id, "shell", "su", "-c", + "settings put global auto_time_zone 0"], + timeout=5 + ) + + # 优先尝试旧格式 (MMDDhhmmYYYY.ss) + adb_time_str_old = f"{month:02d}{day:02d}{hour:02d}{minute:02d}{year:04d}.{second:02d}" + cmd_old = [ + "adb", "-s", device_id, "shell", "su", "-c", + f"date {adb_time_str_old}" + ] + result = subprocess.run(cmd_old, capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + self.logger.warning(f"旧格式失败,尝试新格式设置日期时间") + + # 尝试新格式(Toybox),使用su -c确保以root权限执行 + adb_time_str_new = f"{year:04d}{month:02d}{day:02d}.{hour:02d}{minute:02d}{second:02d}" + cmd_new = [ + "adb", "-s", device_id, "shell", "su", "-c", + f"date {adb_time_str_new}" + ] + result = subprocess.run(cmd_new, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + self.logger.info(f"设备 {device_id} 时间设置成功: {year}-{month}-{day} {hour}:{minute}:{second}") + return True + else: + self.logger.error(f"设备 {device_id} 设置时间失败: {result.stderr.strip()}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 设置时间命令执行超时") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 设置时间时发生异常: {str(e)}") + return False + + def disable_wifi(self, device_id): + """ + 通过ADB关闭设备WiFi + + 返回: + bool: 操作是否成功 + """ + try: + # 关闭WiFi + cmd_disable_wifi = [ + "adb", "-s", device_id, + "shell", "svc", "wifi", "disable" + ] + + result = subprocess.run(cmd_disable_wifi, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + self.logger.info(f"设备 {device_id} WiFi已关闭") + time.sleep(1) # 等待WiFi完全关闭 + return True + else: + self.logger.error(f"设备 {device_id} 关闭WiFi失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 关闭WiFi命令执行超时") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 关闭WiFi时发生错误: {str(e)}") + return False + + def enable_wifi(self, device_id): + """ + 通过ADB打开设备WiFi + + 返回: + bool: 操作是否成功 + """ + try: + # 打开WiFi + cmd_enable_wifi = [ + "adb", "-s", device_id, + "shell", "svc", "wifi", "enable" + ] + + result = subprocess.run(cmd_enable_wifi, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + self.logger.info(f"设备 {device_id} WiFi已打开") + time.sleep(3) # 等待WiFi完全连接 + return True + else: + self.logger.error(f"设备 {device_id} 打开WiFi失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 打开WiFi命令执行超时") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 打开WiFi时发生错误: {str(e)}") + return False + + def get_current_time(self, device_id): + """ + 获取设备当前时间 + + 返回: + str: 设备当前时间字符串,如果获取失败则返回None + """ + try: + cmd_get_time = [ + "adb", "-s", device_id, + "shell", "date" + ] + + result = subprocess.run(cmd_get_time, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + current_time = result.stdout.strip() + self.logger.info(f"设备 {device_id} 当前时间: {current_time}") + return current_time + else: + self.logger.error(f"设备 {device_id} 获取时间失败: {result.stderr}") + return None + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 获取时间命令执行超时") + return None + except Exception as e: + self.logger.error(f"设备 {device_id} 获取时间时发生错误: {str(e)}") + return None + + def check_wifi_status(self, device_id): + """ + 检查设备WiFi状态 + + 返回: + str: "enabled"表示已开启, "disabled"表示已关闭, None表示获取失败 + """ + try: + cmd_check_wifi = [ + "adb", "-s", device_id, + "shell", "dumpsys", "wifi", "|", "grep", "Wi-Fi" + ] + + result = subprocess.run(cmd_check_wifi, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + wifi_status = result.stdout.strip() + if "enabled" in wifi_status.lower(): + self.logger.info(f"设备 {device_id} WiFi状态: 已开启") + return "enabled" + elif "disabled" in wifi_status.lower(): + self.logger.info(f"设备 {device_id} WiFi状态: 已关闭") + return "disabled" + else: + self.logger.warning(f"设备 {device_id} 无法确定WiFi状态: {wifi_status}") + return None + else: + # 尝试另一种方法检查WiFi状态 + cmd_check_wifi_alt = [ + "adb", "-s", device_id, + "shell", "settings", "get", "global", "wifi_on" + ] + + result_alt = subprocess.run(cmd_check_wifi_alt, capture_output=True, text=True, timeout=10) + + if result_alt.returncode == 0: + wifi_on = result_alt.stdout.strip() + if wifi_on == "1": + self.logger.info(f"设备 {device_id} WiFi状态: 已开启") + return "enabled" + elif wifi_on == "0": + self.logger.info(f"设备 {device_id} WiFi状态: 已关闭") + return "disabled" + else: + self.logger.warning(f"设备 {device_id} 无法确定WiFi状态: {wifi_on}") + return None + else: + self.logger.error(f"设备 {device_id} 检查WiFi状态失败: {result_alt.stderr}") + return None + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 检查WiFi状态命令执行超时") + return None + except Exception as e: + self.logger.error(f"设备 {device_id} 检查WiFi状态时发生错误: {str(e)}") + return None + + def take_screenshot(self, filename_prefix="screenshot"): + """ + 通过Appium驱动截取设备屏幕 + + 参数: + filename_prefix: 截图文件前缀 + + 返回: + bool: 操作是否成功 + """ + try: + # 创建测试结果目录 + screenshots_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../test_results/screenshots') + if not os.path.exists(screenshots_dir): + os.makedirs(screenshots_dir) + self.logger.info(f"创建截图目录: {screenshots_dir}") + + # 截图保存 + screenshot_file = os.path.join( + screenshots_dir, + f"{filename_prefix}_{datetime.now().strftime('%Y%m%d')}.png" + ) + self.driver.save_screenshot(screenshot_file) + self.logger.info(f"截图已保存: {screenshot_file}") + return True + + except Exception as e: + self.logger.error(f"截图时发生错误: {str(e)}") + return False + + def wait_for_measurement_end(self, timeout=900): + """ + 等待按钮变成"测量结束",最多15分钟,包含驱动重新初始化机制 + + Args: + timeout: 超时时间,默认900秒(15分钟) + + Returns: + bool: 是否成功等到测量结束按钮 + """ + try: + # 更新WebDriverWait等待时间为900秒 + self.wait = WebDriverWait(self.driver, 900) + self.logger.info(f"设备等待测量结束按钮出现,最多等待 {timeout} 秒") + + start_time = time.time() + reinit_attempts = 0 + max_reinit_attempts = 3 # 最大重新初始化次数 + + while time.time() - start_time < timeout: + try: + # 使用XPath查找文本为"测量结束"的按钮 + measurement_end_button = self.driver.find_element( + AppiumBy.XPATH, + "//android.widget.Button[@text='测量结束']" + ) + + if measurement_end_button.is_displayed() and measurement_end_button.is_enabled(): + self.logger.info(f"设备检测到测量结束按钮") + return True + + except NoSuchElementException: + # 按钮未找到,继续等待 + pass + except Exception as e: + error_msg = str(e) + self.logger.warning(f"设备查找测量结束按钮时出现异常: {error_msg}") + + # 检测是否是UiAutomator2服务崩溃 + if 'UiAutomator2 server' in error_msg and 'instrumentation process is not running' in error_msg and reinit_attempts < max_reinit_attempts: + reinit_attempts += 1 + self.logger.info(f"设备检测到UiAutomator2服务崩溃,尝试第 {reinit_attempts} 次重新初始化驱动") + + # 尝试重新初始化驱动 + if self._reinit_driver(): + self.logger.info(f"设备驱动重新初始化成功") + else: + self.logger.error(f"设备驱动重新初始化失败") + # 继续尝试,而不是立即失败 + + # 等待一段时间后再次检查 + time.sleep(3) + + # 每30秒输出一次等待状态 + if int(time.time() - start_time) % 30 == 0: + elapsed = int(time.time() - start_time) + self.logger.info(f"设备 {self.device_id} 已等待 {elapsed} 秒,仍在等待测量结束...") + + self.logger.error(f"设备 {self.device_id} 等待测量结束按钮超时") + return False + + except Exception as e: + self.logger.error(f"设备 {self.device_id} 等待测量结束时发生错误: {str(e)}") + return False + + def _reinit_driver(self): + """ + 重新初始化Appium驱动 + + Returns: + bool: 是否成功重新初始化 + """ + try: + # 首先尝试关闭现有的驱动 + if hasattr(self, 'driver') and self.driver: + try: + self.driver.quit() + except: + self.logger.warning("关闭现有驱动时出现异常") + + # 导入必要的模块 + from appium import webdriver + from appium.options.android import UiAutomator2Options + + # 重新创建驱动配置 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = self.device_id + options.app_package = "com.bjjw.cjgc" + options.app_activity = ".activity.LoginActivity" + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 300 + options.udid = self.device_id + + # 重新连接驱动 + self.logger.info(f"正在重新初始化设备 {self.device_id} 的驱动...") + self.driver = webdriver.Remote("http://localhost:4723", options=options) + + # 重新初始化等待对象 + from selenium.webdriver.support.ui import WebDriverWait + self.wait = WebDriverWait(self.driver, 5) + + self.logger.info(f"设备 {self.device_id} 驱动重新初始化完成") + return True + + except Exception as e: + self.logger.error(f"设备 {self.device_id} 驱动重新初始化失败: {str(e)}") + return False + + def handle_confirmation_dialog(self, device_id, timeout=2): + """ + 处理确认弹窗,点击"是"按钮 + + Args: + device_id: 设备ID + timeout: 等待弹窗的超时时间 + + Returns: + bool: 是否成功处理弹窗 + """ + # 等待弹窗出现(最多等待2秒) + try: + dialog_message = WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((AppiumBy.XPATH, "//android.widget.TextView[@text='是否退出测量界面?']")) + ) + + self.logger.info(f"设备 {device_id} 检测到确认弹窗") + + # 查找并点击"是"按钮 + confirm_button = self.driver.find_element( + AppiumBy.XPATH, + "//android.widget.Button[@text='是' and @resource-id='android:id/button1']" + ) + + if confirm_button.is_displayed() and confirm_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") + confirm_button.click() + time.sleep(0.5) + return True + else: + self.logger.error(f"设备 {device_id} '是'按钮不可点击") + return False + + except TimeoutException: + # 超时未找到弹窗,认为没有弹窗,返回成功 + self.logger.info(f"设备 {device_id} 等待 {timeout} 秒未发现确认弹窗,可能没有弹窗,返回成功") + return True + + def click_back_button(self, device_id): + """点击手机系统返回按钮""" + try: + self.driver.back() + self.logger.info("已点击手机系统返回按钮") + return True + except Exception as e: + self.logger.error(f"点击手机系统返回按钮失败: {str(e)}") + return False + + def handle_back_button_with_confirmation(self, device_id, timeout=10): + """ + 处理返回按钮的确认弹窗 + + Args: + device_id: 设备ID + timeout: 等待弹窗的超时时间 + + Returns: + bool: 是否成功处理返回确认弹窗 + """ + logging.info(f"进入handle_back_button_with_confirmation函数") + try: + self.logger.info(f"设备 {device_id} 等待返回确认弹窗出现") + + start_time = time.time() + while time.time() - start_time < timeout: + try: + # 检查是否存在确认弹窗 - 使用多种定位策略提高兼容性 + dialog_selectors = [ + "//android.widget.TextView[@text='是否退出测量界面?']", + "//android.widget.TextView[contains(@text, '退出测量界面')]", + "//android.widget.TextView[contains(@text, '是否退出')]" + ] + + dialog_message = None + for selector in dialog_selectors: + try: + dialog_message = self.driver.find_element(AppiumBy.XPATH, selector) + if dialog_message.is_displayed(): + break + except NoSuchElementException: + continue + + if dialog_message and dialog_message.is_displayed(): + self.logger.info(f"设备 {device_id} 检测到返回确认弹窗") + + # 查找并点击"是"按钮 - 使用多种定位策略 + confirm_selectors = [ + "//android.widget.Button[@text='是' and @resource-id='android:id/button1']", + "//android.widget.Button[@text='是']", + "//android.widget.Button[@resource-id='android:id/button1']", + "//android.widget.Button[contains(@text, '是')]" + ] + + confirm_button = None + for selector in confirm_selectors: + try: + confirm_button = self.driver.find_element(AppiumBy.XPATH, selector) + if confirm_button.is_displayed() and confirm_button.is_enabled(): + break + except NoSuchElementException: + continue + + if confirm_button and confirm_button.is_displayed() and confirm_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") + confirm_button.click() + time.sleep(1) + + # 验证弹窗是否消失 + try: + self.driver.find_element(AppiumBy.XPATH, "//android.widget.TextView[@text='是否退出测量界面?']") + self.logger.warning(f"设备 {device_id} 确认弹窗可能未正确关闭") + return False + except NoSuchElementException: + self.logger.info(f"设备 {device_id} 确认弹窗已成功关闭") + return True + else: + self.logger.error(f"设备 {device_id} 未找到可点击的'是'按钮") + return False + + except NoSuchElementException: + # 弹窗未找到,继续等待 + pass + except Exception as e: + self.logger.warning(f"设备 {device_id} 查找确认弹窗时出现异常: {str(e)}") + + time.sleep(1) + + self.logger.error(f"设备 {device_id} 等待返回确认弹窗超时") + return False + + except Exception as e: + self.logger.error(f"设备 {device_id} 处理返回确认弹窗时发生错误: {str(e)}") + return False + + def handle_adjustment_result_dialog(self): + """处理平差结果确认弹窗""" + try: + self.logger.info("开始检测平差结果弹窗") + + # 等待弹窗出现(最多等待5秒) + warning_dialog = WebDriverWait(self.driver, 5).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/parentPanel")) + ) + + # 验证弹窗内容 + alert_title = warning_dialog.find_element(AppiumBy.ID, "android:id/alertTitle") + alert_message = warning_dialog.find_element(AppiumBy.ID, "android:id/message") + + self.logger.info(f"检测到弹窗 - 标题: {alert_title.text}, 消息: {alert_message.text}") + + # 确认是目标弹窗 + if "警告" in alert_title.text and "是否保留测量成果" in alert_message.text: + self.logger.info("确认是平差结果确认弹窗") + + # 点击"是 保留成果"按钮 + yes_button = warning_dialog.find_element(AppiumBy.ID, "android:id/button1") + if yes_button.text == "是 保留成果": + yes_button.click() + self.logger.info("已点击'是 保留成果'按钮") + + # 等待弹窗消失 + WebDriverWait(self.driver, 5).until( + EC.invisibility_of_element_located((AppiumBy.ID, "android:id/parentPanel")) + ) + self.logger.info("弹窗已关闭") + return True + else: + self.logger.error(f"按钮文本不匹配,期望'是 保留成果',实际: {yes_button.text}") + return False + else: + self.logger.warning("弹窗内容不匹配,不是目标弹窗") + return False + + except TimeoutException: + self.logger.info("未检测到平差结果弹窗,继续流程") + return True # 没有弹窗也是正常情况 + except Exception as e: + self.logger.error(f"处理平差结果弹窗时出错: {str(e)}") + return False + + def check_measurement_list(self, device_id): + """ + 检查是否存在测量列表 + + Args: + device_id: 设备ID + + Returns: + bool: 如果不存在测量列表返回True,存在返回False + """ + try: + # 等待线路列表容器出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_LIST_ID)) + ) + self.logger.info("线路列表容器已找到") + + # 如果存在MEASURE_LIST_ID,说明有测量列表,不需要执行后续步骤 + self.logger.info(f"设备 {device_id} 存在测量列表,无需执行后续返回操作") + return False + + except TimeoutException: + # 等待超时,说明没有测量列表 + self.logger.info(f"设备 {device_id} 未找到测量列表,可以继续执行后续步骤") + return True + except Exception as e: + self.logger.error(f"设备 {device_id} 检查测量列表时发生错误: {str(e)}") + return True + + def handle_back_navigation(self, breakpoint_name, device_id): + """ + 完整的返回导航处理流程 + + Args: + breakpoint_name: 断点名称 + device_id: 设备ID + + Returns: + bool: 整个返回导航流程是否成功 + """ + try: + time.sleep(2) + self.logger.info(f"已点击平差处理按钮,检查是否在测量页面") + + # 检测是否存在测量列表(修正逻辑) + has_measurement_list = self.check_measurement_list(device_id) + if not has_measurement_list: + self.logger.info(f"设备 {device_id} 存在测量列表,重新执行平差流程") + + # 把断点名称给find_keyword + if not self.find_keyword(breakpoint_name): + self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") + return False + + if not self.handle_measurement_dialog(): + self.logger.error(f"设备 {device_id} 处理测量弹窗失败") + return False + + if not self.check_apply_btn(): + self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") + return False + + # 滑动列表到底部 + if not self.scroll_list_to_bottom(device_id): + self.logger.error(f"设备 {device_id} 下滑列表到底部失败") + return False + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(device_id): + self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") + return False + + # 3. 再下滑一次 + if not self.scroll_down_once(device_id): + self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行") + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(device_id): + self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + return False + + self.logger.info(f"重新选择断点并点击平差处理按钮成功") + return True + + else: + self.logger.info(f"不在测量页面,继续执行后续返回操作") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 处理返回导航时发生错误: {str(e)}") + return False + + def execute_back_navigation_steps(self, device_id): + """ + 执行实际的返回导航步骤 + + Args: + device_id: 设备ID + + Returns: + bool: 导航是否成功 + """ + try: + # 1. 首先点击返回按钮 + if not self.click_back_button(device_id): + self.logger.error(f"设备 {device_id} 点击返回按钮失败") + return False + + # 2. 处理返回确认弹窗 + self.logger.info(f"已点击返回按钮,等待处理返回确认弹窗") + if not self.handle_confirmation_dialog(device_id): + self.logger.error(f"设备 {device_id} 处理返回确认弹窗失败") + return False + + + # 3. 验证是否成功返回到上一页面 + time.sleep(1) # 等待页面跳转完成 + + # 可以添加页面验证逻辑,比如检查是否返回到预期的页面 + # 这里可以根据实际应用添加特定的页面元素验证 + + self.logger.info(f"设备 {device_id} 返回导航流程完成") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 执行返回导航步骤时发生错误: {str(e)}") + return False + + def scroll_to_bottom_and_screenshot(self, device_id): + """ + 检测到测量结束后,下滑列表到最底端,点击最后一个spinner,再下滑一次,点击平差处理按钮后截图 + + Args: + device_id: 设备ID + + Returns: + bool: 操作是否成功 + """ + try: + self.logger.info(f"设备 {device_id} 开始执行测量结束后的操作流程") + time.sleep(5) + + # 1. 下滑列表到最底端 + if not self.scroll_list_to_bottom(device_id): + self.logger.error(f"设备 {device_id} 下滑列表到底部失败") + return False + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(device_id): + self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") + return False + + # 3. 再下滑一次 + if not self.scroll_down_once(device_id): + self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行") + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(device_id): + self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + return False + + # # 5. 在点击平差处理按钮后截图 + # time.sleep(2) # 等待平差处理按钮点击后的界面变化 + # if not self.take_screenshot("after_adjustment_button_click"): + # self.logger.error(f"设备 {device_id} 截图失败") + # return False + + self.logger.info(f"设备 {device_id} 测量结束后操作流程完成") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 执行测量结束后操作时发生错误: {str(e)}") + return False + + def scroll_list_to_bottom(self, device_id, max_swipes=60): + """ + 下滑列表到最底端 + + Args: + device_id: 设备ID + max_swipes: 最大下滑次数 + + Returns: + bool: 是否滑动到底部 + """ + try: + self.logger.info(f"设备 {device_id} 开始下滑列表到底部") + + # 获取列表元素 + list_view = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/auto_data_list") + + same_content_count = 0 + + # 初始化第一次的子元素文本 + initial_child_elements = list_view.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + current_child_texts = "|".join([ + elem.text.strip() for elem in initial_child_elements + if elem.text and elem.text.strip() + ]) + + for i in range(max_swipes): + # 执行下滑操作 + self.driver.execute_script("mobile: scrollGesture", { + 'elementId': list_view.id, + 'direction': 'down', + 'percent': 0.8, + 'duration': 500 + }) + + time.sleep(0.5) + + # 获取滑动后的子元素文本 + new_child_elements = list_view.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + new_child_texts = "|".join([ + elem.text.strip() for elem in new_child_elements + if elem.text and elem.text.strip() + ]) + + # 判断内容是否变化:若连续3次相同,认为到达底部 + if new_child_texts == current_child_texts: + same_content_count += 1 + if same_content_count >= 2: + self.logger.info(f"设备 {device_id} 列表已滑动到底部,共滑动 {i+1} 次") + return True + else: + same_content_count = 0 # 内容变化,重置计数 + current_child_texts = new_child_texts # 更新上一次内容 + + self.logger.debug(f"设备 {device_id} 第 {i+1} 次下滑完成,当前子元素文本: {new_child_texts[:50]}...") # 打印部分文本 + + self.logger.warning(f"设备 {device_id} 达到最大下滑次数 {max_swipes},可能未完全到底部") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 下滑列表时发生错误: {str(e)}") + return False + + def click_last_spinner_with_retry(self, device_id, max_retries=2): + """带重试机制的点击方法""" + for attempt in range(max_retries): + try: + if self.click_last_spinner(device_id): + return True + self.logger.warning(f"设备 {device_id} 第{attempt + 1}次点击失败,准备重试") + time.sleep(1) # 重试前等待 + except Exception as e: + self.logger.error(f"设备 {device_id} 第{attempt + 1}次尝试失败: {str(e)}") + + self.logger.error(f"设备 {device_id} 所有重试次数已用尽") + return False + + def click_last_spinner(self, device_id): + """ + 点击最后一个spinner + + Args: + device_id: 设备ID + + Returns: + bool: 是否成功点击 + """ + try: + self.logger.info(f"设备 {device_id} 查找最后一个spinner") + + # 查找所有的spinner元素 + spinners = self.driver.find_elements(AppiumBy.ID, "com.bjjw.cjgc:id/spinner") + + if not spinners: + self.logger.error(f"设备 {device_id} 未找到任何spinner元素") + return False + + # 获取最后一个spinner + last_spinner = spinners[-1] + + if not (last_spinner.is_displayed() and last_spinner.is_enabled()): + self.logger.error(f"设备 {device_id} 最后一个spinner不可点击") + return False + + # 点击操作 + self.logger.info(f"设备 {device_id} 点击最后一个spinner") + last_spinner.click() + + # 执行额外一次下滑操作 + self.scroll_down_once(device_id) + + max_retries = 3 # 最大重试次数 + retry_count = 0 + wait_timeout = 5 # 增加等待时间到5秒 + + while retry_count < max_retries: + try: + # 确保device_id正确设置,使用全局变量作为备用 + if not hasattr(self, 'device_id') or not self.device_id: + # 优先使用传入的device_id,其次使用全局变量 + self.device_id = device_id if device_id else global_variable.GLOBAL_DEVICE_ID + + # 使用self.device_id,确保有默认值 + actual_device_id = self.device_id if self.device_id else global_variable.GLOBAL_DEVICE_ID + + if not check_session_valid(self.driver, actual_device_id): + self.logger.warning(f"设备 {actual_device_id} 会话无效,尝试重新连接驱动...") + try: + # 使用正确的设备ID进行重连 + new_driver, new_wait = reconnect_driver(actual_device_id, self.driver) + if new_driver: + self.driver = new_driver + self.wait = new_wait + self.logger.info(f"设备 {actual_device_id} 驱动重连成功") + else: + self.logger.error(f"设备 {actual_device_id} 驱动重连失败") + retry_count += 1 + continue + except Exception as e: + self.logger.error(f"设备 {actual_device_id} 驱动重连异常: {str(e)}") + retry_count += 1 + continue + + # 点击spinner(如果是重试,需要重新获取元素) + if retry_count > 0: + spinners = self.driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.Spinner") + if not spinners: + self.logger.error(f"设备 {device_id} 未找到spinner元素") + retry_count += 1 + continue + last_spinner = spinners[-1] + if not (last_spinner.is_displayed() and last_spinner.is_enabled()): + self.logger.error(f"设备 {device_id} spinner不可点击") + retry_count += 1 + continue + self.logger.info(f"设备 {device_id} 重新点击spinner") + last_spinner.click() + # 重试时也执行下滑操作 + self.scroll_down_once(device_id) + + # 等待下拉菜单出现,增加等待时间到5秒 + wait = WebDriverWait(self.driver, wait_timeout) + detail_show = wait.until( + EC.presence_of_element_located((AppiumBy.ID, "com.bjjw.cjgc:id/detailshow")) + ) + + if detail_show.is_displayed(): + self.logger.info(f"设备 {device_id} spinner点击成功,下拉菜单已展开") + return True + else: + self.logger.error(f"设备 {device_id} 下拉菜单未显示") + retry_count += 1 + continue + + except Exception as wait_error: + error_msg = str(wait_error) + self.logger.error(f"设备 {device_id} 等待下拉菜单超时 (第{retry_count+1}次尝试): {error_msg}") + + # 检查是否是连接断开相关的错误 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + # if any(keyword in error_msg for keyword in ['socket hang up', 'Could not proxy command']): + # self.logger.warning(f"设备 {device_id} 检测到连接相关错误,尝试重连...") + if not reconnect_driver(device_id, self.driver): + self.logger.error(f"设备 {device_id} 驱动重连失败") + + retry_count += 1 + if retry_count < max_retries: + self.logger.info(f"设备 {device_id} 将在1秒后进行第{retry_count+1}次重试") + time.sleep(1) # 等待1秒后重试 + + self.logger.error(f"设备 {device_id} 经过{max_retries}次重试后仍无法展开下拉菜单") + return False + + except Exception as e: + self.logger.error(f"设备 {device_id} 点击最后一个spinner时发生错误: {str(e)}") + return False + + def scroll_down_once(self, device_id): + """ + 再次下滑一次 + + Args: + device_id: 设备ID + + Returns: + bool: 是否成功下滑 + """ + try: + self.logger.info(f"设备 {device_id} 执行额外一次下滑") + + # 获取列表元素 + list_view = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/auto_data_list") + + # 执行下滑操作 + self.driver.execute_script("mobile: scrollGesture", { + 'elementId': list_view.id, + 'direction': 'down', + 'percent': 0.5 + }) + + time.sleep(1) + self.logger.info(f"设备 {device_id} 额外下滑完成") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 额外下滑时发生错误: {str(e)}") + return False + + def click_adjustment_button(self, device_id): + """ + 点击平差处理按钮 + + Args: + device_id: 设备ID + + Returns: + bool: 是否成功点击 + """ + try: + self.logger.info(f"设备 {device_id} 查找平差处理按钮") + + # 查找平差处理按钮 + adjustment_button = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/point_measure_btn") + + # 验证按钮文本 + button_text = adjustment_button.text + if "平差处理" not in button_text: + self.logger.warning(f"设备 {device_id} 按钮文本不匹配,期望'平差处理',实际: {button_text}") + + if adjustment_button.is_displayed() and adjustment_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击平差处理按钮") + adjustment_button.click() + time.sleep(3) # 等待平差处理完成 + return True + else: + self.logger.error(f"设备 {device_id} 平差处理按钮不可点击") + return False + + except NoSuchElementException: + self.logger.error(f"设备 {device_id} 未找到平差处理按钮") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 点击平差处理按钮时发生错误: {str(e)}") + return False + + def add_breakpoint_to_upload_list(self, breakpoint_name, line_num): + """添加平差完成的断点到上传列表和字典""" + if breakpoint_name and breakpoint_name not in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST: + global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST.append(breakpoint_name) + global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[breakpoint_name] = { + 'breakpoint_name': breakpoint_name, + 'line_num': line_num + } + + logging.info(f"成功添加断点 '{breakpoint_name}' 到上传列表") + logging.info(f"断点详细信息: 线路编码={line_num}") + return True + else: + logging.warning(f"断点名为空或已存在于列表中") + return False + + # def screenshot_page_manager(self, device_id, results_dir): + # """执行截图页面管理操作""" + # try: + # # 加载指定文件中的线路编码和时间到全局字典 + # if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + # self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败") + # return False + + # time.sleep(5) + + # # 循环检查数据数量是否一致,直到获取到完整数据 + # retry_count = 0 + # while True: + # # 获取断点列表和线路时间字典的数量 + # breakpoint_count = len(global_variable.GLOBAL_BREAKPOINT_DICT) + # line_time_count = len(global_variable.LINE_TIME_MAPPING_DICT) + # self.logger.info(f"设备 {device_id} 断点列表数量: {breakpoint_count}, 文件中获取的线路时间数量: {line_time_count}") + + # # 如果断点列表为空,无法比较,直接跳出循环 + # if breakpoint_count == 0: + # self.logger.warning(f"设备 {device_id} 断点列表为空,无法进行数量比较") + # break + + # # 如果数量一致,获取到完整数据,跳出循环 + # if line_time_count == breakpoint_count: + # self.logger.info(f"设备 {device_id} 数据数量一致,已获取完整数据") + # break + + # # 数量不一致,等待三分钟后再次获取文件 + # retry_count += 1 + # self.logger.warning(f"设备 {device_id} 数据数量不一致: 断点列表({breakpoint_count}) != 线路时间({line_time_count}),第{retry_count}次重试,等待1分钟后重新加载文件") + # time.sleep(60) # 等待3分钟 + + # # 重新加载文件 + # if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + # self.logger.error(f"设备 {device_id} 重新加载线路时间映射字典失败") + # else: + # self.logger.info(f"设备 {device_id} 重新加载完成,新的线路时间数量: {len(global_variable.LINE_TIME_MAPPING_DICT)}") + + # # # 禁用WiFi + # # if not self.disable_wifi(device_id): + # # self.logger.error(f"设备 {device_id} 禁用WiFi失败") + # # return False + + # # 获取GLOBAL_BREAKPOINT_DICT中的断点名称和对应的线路编码 + # # 检查GLOBAL_BREAKPOINT_DICT是否为空,如果为空则初始化一些测试数据 + # if not global_variable.GLOBAL_BREAKPOINT_DICT: + # self.logger.warning("global_variable.GLOBAL_BREAKPOINT_DICT为空,正在初始化测试数据") + # # 添加一些测试断点数据,实际使用时应该从其他地方加载 + # # 注意:这里的值应该是字典,与section_mileage_config_page.py中的数据结构保持一致 + # global_variable.GLOBAL_BREAKPOINT_DICT = { + # "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区": { + # 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区", + # 'line_num': "L205413" + # }, + # "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区": { + # 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区", + # 'line_num': "L205414" + # } + # } + + # # 开始循环 + # for breakpoint_name in global_variable.GLOBAL_BREAKPOINT_DICT.keys(): + # self.logger.info(f"开始处理要平差截图的断点 {breakpoint_name}") + # # 把断点名称给find_keyword + # if not self.find_keyword(breakpoint_name): + # self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") + # return False + + # if not self.handle_measurement_dialog(): + # self.logger.error(f"设备 {device_id} 处理测量弹窗失败") + # return False + + # if not self.check_apply_btn(): + # self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") + # return False + + # # 根据断点名称在GLOBAL_BREAKPOINT_DICT中获取对应的字典 + # breakpoint_info = global_variable.GLOBAL_BREAKPOINT_DICT.get(breakpoint_name) + # if not breakpoint_info: + # self.logger.error(f"设备 {device_id} 未找到断点 {breakpoint_name} 对应的信息") + # return False + + # # 从字典中获取线路编码 + # line_code = breakpoint_info.get('line_num') + # if not line_code: + # self.logger.error(f"设备 {device_id} 断点 {breakpoint_name} 没有线路编码信息") + # return False + + # # 根据线路编码查找对应的时间 + # date_str, time_str = self.get_line_end_time(line_code) + # if not time_str or not date_str: + # self.logger.error(f"设备 {device_id} 未找到线路 {line_code} 对应的时间") + # return False + + # # 修改时间 + # if not self.set_device_time(device_id, time_str, date_str): + # self.logger.error(f"设备 {device_id} 设置设备时间失败") + # return False + + # # 滑动列表到底部 + # if not self.scroll_list_to_bottom(device_id): + # self.logger.error(f"设备 {device_id} 下滑列表到底部失败") + # return False + + # # 2. 点击最后一个spinner + # if not self.click_last_spinner_with_retry(device_id): + # self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") + # return False + + # # 3. 再下滑一次 + # if not self.scroll_down_once(device_id): + # self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行") + + # # 4. 点击平差处理按钮 + # if not self.click_adjustment_button(device_id): + # self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + # return False + + # # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个spinner, 再下滑一次,点击平差处理按钮平差 + # if not self.handle_back_navigation(breakpoint_name, device_id): + # self.logger.error(f"{breakpoint_name}平差失败,未截图") + # return False + + + # # 检测并处理"是 保留成果"弹窗 + # if not self.handle_adjustment_result_dialog(): + # self.logger.error("处理平差结果弹窗失败") + # return False + + # # 平差完成,将断点数据保存到上传列表中 + # if not self.add_breakpoint_to_upload_list(breakpoint_name, line_code): + # self.logger.error(f"设备 {device_id} 保存断点 {breakpoint_name} 到上传列表失败") + # return False + + # # 平差处理完成后截图 + # time.sleep(3) # 等待平差处理按钮点击后的界面变化 + # logging.info("断点保存到上传列表成功,开始截图") + # if not self.take_screenshot(breakpoint_name): + # self.logger.error(f"设备 {device_id} 截图失败") + # return False + + # # 点击返回按钮并处理弹窗 + # if not self.execute_back_navigation_steps(device_id): + # self.logger.error(f"设备 {device_id} 处理返回按钮确认失败") + # return False + # # 启用WiFi + # if not self.enable_wifi(device_id): + # self.logger.error(f"设备 {device_id} 启用WiFi失败") + # return False + + # self.logger.info(f"设备 {device_id} 截图页面操作执行完成") + # return True + # except Exception as e: + # self.logger.error(f"设备 {device_id} 执行截图页面操作时出错: {str(e)}") + # # 保存错误截图 + # # error_screenshot_file = os.path.join( + # # results_dir, + # # f"screenshot_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # # ) + # # self.driver.save_screenshot(error_screenshot_file) + # # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + # return False + + def handle_confirmation_dialog_save(self, device_id, timeout=2): + """ + 处理确认弹窗,点击"是"按钮 + + Args: + device_id: 设备ID + timeout: 等待弹窗的超时时间 + + Returns: + bool: 是否成功处理弹窗 + """ + # 等待弹窗出现(最多等待2秒) + try: + dialog_message = WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/content")) + ) + + self.logger.info(f"设备 {device_id} 检测到确认弹窗") + + # 查找并点击"是"按钮 + confirm_button = self.driver.find_element( + AppiumBy.ID, "android:id/button1" + ) + + if confirm_button.is_displayed() and confirm_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") + confirm_button.click() + time.sleep(0.5) + return True + else: + self.logger.error(f"设备 {device_id} '是'按钮不可点击") + return False + + except TimeoutException: + # 超时未找到弹窗,认为没有弹窗,返回成功 + self.logger.info(f"设备 {device_id} 等待 {timeout} 秒未发现确认弹窗,可能没有弹窗,返回成功") + return True + + + def screenshot_page_manager(self, device_id, results_dir): + """执行截图页面管理操作""" + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + try: + # 检查Appium是否运行,如果没有运行则重启 + if not driver_utils.is_appium_running(4723): + self.logger.warning("Appium服务器未运行,尝试重启...") + if not driver_utils.restart_appium_server(4723): + self.logger.error("重启Appium服务器失败") + return False + # 重新初始化driver + if not reconnect_driver(device_id): + self.logger.error("重新初始化driver失败") + return False + + # 加载指定文件中的线路编码和时间到全局字典 + if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败") + return False + + time.sleep(5) + + # 循环检查数据数量是否一致,直到获取到完整数据 + data_retry_count = 0 + while True: + # 获取断点列表和线路时间字典的数量 + breakpoint_count = len(global_variable.GLOBAL_BREAKPOINT_DICT) + line_time_count = len(global_variable.LINE_TIME_MAPPING_DICT) + self.logger.info(f"设备 {device_id} 断点列表数量: {breakpoint_count}, 文件中获取的线路时间数量: {line_time_count}") + + # 如果断点列表为空,无法比较,直接跳出循环 + if breakpoint_count == 0: + self.logger.warning(f"设备 {device_id} 断点列表为空,无法进行数量比较") + break + + # 如果数量一致,获取到完整数据,跳出循环 + # if line_time_count >= 2: + if line_time_count >= breakpoint_count: + self.logger.info(f"设备 {device_id} 数据数量一致,已获取完整数据") + break + + # 数量不一致,等待三分钟后再次获取文件 + data_retry_count += 1 + self.logger.warning(f"设备 {device_id} 数据数量不一致: 断点列表({breakpoint_count}) != 线路时间({line_time_count}),第{data_retry_count}次重试,等待1分钟后重新加载文件") + time.sleep(60) # 等待1分钟 + + # 重新加载文件 + if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + self.logger.error(f"设备 {device_id} 重新加载线路时间映射字典失败") + else: + self.logger.info(f"设备 {device_id} 重新加载完成,新的线路时间数量: {len(global_variable.LINE_TIME_MAPPING_DICT)}") + + # 检查GLOBAL_BREAKPOINT_DICT是否为空,如果为空则初始化一些测试数据 + if not global_variable.GLOBAL_BREAKPOINT_DICT: + self.logger.warning("global_variable.GLOBAL_BREAKPOINT_DICT为空,正在初始化测试数据") + global_variable.GLOBAL_BREAKPOINT_DICT = { + "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区": { + 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区", + 'line_num': "L205413" + }, + "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区": { + 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区", + 'line_num': "L205414" + } + } + + # 创建断点列表的副本,用于重试时重新处理 + breakpoint_names = list(global_variable.GLOBAL_BREAKPOINT_DICT.keys()) + processed_breakpoints = [] + + # 开始循环处理断点 + for breakpoint_name in breakpoint_names: + if breakpoint_name in processed_breakpoints: + continue + + self.logger.info(f"开始处理要平差截图的断点 {breakpoint_name}") + + # 把断点名称给find_keyword + if not self.find_keyword(breakpoint_name): + self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") + continue # 继续处理下一个断点 + + if not self.handle_measurement_dialog(): + self.logger.error(f"设备 {device_id} 处理测量弹窗失败") + continue + + if not self.check_apply_btn(): + self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") + continue + + # 根据断点名称在GLOBAL_BREAKPOINT_DICT中获取对应的字典 + breakpoint_info = global_variable.GLOBAL_BREAKPOINT_DICT.get(breakpoint_name) + if not breakpoint_info: + self.logger.error(f"设备 {device_id} 未找到断点 {breakpoint_name} 对应的信息") + continue + + # 从字典中获取线路编码 + line_code = breakpoint_info.get('line_num') + if not line_code: + self.logger.error(f"设备 {device_id} 断点 {breakpoint_name} 没有线路编码信息") + continue + + # 根据线路编码查找对应的时间 + date_str, time_str = self.get_line_end_time(line_code) + if not time_str or not date_str: + self.logger.error(f"设备 {device_id} 未找到线路 {line_code} 对应的时间") + continue + + # 修改时间 + if not self.set_device_time(device_id, time_str, date_str): + self.logger.error(f"设备 {device_id} 设置设备时间失败") + continue + + # 滑动列表到底部 + if not self.scroll_list_to_bottom(device_id): + self.logger.error(f"设备 {device_id} 下滑列表到底部失败") + continue + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(device_id): + self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") + continue + + # 3. 再下滑一次 + if not self.scroll_down_once(device_id): + self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行") + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(device_id): + self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + continue + + # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个spinner, 再下滑一次,点击平差处理按钮平差 + if not self.handle_back_navigation(breakpoint_name, device_id): + self.logger.error(f"{breakpoint_name}平差失败,未截图") + continue + + + # 检测并处理"是 保留成果"弹窗 + if not self.handle_adjustment_result_dialog(): + self.logger.error("处理平差结果弹窗失败") + continue + + # 平差完成,将断点数据保存到上传列表中 + if not self.add_breakpoint_to_upload_list(breakpoint_name, line_code): + self.logger.error(f"设备 {device_id} 保存断点 {breakpoint_name} 到上传列表失败") + continue + + # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个spinner, 再下滑一次,点击平差处理按钮平差 + if not self.handle_back_navigation(breakpoint_name, device_id): + self.logger.error(f"{breakpoint_name}平差失败,未截图") + continue + + # 检测并处理"是 保留成果"弹窗 + if not self.handle_adjustment_result_dialog(): + self.logger.error("处理平差结果弹窗失败") + continue + + # 平差处理完成后截图 + time.sleep(3) # 等待平差处理按钮点击后的界面变化 + logging.info("断点保存到上传列表成功,开始截图") + if not self.take_screenshot(breakpoint_name): + self.logger.error(f"设备 {device_id} 截图失败") + continue + + # 点击返回按钮并处理弹窗 + if not self.execute_back_navigation_steps(device_id): + self.logger.error(f"设备 {device_id} 处理返回按钮确认失败") + continue + + # 成功处理完一个断点,添加到已处理列表 + processed_breakpoints.append(breakpoint_name) + self.logger.info(f"成功处理断点: {breakpoint_name}") + + # 检查是否所有断点都处理完成 + if len(processed_breakpoints) == len(breakpoint_names): + # 启用WiFi + if not self.enable_wifi(device_id): + self.logger.error(f"设备 {device_id} 启用WiFi失败") + return False + + self.logger.info(f"设备 {device_id} 截图页面操作执行完成") + return True + else: + self.logger.warning(f"设备 {device_id} 部分断点处理失败,已成功处理 {len(processed_breakpoints)}/{len(breakpoint_names)} 个断点") + return True + + except Exception as e: + retry_count += 1 + self.logger.error(f"设备 {device_id} 执行截图页面操作时出错 (重试 {retry_count}/{max_retries}): {str(e)}") + + # 检查是否为连接错误 + if driver_utils.check_connection_error(e): + self.logger.warning("检测到连接错误,尝试重启Appium服务器...") + if not driver_utils.restart_appium_server(4723): + self.logger.error("重启Appium服务器失败") + else: + self.logger.info("Appium服务器重启成功,等待重新连接...") + time.sleep(10) + + # 重新初始化driver + if not reconnect_driver(device_id): + self.logger.error("重新初始化driver失败") + if retry_count >= max_retries: + break + continue + + # # 保存错误截图 + # error_screenshot_file = os.path.join( + # results_dir, + # f"screenshot_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # ) + # try: + # self.driver.save_screenshot(error_screenshot_file) + # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + # except: + # self.logger.error("保存错误截图失败") + + if retry_count >= max_retries: + self.logger.error(f"设备 {device_id} 达到最大重试次数,停止执行") + break + + self.logger.info(f"等待10秒后重试...") + time.sleep(10) + + return False + def run_automation_test(self): + # 滑动列表到底部 + if not self.scroll_list_to_bottom(self.device_id): + self.logger.error(f"设备 {self.device_id} 下滑列表到底部失败") + return False + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(self.device_id): + self.logger.error(f"设备 {self.device_id} 点击最后一个spinner失败") + return False + + # 3. 再下滑一次 + if not self.scroll_down_once(self.device_id): + self.logger.warning(f"设备 {self.device_id} 再次下滑失败,但继续执行") + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(self.device_id): + self.logger.error(f"设备 {self.device_id} 点击平差处理按钮失败") + return False diff --git a/page_objects/section_mileage_config_page.py b/page_objects/section_mileage_config_page.py new file mode 100644 index 0000000..c1e8dac --- /dev/null +++ b/page_objects/section_mileage_config_page.py @@ -0,0 +1,1112 @@ +# page_objects/section_mileage_config_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException +from globals import apis, global_variable +from globals.ex_apis import get_weather_simple +import time +import logging + +import globals.ids as ids # 导入元素ID +from globals.driver_utils import check_session_valid, reconnect_driver +from check_station import CheckStation + +class SectionMileageConfigPage: + def __init__(self, driver, wait, device_id): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + self.check_station_page = CheckStation(self.driver, self.wait,self.device_id) + + + def is_on_config_page(self): + """检查是否在断面里程配置页面""" + try: + title_bar = self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_TITLE_ID)) + ) + return True + except TimeoutException: + self.logger.warning("未找到断面里程配置页面的标题栏") + return False + + def scroll_to_find_element(self, element_id, max_scroll_attempts=5): + """ + 下滑页面直到找到指定元素 + + 参数: + element_id: 要查找的元素ID + max_scroll_attempts: 最大下滑次数 + + 返回: + bool: 是否找到元素 + """ + try: + for attempt in range(max_scroll_attempts): + # 检查元素是否存在 + try: + element = self.driver.find_element(AppiumBy.ID, element_id) + if element.is_displayed(): + # self.logger.info(f"找到元素: {element_id}") + return True + except NoSuchElementException: + pass + + # 如果没找到,下滑页面 + self.logger.info(f"第 {attempt + 1} 次下滑查找元素: {element_id}") + window_size = self.driver.get_window_size() + start_x = window_size['width'] // 2 + self.driver.swipe(start_x, 1600, start_x, 500, 500) + time.sleep(1) # 等待滑动完成 + + self.logger.warning(f"下滑 {max_scroll_attempts} 次后仍未找到元素: {element_id}") + return False + + except Exception as e: + self.logger.error(f"下滑查找元素时出错: {str(e)}") + return False + + def scroll_to_find_all_elements(self, element_ids, max_scroll_attempts=10): + """ + 下滑页面直到找到所有指定元素 + + 参数: + element_ids: 要查找的元素ID列表 + max_scroll_attempts: 最大下滑次数 + + 返回: + bool: 是否找到所有元素 + """ + try: + found_count = 0 + target_count = len(element_ids) + + for attempt in range(max_scroll_attempts): + # 检查每个元素是否存在 + current_found = 0 + for element_id in element_ids: + try: + element = self.driver.find_element(AppiumBy.ID, element_id) + if element.is_displayed(): + current_found += 1 + except NoSuchElementException: + pass + + if current_found > found_count: + found_count = current_found + self.logger.info(f"找到 {found_count}/{target_count} 个元素") + + if found_count == target_count: + self.logger.info(f"成功找到所有 {target_count} 个元素") + return True + + # 如果没找到全部,下滑页面 + self.logger.info(f"第 {attempt + 1} 次下滑,已找到 {found_count}/{target_count} 个元素") + window_size = self.driver.get_window_size() + start_x = window_size['width'] // 2 + self.driver.swipe(start_x, 1600, start_x, 500, 500) + time.sleep(1) # 等待滑动完成 + + self.logger.warning(f"下滑 {max_scroll_attempts} 次后仍未找到所有元素,只找到 {found_count}/{target_count} 个") + return found_count > 0 # 至少找到一个元素也算部分成功 + + except Exception as e: + self.logger.error(f"下滑查找所有元素时出错: {str(e)}") + return False + + def check_work_base_consistency(self): + """ + 判断第一个工作基点和最后一个工作基点是否一致 + + 返回: + bool: 是否一致,True表示一致,False表示不一致 + """ + try: + # 获取工作基点路径文本 + point_order_element = self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, "com.bjjw.cjgc:id/select_point_order_name")) + ) + point_order_text = point_order_element.text + # self.logger.info(f"工作基点路径: {point_order_text}") + + # 解析第一个工作基点和最后一个工作基点 + if "--->" in point_order_text: + parts = point_order_text.split("--->") + + # 提取第一个工作基点 + first_base = parts[0].strip() + # 提取最后一个工作基点 + last_base = parts[-1].strip() + + # self.logger.info(f"第一个工作基点: {first_base}") + # self.logger.info(f"最后一个工作基点: {last_base}") + + # 判断是否一致(去除可能的工作基点标识) + first_base_clean = first_base.replace("(工作基点)", "").strip() + last_base_clean = last_base.replace("(工作基点)", "").strip() + + is_consistent = first_base_clean == last_base_clean + # self.logger.info(f"工作基点一致性: {is_consistent}") + + return is_consistent + else: + self.logger.warning("无法解析工作基点路径格式") + return False + + except Exception as e: + self.logger.error(f"检查工作基点一致性时出错: {str(e)}") + return False + + def get_observation_type_based_on_base(self): + """ + 根据工作基点一致性返回观测类型 + + 返回: + str: 观测类型 + """ + try: + is_consistent = self.check_work_base_consistency() + + if is_consistent: + obs_type = "往:aBFFB 返:aBFFB" + # self.logger.info("工作基点一致,选择观测类型: 往:aBFFB 返:aBFFB") + else: + obs_type = "aBFFB" + # self.logger.info("工作基点不一致,选择观测类型: aBFFB") + + return obs_type + + except Exception as e: + self.logger.error(f"获取观测类型时出错: {str(e)}") + return "aBFFB" # 默认值 + + def select_weather(self, weather_option="阴"): + """选择天气""" + try: + # 先下滑查找天气元素 + if not self.scroll_to_find_element(ids.MEASURE_WEATHER_ID): + self.logger.error("未找到天气下拉框") + return False + + # 点击天气下拉框 + weather_spinner = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_WEATHER_ID)) + ) + weather_spinner.click() + time.sleep(1) # 等待选项弹出 + + # 等待选择对话框出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_SELECT_ID)) + ) + + # 选择指定天气选项 + weather_xpath = f"//android.widget.TextView[@resource-id='{ids.SELECT_DIALOG_TEXT1_ID}' and @text='{weather_option}']" + weather_option_element = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, weather_xpath)) + ) + weather_option_element.click() + self.logger.info(f"已选择天气: {weather_option}") + return True + except Exception as e: + self.logger.error(f"选择天气失败: {str(e)}") + return False + + def select_observation_type(self, obs_type=None): + """选择观测类型""" + try: + # 如果未指定观测类型,根据工作基点自动选择 + if obs_type is None: + obs_type = self.get_observation_type_based_on_base() + + # 先下滑查找观测类型元素 + if not self.scroll_to_find_element(ids.MEASURE_TYPE_ID): + self.logger.error("未找到观测类型下拉框") + return False + + # 点击观测类型下拉框 + obs_type_spinner = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_TYPE_ID)) + ) + obs_type_spinner.click() + time.sleep(2) # 等待选项弹出 + + # 等待选择对话框出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_SELECT_ID)) + ) + + # 选择指定观测类型 + obs_type_xpath = f"//android.widget.TextView[@resource-id='{ids.SELECT_DIALOG_TEXT1_ID}' and @text='{obs_type}']" + obs_type_option_element = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, obs_type_xpath)) + ) + obs_type_option_element.click() + self.logger.info(f"已选择观测类型: {obs_type}") + return True + except Exception as e: + self.logger.error(f"选择观测类型失败: {str(e)}") + return False + + def enter_temperature(self, temperature="25"): + """填写温度""" + try: + # 先下滑查找温度输入框 + if not self.scroll_to_find_element(ids.MEASURE_TEMPERATURE_ID): + self.logger.error("未找到温度输入框") + return False + + # 找到温度输入框并输入温度值 + temp_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_TEMPERATURE_ID)) + ) + temp_input.clear() # 清空原有内容 + temp_input.send_keys(temperature) + # self.logger.info(f"已输入温度: {temperature}") + return True + except Exception as e: + self.logger.error(f"输入温度失败: {str(e)}") + return False + + def enter_barometric_pressure(self, pressure="800"): + """填写气压""" + try: + # 先下滑查找气压输入框 + if not self.scroll_to_find_element("com.bjjw.cjgc:id/point_list_barometric_et"): + self.logger.error("未找到气压输入框") + return False + + # 找到气压输入框并输入气压值 + pressure_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/point_list_barometric_et")) + ) + pressure_input.clear() # 清空原有内容 + pressure_input.send_keys(pressure) + # self.logger.info(f"已输入气压: {pressure}") + return True + except Exception as e: + self.logger.error(f"输入气压失败: {str(e)}") + return False + + def click_save_button(self): + """点击保存按钮""" + try: + # 先下滑查找保存按钮 + if not self.scroll_to_find_element(ids.MEASURE_SAVE_ID): + self.logger.error("未找到保存按钮") + return False + + save_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_SAVE_ID)) + ) + save_button.click() + self.logger.info("已点击保存按钮") + return True + except Exception as e: + self.logger.error(f"点击保存按钮失败: {str(e)}") + return False + + def click_conn_level_btn(self): + """点击连接水准仪按钮""" + try: + conn_level_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.CONNECT_LEVEL_METER)) + ) + conn_level_btn.click() + self.logger.info("已点击连接水准仪按钮1") + + # 等待设备选择对话框出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/content")) + ) + return True + except Exception as e: + self.logger.error(f"点击连接水准仪按钮失败1: {str(e)}") + return False + + def connect_to_device(self): + """连接设备,处理可能出现的弹窗""" + try: + # 检查已配对设备列表中是否有设备 + paired_devices_list_xpath = "//android.widget.ListView[@resource-id='com.bjjw.cjgc:id/paired_devices']" + try: + # 等待配对设备列表出现 + paired_list = self.wait.until( + EC.visibility_of_element_located((AppiumBy.XPATH, paired_devices_list_xpath)) + ) + + # 获取列表中的所有子元素(设备项) + device_items = paired_list.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + + if device_items: + # 获取第一个设备的文本 + first_device_text = device_items[0].text + # self.logger.info(f"找到设备: {first_device_text}") + + + # 检查第一个设备是否不是"没有已配对的设备" + if "没有已配对的设备" not in first_device_text: + # 存在元素,点击第一个设备 + first_device = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, f"{paired_devices_list_xpath}/android.widget.TextView[1]")) + ) + first_device.click() + self.logger.info(f"已点击第一个设备: {first_device_text}") + # 处理可能出现的弹窗 + # if self._handle_alert_dialog(): + # # 弹窗已处理,重新尝试连接 + # self.logger.info("弹窗已处理,重新尝试连接设备") + # return self._retry_connection(paired_devices_list_xpath) + # else: + # # 没有弹窗,正常检查连接状态 + # return self._check_connection_status() + # 新增:最多尝试3次连接 + max_retry_times = 3 # 最大重试次数 + current_retry = 0 # 当前重试计数器(初始为0,代表第1次尝试) + + while current_retry < max_retry_times: + current_retry += 1 # 每次循环先+1,记录当前是第几次尝试 + self.logger.info(f"第 {current_retry} 次尝试连接设备(最多{max_retry_times}次)") + + if self._handle_alert_dialog(): + # 弹窗已处理,执行本次重试连接 + self.logger.info("弹窗已处理,查看按钮状态") + connect_success = self._retry_connection(paired_devices_list_xpath) + + # 若本次连接成功,立即返回True,终止重试 + if connect_success: + self.logger.info(f"第 {current_retry} 次尝试连接成功") + return True + else: + # 本次连接失败,判断是否还有剩余重试次数 + remaining_times = max_retry_times - current_retry + if remaining_times > 0: + self.logger.warning(f"第 {current_retry} 次尝试连接失败,剩余 {remaining_times} 次重试机会") + else: + self.logger.error(f"第 {current_retry} 次尝试连接失败,已达到最大重试次数({max_retry_times}次)") + else: + # 未检测到弹窗,直接尝试连接(逻辑与原代码一致,仅增加重试计数) + # self.logger.info("未检测到弹窗,尝试连接设备") + # connect_success = self._retry_connection(paired_devices_list_xpath) + connect_success = self._check_connection_status() + + if connect_success: + self.logger.info(f"第 {current_retry} 次尝试连接成功") + return True + else: + remaining_times = max_retry_times - current_retry + if remaining_times > 0: + self.logger.warning(f"第 {current_retry} 次尝试连接失败,剩余 {remaining_times} 次重试机会") + else: + self.logger.error(f"第 {current_retry} 次尝试连接失败,已达到最大重试次数({max_retry_times}次)") + # 循环结束:3次均失败,返回False + return False + else: + self.logger.info("第一个设备是'没有已配对的设备',不点击,等待用户手动连接") + return self._wait_for_manual_connection() + + else: + self.logger.info("没有找到已配对设备") + return False + + except TimeoutException: + self.logger.info("没有已配对设备;配对设备失败") + return False + + except Exception as e: + self.logger.error(f"连接设备过程中出错: {str(e)}") + return False + + def _handle_alert_dialog(self): + """处理连接蓝牙失败警告弹窗""" + try: + # 等待弹窗出现(短暂等待) + alert_dialog = WebDriverWait(self.driver, 5).until( + EC.visibility_of_element_located((AppiumBy.ID, "android:id/content")) + ) + + # 查找关闭报警按钮 + close_alert_btn = WebDriverWait(self.driver, 3).until( + EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[@text='关闭报警']")) + ) + + close_alert_btn.click() + self.logger.info("已点击'关闭报警'按钮") + + # # 等待弹窗消失 + # WebDriverWait(self.driver, 5).until( + # EC.invisibility_of_element_located((AppiumBy.ID, "android:id/content")) + # ) + # self.logger.info("弹窗已关闭") + + return True + + except TimeoutException: + self.logger.info("未检测到弹窗,继续正常流程") + return False + except Exception as e: + self.logger.error(f"处理弹窗时出错: {str(e)}") + return False + + def _retry_connection(self, paired_devices_list_xpath): + """重新尝试连接设备""" + try: + # 再次点击连接蓝牙设备按钮 + connect_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.CONNECT_LEVEL_METER)) + ) + connect_btn.click() + self.logger.info("已重新点击连接蓝牙设备按钮") + + # 等待设备列表重新出现 + paired_list = self.wait.until( + EC.visibility_of_element_located((AppiumBy.XPATH, paired_devices_list_xpath)) + ) + + # 获取设备列表并点击第一个设备 + device_items = paired_list.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + if device_items and "没有已配对的设备" not in device_items[0].text: + first_device = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, f"{paired_devices_list_xpath}/android.widget.TextView[1]")) + ) + first_device.click() + self.logger.info("已重新点击第一个设备") + + # 检查连接状态 + return self._check_connection_status() + else: + self.logger.warning("重新尝试时未找到可用设备") + return False + + except Exception as e: + self.logger.error(f"重新尝试连接时出错: {str(e)}") + return False + + def _check_connection_status(self): + """检查连接状态""" + try: + # 等待连接状态更新 + time.sleep(3) + + conn_level_btn = self.driver.find_element(AppiumBy.ID, ids.CONNECT_LEVEL_METER) + if "已连接上" in conn_level_btn.text: + self.logger.info(f"蓝牙设备连接成功: {conn_level_btn.text}") + return True + else: + self.logger.warning(f"蓝牙设备连接失败: {conn_level_btn.text}") + return False + except NoSuchElementException: + self.logger.warning("未找到连接按钮") + return False + + def _wait_for_manual_connection(self): + """等待用户手动连接""" + max_wait_time = 60 # 总最大等待时间:600秒 + poll_interval = 5 # 每次检查后的休眠间隔:30秒 + btn_wait_time = 15 # 单个循环内,等待"连接按钮"出现的最大时间:15秒 + start_time = time.time() # 记录总等待的开始时间戳 + + while time.time() - start_time < max_wait_time: + conn_level_btn = None # 初始化按钮对象,避免上一轮残留值影响 + try: + # 第一步:先等待15秒,直到按钮出现或超时(解决按钮延迟加载问题) + conn_level_btn = WebDriverWait(self.driver, btn_wait_time).until( + EC.presence_of_element_located((AppiumBy.ID, ids.CONNECT_LEVEL_METER)) + ) + self.logger.debug("已检测到连接按钮") + + if "已连接上" in conn_level_btn.text: + self.logger.info(f"用户手动连接成功: {conn_level_btn.text}") + return True + else: + # 未连接,计算总剩余等待时间并记录 + elapsed_total = time.time() - start_time + remaining_total = max_wait_time - elapsed_total + self.logger.debug(f"等待手动连接中...总剩余时间: {remaining_total:.0f}秒") + # 情况1:10秒内未找到按钮(btn_wait_time超时) + except TimeoutException: + elapsed_total = time.time() - start_time + remaining_total = max_wait_time - elapsed_total + self.logger.warning(f"未检测到连接按钮,{remaining_total:.0f}秒内将再次检查") + + # 情况2:其他意外异常(如驱动异常) + except Exception as e: + self.logger.error(f"检查连接状态时出现意外错误: {str(e)}") + + # 无论是否找到按钮,都休眠poll_interval秒再进入下一轮循环 + time.sleep(poll_interval) + + self.logger.warning(f"等待{max_wait_time}秒后仍未连接成功,终止等待") + return False + + def wait_for_measurement_data(self, timeout=900): + """ + 等待并轮询测量数据接口,每10秒访问一次,直到有数据返回 + + 参数: + timeout: 最大等待时间(秒) + + 返回: + bool: 是否成功获取到数据 + """ + try: + start_time = time.time() + + while time.time() - start_time < timeout: + try: + task_data = apis.get_end_with_num() + if not task_data: + # self.logger.info("接口返回但没有数据,继续等待...") + time.sleep(10) + # # 1. 获取屏幕尺寸,计算中心坐标(通用适配所有设备) + # screen_size = self.driver.get_window_size() + # center_x = screen_size['width'] / 2 + # center_y = screen_size['height'] / 2 + # # 2. 点击屏幕中心(点击时长500ms,和常规操作一致) + # self.driver.tap([(center_x, center_y)], 500) + # self.logger.info(f"已点击屏幕中心(坐标:{center_x}, {center_y}),间隔30秒触发") + + continue + self.logger.info(f"接口返回数据:{task_data}") + if task_data.get('status') == 3: + self.logger.info("测量任务状态为3,测量结束") + return True + else: + self.logger.info("测量任务状态不为3,继续等待...") + continue + except Exception as e: + self.logger.error(f"处理接口响应时出错: {str(e)}") + + self.logger.error(f"在 {timeout} 秒内未获取到有效数据") + return False + + except Exception as e: + self.logger.error(f"等待测量结束时发生错误: {str(e)}") + return False + + + def wait_for_measurement_end(self, timeout=900): + """ + 等待按钮变成"测量结束",最多15分钟,包含驱动重新初始化机制 + + Args: + timeout: 超时时间,默认900秒(15分钟) + + Returns: + bool: 是否成功等到测量结束按钮 + """ + try: + # 更新WebDriverWait等待时间为900秒 + self.wait = WebDriverWait(self.driver, 900) + self.logger.info(f"等待测量结束按钮出现,最多等待 {timeout} 秒") + + start_time = time.time() + reinit_attempts = 0 + max_reinit_attempts = 3 # 最大重新初始化次数 + + while time.time() - start_time < timeout: + try: + # 使用ID查找测量控制按钮 + measure_btn = self.driver.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/btn_control_begin_or_end" + ) + + if "测量结束" in measure_btn.text: + self.logger.info("检测到测量结束按钮,暂停两秒等待电脑执行点击任务") + time.sleep(5) + return True + + except NoSuchElementException: + # 按钮未找到,继续等待 + pass + except Exception as e: + error_msg = str(e) + self.logger.warning(f"查找测量结束按钮时出现异常: {error_msg}") + + # 检测是否是UiAutomator2服务崩溃 + if 'UiAutomator2 server' in error_msg and 'instrumentation process is not running' in error_msg and reinit_attempts < max_reinit_attempts: + reinit_attempts += 1 + self.logger.info(f"检测到UiAutomator2服务崩溃,尝试第 {reinit_attempts} 次重新初始化驱动") + + # 尝试重新初始化驱动 + if self._reinit_driver(): + self.logger.info("驱动重新初始化成功") + else: + self.logger.error("驱动重新初始化失败") + # 继续尝试,而不是立即失败 + + # 等待一段时间后再次检查 + time.sleep(5) + + # 每30秒输出一次等待状态 + if int(time.time() - start_time) % 30 == 0: + elapsed = int(time.time() - start_time) + self.logger.info(f"已等待 {elapsed} 秒,仍在等待测量结束...") + + self.logger.error("等待测量结束按钮超时") + return False + + except Exception as e: + self.logger.error(f"等待测量结束时发生严重错误: {str(e)}") + return False + + def _reinit_driver(self): + """ + 重新初始化Appium驱动 + + Returns: + bool: 是否成功重新初始化 + """ + try: + # 首先尝试关闭现有的驱动 + if hasattr(self, 'driver') and self.driver: + try: + self.driver.quit() + except: + self.logger.warning("关闭现有驱动时出现异常") + + # 导入必要的模块 + from appium import webdriver + from appium.options.android import UiAutomator2Options + + # 重新创建驱动配置 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = self.device_id + options.app_package = "com.bjjw.cjgc" + options.app_activity = ".activity.LoginActivity" + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 300 + options.udid = self.device_id + + # 重新连接驱动 + self.logger.info(f"正在重新初始化设备 {self.device_id} 的驱动...") + self.driver = webdriver.Remote("http://localhost:4723", options=options) + + # 重新初始化等待对象 + from selenium.webdriver.support.ui import WebDriverWait + self.wait = WebDriverWait(self.driver, 20) + + self.logger.info(f"设备 {self.device_id} 驱动重新初始化完成") + return True + + except Exception as e: + self.logger.error(f"设备 {self.device_id} 驱动重新初始化失败: {str(e)}") + return False + + def click_start_measure_btn(self): + """点击开始测量按钮并处理确认弹窗""" + try: + # 查找并点击开始测量按钮 + start_measure_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/btn_control_begin_or_end")) + ) + + # 检查按钮文本 + btn_text = start_measure_btn.text + self.logger.info(f"测量按钮文本: {btn_text}") + + if "点击开始测量" in btn_text or "开始测量" in btn_text: + start_measure_btn.click() + self.logger.info("已点击开始测量按钮") + + # 处理确认弹窗 + dialog_result = self._handle_start_measure_confirm_dialog() + + # 如果确认弹窗处理成功,检查线路前测表,查看是否要添加转点 + if not dialog_result: + logging.error(f"设备 {self.device_id} 处理开始测量弹窗失败") + return False + + else: + self.logger.warning(f"测量按钮状态不是开始测量,当前状态: {btn_text}") + return False + + return True + + + except TimeoutException: + self.logger.error("等待开始测量按钮超时") + return False + except Exception as e: + self.logger.error(f"点击开始测量按钮时出错: {str(e)}") + return False + + + def add_transition_point(self): + """添加转点""" + try: + # 查找并点击添加转点按钮 + add_transition_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/btn_add_ZPoint")) + ) + add_transition_btn.click() + self.logger.info("已点击添加转点按钮") + return True + except TimeoutException: + self.logger.error("等待添加转点按钮超时") + return False + except Exception as e: + self.logger.error(f"添加转点时出错: {str(e)}") + return False + + # def chang_status(self): + # """修改断点状态1->2""" + # try: + # # 修改断点状态1->2 + # user_name = global_variable.GLOBAL_USERNAME + # line_num = global_variable.GLOBAL_LINE_NUM + + # if line_num: + # success = apis.change_breakpoint_status(user_name, line_num, 2) + # if success: + # self.logger.info(f"成功修改断点状态: 线路{line_num} 状态1->2") + # return True + # else: + # self.logger.error(f"修改断点状态失败: 线路{line_num} 状态1->2") + # return False + # else: + # self.logger.warning("未找到线路编码,跳过修改断点状态") + # return False + # except Exception as e: + # self.logger.error(f"修改状态时出错: {str(e)}") + # return False + + + def _handle_start_measure_confirm_dialog(self): + """处理开始测量确认弹窗""" + try: + # 等待确认弹窗出现 + confirm_dialog = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/content")) + ) + self.logger.info("检测到开始测量确认弹窗") + + # 检查弹窗标题和消息 + try: + title_element = self.driver.find_element(AppiumBy.ID, "android:id/alertTitle") + message_element = self.driver.find_element(AppiumBy.ID, "android:id/message") + self.logger.info(f"弹窗标题: {title_element.text}, 消息: {message_element.text}") + except NoSuchElementException: + self.logger.info("无法获取弹窗详细信息") + + # 点击"是"按钮 + yes_button = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button1")) + ) + + if yes_button.text == "是": + yes_button.click() + self.logger.info("已点击'是'按钮确认开始测量") + + # # 等待弹窗消失 + # WebDriverWait(self.driver, 5).until( + # EC.invisibility_of_element_located((AppiumBy.ID, "android:id/content")) + # ) + self.logger.info("确认弹窗已关闭") + return True + else: + self.logger.error(f"确认按钮文本不是'是',实际文本: {yes_button.text}") + return False + + except TimeoutException: + self.logger.warning("未检测到开始测量确认弹窗,可能不需要确认") + return True # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"处理开始测量确认弹窗时出错: {str(e)}") + return False + + def click_system_back_button(self): + """点击手机系统返回按钮""" + try: + self.driver.back() + self.logger.info("已点击手机系统返回按钮") + return True + except Exception as e: + self.logger.error(f"点击手机系统返回按钮失败: {str(e)}") + return False + + def add_breakpoint_to_tested_list(self): + """添加测量结束的断点到列表和字典""" + breakpoint_name = global_variable.GLOBAL_CURRENT_PROJECT_NAME + line_num = global_variable.GLOBAL_LINE_NUM + if breakpoint_name and breakpoint_name not in global_variable.GLOBAL_TESTED_BREAKPOINT_LIST: + global_variable.GLOBAL_TESTED_BREAKPOINT_LIST.append(breakpoint_name) + global_variable.GLOBAL_BREAKPOINT_DICT[breakpoint_name] = { + 'breakpoint_name': breakpoint_name, + 'line_num': line_num + } + + logging.info(f"成功添加断点 '{breakpoint_name}' 到上传列表") + logging.info(f"断点详细信息: 线路编码={line_num}") + return True + else: + logging.warning(f"断点名为空或已存在于列表中") + return False + + def click_back_button(self): + """点击手机系统返回按钮""" + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {self.device_id} 驱动重连失败") + try: + self.driver.back() + self.logger.info("已点击手机系统返回按钮") + return True + except Exception as e: + self.logger.error(f"点击手机系统返回按钮失败: {str(e)}") + return False + + # def check_listview_stability(self, timeout=20, poll_interval=2, target_count=2): + # """ + # 检查列表视图中级2 LinearLayout数量的稳定性 + + # 参数: + # timeout: 超时时间(秒),默认20秒 + # poll_interval: 轮询间隔(秒),默认2秒 + # target_count: 目标数量,默认2个 + + # 返回: + # str: + # - "flash": 20秒内数量无变化 + # - "error": 变化后20秒内未达到目标数量 + # - "stable": 达到目标数量并保持稳定 + # """ + # listview_id = "com.bjjw.cjgc:id/auto_data_list" + # start_time = time.time() + # last_count = None + # change_detected = False + # change_time = None + + # self.logger.info(f"开始监控列表 {listview_id} 中级2 LinearLayout数量,目标数量: {target_count},超时时间: {timeout}秒") + + # try: + # while time.time() - start_time < timeout: + # try: + # # 获取当前层级2 LinearLayout数量 + # current_count = self._get_level2_linear_layout_count() + + # # 首次获取或数量发生变化 + # if last_count is None: + # self.logger.info(f"初始层级2 LinearLayout数量: {current_count}") + # last_count = current_count + # change_time = time.time() + # elif current_count != last_count: + # self.logger.info(f"层级2 LinearLayout数量发生变化: {last_count} -> {current_count}") + # last_count = current_count + # change_detected = True + # change_time = time.time() + + # # 检查是否达到目标数量 + # if current_count >= target_count: + # self.logger.info(f"已达到目标数量 {target_count},继续监控稳定性") + # # 重置计时器,继续监控是否稳定 + # start_time = time.time() + + # # 检查是否在变化后20秒内未达到目标数量 + # if change_detected and change_time and (time.time() - change_time) > timeout: + # if last_count < target_count: + # self.logger.error(f"变化后{timeout}秒内未达到目标数量{target_count},当前数量: {last_count}") + # return "error" + + # # 检查是否20秒内无变化 + # if change_time and (time.time() - change_time) > timeout: + # self.logger.info(f"层级2 LinearLayout数量在{timeout}秒内无变化,返回flash") + # return "flash" + + # time.sleep(poll_interval) + + # except StaleElementReferenceException: + # self.logger.warning("元素已过时,重新获取") + # continue + # except Exception as e: + # self.logger.error(f"获取层级2 LinearLayout数量时出错: {str(e)}") + # if change_detected and change_time and (time.time() - change_time) > timeout: + # return "error" + # else: + # time.sleep(poll_interval) + # continue + + # # 超时处理 + # if change_detected: + # if last_count >= target_count: + # self.logger.info(f"已达到目标数量{target_count}并保持稳定") + # return "stable" + # else: + # self.logger.error(f"变化后{timeout}秒内未达到目标数量{target_count},当前数量: {last_count}") + # return "error" + # else: + # self.logger.info(f"层级2 LinearLayout数量在{timeout}秒内无变化,返回flash") + # return "flash" + + # except Exception as e: + # self.logger.error(f"监控列表稳定性时出错: {str(e)}") + # return "error" + + # def _get_level2_linear_layout_count(self): + # """ + # 获取层级2 LinearLayout的数量 + + # 返回: + # int: 层级2 LinearLayout的数量 + # """ + # try: + # # 定位到ListView + # listview = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/auto_data_list") + + # # 获取层级2的LinearLayout(ListView的直接子元素) + # # 使用XPath查找直接子元素 + # level2_layouts = listview.find_elements(AppiumBy.XPATH, "./android.widget.LinearLayout") + + # count = len(level2_layouts) + # self.logger.debug(f"当前层级2 LinearLayout数量: {count}") + + # return count + + # except NoSuchElementException: + # self.logger.error(f"未找到列表元素: com.bjjw.cjgc:id/auto_data_list") + # return 0 + # except Exception as e: + # self.logger.error(f"获取层级2 LinearLayout数量时出错: {str(e)}") + # return 0 + + def handle_measurement_dialog(self): + """处理测量弹窗 - 选择继续测量""" + try: + self.logger.info("检查线路弹出测量弹窗...") + + # 直接尝试点击"继续测量"按钮 + remeasure_btn = WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/measure_remeasure_all_btn")) + ) + remeasure_btn.click() + self.logger.info("已点击'重新测量'按钮") + try: + WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button1")) + ) + except TimeoutException: + self.logger.info("未找到'是'按钮,可能弹窗未出现") + return False + + confirm_btn = WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button1")) + ) + confirm_btn.click() + self.logger.info("已点击'是'按钮") + return True + + except TimeoutException: + self.logger.info("未找到继续测量按钮,可能没有弹窗") + return True # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"点击线路测量弹窗按钮时出错: {str(e)}") + return False + def section_mileage_config_page_manager(self, address=None, obs_type=None): + """执行完整的断面里程配置""" + try: + + if not self.handle_measurement_dialog(): + self.logger.error("检测到线路测量弹窗,但处理测量弹窗失败") + return False + + self.logger.info("检测到线路测量弹窗,且处理测量弹窗成功") + + if not self.is_on_config_page(): + self.logger.error("不在断面里程配置页面") + return False + + # 下滑查找所有必要元素 + required_elements = [ + ids.MEASURE_WEATHER_ID, + ids.MEASURE_TYPE_ID, + ids.MEASURE_TEMPERATURE_ID, + "com.bjjw.cjgc:id/point_list_barometric_et" + ] + + if not self.scroll_to_find_all_elements(required_elements): + self.logger.warning("未找到所有必要元素,但继续尝试配置") + + # 临时地址 + address = apis.get_one_addr(global_variable.GLOBAL_USERNAME) or "四川省资阳市" + + + # 获取实时天气信息 + if address: + weather, temperature, pressure = get_weather_simple(address) + self.logger.info(f"获取到实时天气: {weather}, 温度: {temperature}°C, 气压: {pressure}hPa") + else: + # 使用默认值 + weather, temperature, pressure = "阴", 25.0, 720.0 + + # 选择天气 + if not self.select_weather(weather): + return False + + time.sleep(1) # 短暂等待 + + # 选择观测类型(如果未指定,根据工作基点自动选择) + if not self.select_observation_type(obs_type): + return False + + time.sleep(1) # 短暂等待 + + # 填写温度 + if not self.enter_temperature(temperature): + return False + + time.sleep(1) # 短暂等待 + + # 填写气压 + if not self.enter_barometric_pressure(pressure): + return False + + time.sleep(1) # 短暂等待 + + + # 点击保存 + if not self.click_save_button(): + return False + + # 连接水准仪 + if not self.click_conn_level_btn(): + return False + + # 连接蓝牙设备 + if not self.connect_to_device(): + return False + + # 连接设备成功,要点击“点击开始测量” + if not self.click_start_measure_btn(): + return False + + # if not self.check_station_page.run(): + # self.logger.error("检查站页面运行失败") + # return False + + + # # 添加断点到列表 + # if not self.add_breakpoint_to_tested_list(): + # return False + + # # 点击返回按钮 + # if not self.click_back_button(): + # return False + + # # 测量结束。点击手机物理返回按钮,返回测量页面 + # # 点击了手机独步导航栏返回键 + # if not self.click_system_back_button(): + # return False + + self.logger.info("断面里程配置完成,正在执行测量") + return True + + except Exception as e: + self.logger.error(f"断面里程配置过程中出错: {str(e)}") + return False \ No newline at end of file diff --git a/page_objects/upload_config_page.py b/page_objects/upload_config_page.py new file mode 100644 index 0000000..faf178e --- /dev/null +++ b/page_objects/upload_config_page.py @@ -0,0 +1,2227 @@ +#上传配置页面 +# \page_objects\test_upload_config_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +import logging +import time +import os +import re +import pandas as pd +from datetime import datetime +from typing import Dict, Optional, List + +from page_objects.more_download_page import MoreDownloadPage +from globals.driver_utils import check_session_valid, reconnect_driver, go_main_click_tabber_button # 导入会话检查和重连函数 +import globals.apis as apis +import globals.global_variable as global_variable + +class UploadConfigPage: + def __init__(self, driver, wait, device_id): + self.driver = driver + self.wait = wait + self.logger = logging.getLogger(__name__) + self.more_download_page = MoreDownloadPage(driver, wait,device_id) + self.device_id = device_id + + def go_upload_config_page(self): + """点击img_2_layout(上传页面按钮)""" + try: + # 在执行操作前检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning("会话已失效,尝试重新连接...") + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("重新连接成功") + # 首先获取当前页面信息进行调试 + try: + current_activity = self.driver.current_activity + self.logger.info(f"当前Activity: {current_activity}") + except Exception as e: + self.logger.error(f"获取当前activity时出错: {str(e)}") + + # 尝试返回到主页面(如果不在主页面) + self.logger.info("尝试返回到主页面...") + max_back_presses = 5 # 最多按返回键次数 + back_press_count = 0 + + while back_press_count < max_back_presses: + try: + # 检查是否已经在主页面(通过检查主页面特征元素) + # 先尝试查找上传页面按钮 + try: + main_page_indicator = self.driver.find_element( + AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout" + ) + if main_page_indicator.is_displayed(): + self.logger.info("已在主页面,找到上传按钮") + break + except: + # 未找到主页面元素,继续返回 + pass + + # 按返回键 + self.driver.back() + back_press_count += 1 + self.logger.info(f"已按返回键 {back_press_count} 次") + time.sleep(1) # 等待页面响应 + except Exception as e: + self.logger.error(f"按返回键时出错: {str(e)}") + break + + # 现在尝试点击上传页面按钮 + self.logger.info("尝试点击上传页面按钮") + try: + # 使用较短的等待时间,因为我们需要快速响应 + upload_page_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout")) + ) + upload_page_btn.click() + self.logger.info("已点击img_2_layout,进入上传页面") + time.sleep(2) # 增加等待时间确保页面加载完成 + return True + except TimeoutException: + self.logger.warning("快速查找上传按钮超时,尝试使用更长的等待时间") + # 使用更长的等待时间再次尝试 + upload_page_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout")) + ) + upload_page_btn.click() + self.logger.info("已点击img_2_layout,进入上传页面") + time.sleep(2) # 增加等待时间确保页面加载完成 + return True + except TimeoutException: + self.logger.error("等待上传页面按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击上传页面按钮时出错: {str(e)}") + return False + + def check_change_amount_on_page(self): + """直接检查上传配置页面中是否包含变化量属性""" + try: + # 查找页面中是否包含"变化量"文本 + change_amount_elements = self.driver.find_elements( + AppiumBy.XPATH, + "//*[contains(@text, '变化量')]" + ) + + # 如果找到包含"变化量"文本的元素,返回True + if change_amount_elements: + self.logger.info("页面中包含变化量属性") + return True + else: + self.logger.info("页面中未找到变化量属性") + return False + + except Exception as e: + self.logger.error(f"检查变化量属性时出错: {str(e)}") + return False + + def click_upload_by_breakpoint_name(self, breakpoint_name): + """根据断点名称点击上传按钮""" + logging.info(f"需要点击上传按钮,断点名称:{breakpoint_name}") + try: + search_text = "" + # 提取关键部分进行模糊匹配 + if breakpoint_name.endswith('平原') and '-' in breakpoint_name: + # 从右边分割一次,取第一部分(去掉末尾的“-xxx”) + search_text = breakpoint_name.rsplit('-', 1)[0] + self.logger.info(f"处理后搜索文本:{search_text}") + else: + search_text = breakpoint_name + + # 找到包含指定断点名称的itemContainer + item_container_xpath = f"//android.widget.LinearLayout[@resource-id='com.bjjw.cjgc:id/itemContainer']//android.widget.TextView[@resource-id='com.bjjw.cjgc:id/title' and @text='{search_text}']/ancestor::android.widget.LinearLayout[@resource-id='com.bjjw.cjgc:id/itemContainer']" + + # 等待itemContainer出现 + item_container = self.wait.until( + EC.presence_of_element_located((AppiumBy.XPATH, item_container_xpath)) + ) + # self.logger.info(f"找到包含断点 {breakpoint_name} 的itemContainer") + + # 在itemContainer中查找上传按钮 + upload_btn = item_container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/upload_btn" + ) + + # 点击上传按钮 + upload_btn.click() + self.logger.info(f"已点击断点 {breakpoint_name} 的上传按钮") + + # 等待上传操作开始 + time.sleep(3) + + # # 检查上传是否开始 + # try: + # upload_indicator = WebDriverWait(self.driver, 20).until( + # EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '上传') or contains(@text, 'Upload')]")) + # ) + # self.logger.info(f"上传操作已开始: {upload_indicator.text}") + # except TimeoutException: + # self.logger.warning("未检测到明确的上传开始提示,但按钮点击已完成") + + return True + + except TimeoutException: + self.logger.error(f"等待断点 {breakpoint_name} 的上传按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"根据断点名称点击上传按钮时出错: {str(e)}") + return False + + def handle_upload_dialog(self): + """处理上传弹窗,点击已同步按钮""" + try: + # 等待弹窗出现 + # time.sleep(2) + + # 检查弹窗是否出现 + dialog_indicators = [ + (AppiumBy.ID, "android:id/alertTitle"), + (AppiumBy.XPATH, "//android.widget.TextView[@text='提示']"), + (AppiumBy.ID, "android:id/message") + ] + + dialog_appeared = False + for by, value in dialog_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + dialog_appeared = True + self.logger.info("检测到上传确认弹窗") + break + except: + continue + + if not dialog_appeared: + self.logger.warning("未检测到上传确认弹窗,可能不需要确认") + return True + + # 点击"已同步"按钮 + synced_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button2")) + ) + synced_btn.click() + self.logger.info("已点击'已同步'按钮") + + # 等待弹窗消失 + time.sleep(2) + + # 检查弹窗是否消失 + try: + # 检查弹窗是否还存在 + dialog_still_exists = False + for by, value in dialog_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + dialog_still_exists = True + break + except: + continue + + if not dialog_still_exists: + self.logger.info("上传确认弹窗已消失") + return True + else: + self.logger.warning("上传确认弹窗仍然存在") + return False + + except Exception as e: + self.logger.info("上传确认弹窗可能已消失") + return True + + except TimeoutException: + self.logger.error("等待上传对话框可点击超时") + return False + except Exception as e: + self.logger.error(f"处理上传弹窗时出错: {str(e)}") + return False + + def execute_download_operation(self): + """执行下载操作:按返回键并点击img_5_layout""" + try: + # 按一次返回键 + self.driver.back() + self.logger.info("已按返回键") + time.sleep(1) + + # 点击img_5_layout(更多下载按钮) + more_download_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout")) + ) + more_download_btn.click() + self.logger.info("已点击更多下载按钮") + + # 等待页面加载 + time.sleep(1) + + # 调用更多下载页面的方法对象 + try: + logging.info(f"设备开始执行更多下载页面测试") + + # 执行更多下载页面管理操作 + success = self.more_download_page.more_download_page_manager() + + if success: + logging.info(f"设备更多下载页面测试执行成功") + # 按一次返回键 + self.driver.back() + self.logger.info("已按返回键,返回到更多下载页面,准备点击上传导航按钮") + return True + else: + logging.error(f"设备更多下载页面测试执行失败") + return False + + except Exception as e: + logging.error(f"设备运行更多下载页面测试时出错: {str(e)}") + # self.take_screenshot("more_download_test_error.png") + return False + + + except TimeoutException: + self.logger.error("等待下载操作元素可点击超时") + return False + except Exception as e: + self.logger.error(f"执行下载操作时出错: {str(e)}") + return False + + def get_point_data(self): + """ + 获取三个测点的数据 + """ + point_data = [] + + try: + # 等待页面加载 + time.sleep(1) + + # 方法1: 通过resource-id获取所有测点名称 + point_name_elements = self.driver.find_elements( + AppiumBy.ID, + 'com.bjjw.cjgc:id/improve_point_name' + ) + + # 方法2: 通过text内容获取测点数据元素 + point_value_elements = self.driver.find_elements( + AppiumBy.ID, + 'com.bjjw.cjgc:id/point_values' + ) + + self.logger.info(f"找到 {len(point_name_elements)} 个测点") + self.logger.info(f"找到 {len(point_value_elements)} 个数据元素") + + # 提取每个测点的数据 + for i, (name_element, value_element) in enumerate(zip(point_name_elements, point_value_elements)): + point_info = {} + + # 获取测点名称 + point_name = name_element.text + point_info['point_name'] = point_name + + # 获取测点数据 + point_value = value_element.text + point_info['point_value'] = point_value + + # 解析详细数据 + try: + # 解析数据格式: "本期:13679.07623m; \n上期:13679.07621m; \n变化量:0.02mm; \n测量时间:2025-10-10 15:47:25.049" + data_parts = point_value.split(';') + parsed_data = {} + + for part in data_parts: + part = part.strip() + if '本期:' in part: + parsed_data['current_value'] = part.replace('本期:', '').strip() + elif '上期:' in part: + parsed_data['previous_value'] = part.replace('上期:', '').strip() + elif '变化量:' in part: + parsed_data['change_amount'] = part.replace('变化量:', '').strip() + elif '测量时间:' in part: + parsed_data['measurement_time'] = part.replace('测量时间:', '').strip() + + point_info['parsed_data'] = parsed_data + + except Exception as e: + self.logger.warning(f"解析数据时出错: {e}") + point_info['parsed_data'] = {} + + point_data.append(point_info) + + self.logger.info(f"测点 {i+1}: {point_name}") + # self.logger.info(f"完整数据: {point_value}") + + except Exception as e: + self.logger.error(f"获取数据时出错: {e}") + + return point_data + + def get_specific_point_data(self, point_name): + """ + 获取特定测点的数据 + """ + try: + # 通过文本内容查找特定测点 + point_name_element = self.driver.find_element( + AppiumBy.XPATH, + f'//android.widget.TextView[@resource-id="com.bjjw.cjgc:id/improve_point_name" and @text="{point_name}"]' + ) + + # 找到对应的数据元素 - 可能需要根据实际结构调整XPath + point_value_element = point_name_element.find_element( + AppiumBy.XPATH, + './following::android.widget.TextView[@resource-id="com.bjjw.cjgc:id/point_values"]' + ) + + return point_value_element.text + + except Exception as e: + self.logger.error(f"获取特定测点 {point_name} 数据时出错: {e}") + return None + + def swipe_up(self, start_y=1500, end_y=300, duration=500): + """ + 从指定起始Y坐标滑动到结束Y坐标(向上滑动页面) + + 参数: + start_y: 起始Y坐标 (默认1500) + end_y: 结束Y坐标 (默认300) + duration: 滑动持续时间(毫秒) (默认500) + """ + try: + # 直接使用固定的X坐标(屏幕中间) + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + x = 540 # 假设屏幕宽度为 1080,取中间值 + + # 执行滑动操作 + self.driver.swipe(x, start_y, x, end_y, duration) + self.logger.info(f"页面已从Y:{start_y}滑动到Y:{end_y}") + + # 滑动后等待页面稳定 + time.sleep(1) + return True + + except Exception as e: + self.logger.error(f"滑动页面时出错: {e}") + return False + + def swipe_down(self, start_y=175, end_y=1310, duration=500): + """ + 从指定起始Y坐标滑动到结束Y坐标(向下滑动页面) + + 参数: + start_y: 起始Y坐标 (默认407) + end_y: 结束Y坐标 (默认1617) + duration: 滑动持续时间(毫秒) (默认500) + """ + try: + # 直接使用固定的X坐标(屏幕中间) + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + x = 540 # 假设屏幕宽度为 1080,取中间值 + + # 执行滑动操作(向下滑动) + self.driver.swipe(x, start_y, x, end_y, duration) + self.logger.info(f"页面已从Y:{start_y}滑动到Y:{end_y}(向下滑动)") + + # 滑动后等待页面稳定 + time.sleep(1) + return True + + except Exception as e: + self.logger.error(f"向下滑动页面时出错: {e}") + return False + + def is_on_upload_config_page(self): + """通过"保存上传"按钮来确定是否在上传配置页面""" + try: + # 使用"保存上传"按钮的resource-id来检查 + save_upload_btn_locator = (AppiumBy.ID, "com.bjjw.cjgc:id/improve_save_btn") + self.wait.until(EC.presence_of_element_located(save_upload_btn_locator)) + self.logger.info("已确认在上传配置页面") + return True + except TimeoutException: + self.logger.warning("未找到保存上传按钮,不在上传配置页面") + return False + except Exception as e: + self.logger.error(f"检查上传配置页面时发生意外错误: {str(e)}") + return False + + def collect_all_point_data(self, results_dir): + """循环滑动收集所有测点数据,直到没有新数据出现""" + all_point_data = [] + seen_point_names = set() # 用于跟踪已经见过的测点名称 + max_scroll_attempts = 20 # 最大滑动次数,防止无限循环 + scroll_attempt = 0 + + self.logger.info("开始循环滑动收集所有测点数据...") + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次尝试获取数据...") + + # 获取当前屏幕的测点数据 + current_point_data = self.get_point_data() + + if not current_point_data: + self.logger.info("当前屏幕没有测点数据,停止滑动") + break + + # 统计新发现的测点 + new_points_count = 0 + for point in current_point_data: + point_name = point.get('point_name') + if point_name and point_name not in seen_point_names: + # 新测点,添加到结果集 + all_point_data.append(point) + seen_point_names.add(point_name) + new_points_count += 1 + + self.logger.info(f"本次获取到 {len(current_point_data)} 个测点,其中 {new_points_count} 个是新测点") + + # 如果没有新数据,停止滑动 + if new_points_count == 0: + self.logger.info("没有发现新测点,停止滑动") + break + + # 滑动到下一页 + self.logger.info("滑动到下一页...") + if not self.swipe_up(): + self.logger.warning("滑动失败,停止收集") + break + + # 等待页面稳定 + time.sleep(1) + + self.logger.info(f"数据收集完成,共获取 {len(all_point_data)} 个测点数据") + return all_point_data + + def collect_check_all_point_data(self, max_variation): + """循环滑动收集所有测点数据,直到没有新数据出现""" + all_point_data = [] + seen_point_names = set() # 用于跟踪已经见过的测点名称 + max_scroll_attempts = 100 # 最大滑动次数,防止无限循环 + scroll_attempt = 0 + + self.logger.info("开始循环滑动收集所有测点数据...") + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次尝试获取数据...") + + # 获取当前屏幕的测点数据 + current_point_data = self.get_point_data() + + if not current_point_data: + self.logger.info("当前屏幕没有测点数据,停止滑动") + break + + # 统计新发现的测点 + new_points_count = 0 + for point in current_point_data: + point_name = point.get('point_name') + if point_name and point_name not in seen_point_names: + # 新测点,添加到结果集 + all_point_data.append(point) + seen_point_names.add(point_name) + new_points_count += 1 + + self.logger.info(f"本次获取到 {len(current_point_data)} 个测点,其中 {new_points_count} 个是新测点") + + # 如果没有新数据,停止滑动 + if new_points_count == 0: + self.logger.info("没有发现新测点,停止滑动") + break + + # 滑动到下一页 + self.logger.info("滑动到下一页...") + if not self.swipe_up(): + self.logger.warning("滑动失败,停止收集") + break + + # 等待页面稳定 + time.sleep(0.2) + + self.logger.info(f"数据收集完成,共获取 {len(all_point_data)} 个测点数据") + + # 直接比对每个测点的变化量 + if max_variation is None: + self.logger.error("获取用户最大变化量失败") + max_variation = 2 + # return False + + self.logger.info(f"开始比对测点变化量,最大允许变化量: {max_variation}mm") + + for i, point in enumerate(all_point_data, 1): + point_name = point.get('point_name', '未知') + point_value = point.get('point_value', '') + + # 从完整数据中提取变化量(格式如:变化量:-0.67mm;) + change_amount_match = re.search(r'变化量:([-\d.]+)mm', point_value) + if change_amount_match: + try: + change_amount = float(change_amount_match.group(1)) + # self.logger.info(f"测点 {point_name} 变化量: {change_amount}mm, 最大允许变化量: {max_variation}mm") + + # 比较绝对值,因为变化量可能是负数 + if abs(change_amount) > max_variation: + self.logger.error(f"测点 {point_name} 变化量 {change_amount}mm 超过最大允许值 {max_variation}mm") + return False + except ValueError as e: + self.logger.error(f"解析测点 {point_name} 的变化量失败: {str(e)},原始数据: {point_value}") + return False + else: + self.logger.error(f"在测点 {point_name} 的数据中未找到变化量信息,原始数据: {point_value}") + return False + + self.logger.info(f"所有测点变化量均在允许范围内(≤{max_variation}mm)") + return True + + # def _load_user_data(self): + # """加载用户数据从Excel文件""" + # try: + # # 默认路径:当前脚本的上一级目录下的"上传人员信息.xlsx" + # current_dir = os.path.dirname(os.path.abspath(__file__)) + # parent_dir = os.path.dirname(current_dir) + # excel_path = os.path.join(parent_dir, "上传人员信息.xlsx") + + # if not os.path.exists(excel_path): + # logging.error(f"Excel文件不存在: {excel_path}") + # return False + + # # 读取Excel文件 + # df = pd.read_excel(excel_path, sheet_name='Sheet1') + + # # 处理合并单元格 - 前向填充标段列 + # df['标段'] = df['标段'].fillna(method='ffill') + + # # 清理数据:去除空行和无效数据 + # df = df.dropna(subset=['测量人员信息']) + # df = df[df['测量人员信息'].str.strip() != ''] + + # # 创建姓名到身份证的映射 + + # for _, row in df.iterrows(): + # name = row['测量人员信息'] + # id_card = str(row.iloc[4]).strip() # 第5列是身份证号 + + # # 处理身份证号格式(如果是浮点数转为整数) + # if id_card.endswith('.0'): + # id_card = id_card[:-2] + + # global_variable.GLOBAL_NAME_TO_ID_MAP[name] = id_card + + # global_variable.GLOBAL_NAME_TO_ID_MAP = df + # logging.info(f"成功加载用户数据,共 {len(df)} 条记录,{len(global_variable.GLOBAL_NAME_TO_ID_MAP)} 个唯一姓名") + # return True + + # except Exception as e: + # logging.error(f"加载用户数据失败: {str(e)}") + # return False + + def _load_user_data(self): + """加载用户数据从Excel文件,只提取名字和身份证到字典""" + try: + # 默认路径:当前脚本的上一级目录下的"上传人员信息.xlsx" + current_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(current_dir) + excel_path = os.path.join(parent_dir, "上传人员信息.xlsx") + + if not os.path.exists(excel_path): + logging.error(f"Excel文件不存在: {excel_path}") + return False + + # 读取Excel文件 + df = pd.read_excel(excel_path, sheet_name='Sheet1') + + # 处理合并单元格 - 前向填充标段列 + # df['标段'] = df['标段'].fillna(method='ffill') + df['标段'] = df['标段'].ffill() + # 清理数据:去除空行和无效数据 + df = df.dropna(subset=['测量人员信息']) + df = df[df['测量人员信息'].str.strip() != ''] + + # 创建姓名到身份证的映射字典 + name_id_map = {} + + for _, row in df.iterrows(): + name = str(row['测量人员信息']).strip() + # 第5列是身份证号(索引为4) + id_card = str(row.iloc[4]).strip() if pd.notna(row.iloc[4]) else "" + + # 处理身份证号格式(如果是浮点数转为整数) + if id_card.endswith('.0'): + id_card = id_card[:-2] + + # 只将有效的姓名和身份证号添加到字典 + if name and id_card and len(id_card) >= 15: # 身份证号至少15位 + name_id_map[name] = id_card + else: + logging.warning(f"跳过无效数据: 姓名='{name}', 身份证='{id_card}'") + + # 将字典保存到全局变量 + global_variable.GLOBAL_NAME_TO_ID_MAP = name_id_map + + logging.info(f"成功加载用户数据,共 {len(df)} 条记录,{len(name_id_map)} 个有效姓名-身份证映射") + + # 打印前几个映射用于调试 + sample_names = list(name_id_map.keys())[:5] + for name in sample_names: + logging.debug(f"映射示例: {name} -> {name_id_map[name]}") + + return True + + except Exception as e: + logging.error(f"加载用户数据失败: {str(e)}") + return False + + def get_first_sjname_and_id(self, linecode: str, work_conditions: Dict) -> Optional[Dict]: + """ + 获取线路的第一个数据员姓名和身份证号 + + Args: + linecode: 线路编码 + + Returns: + 返回字典: {"name": 姓名, "id_card": 身份证号} + """ + if not work_conditions: + logging.error(f"无法获取线路 {linecode} 的工况信息") + return None + + # 直接取第一个测点的数据员 + first_point_info = next(iter(work_conditions.values())) + sjname = first_point_info.get('sjName') + + if not sjname: + logging.error("第一个测点没有数据员信息") + return None + + logging.info(f"使用第一个数据员: {sjname}") + + # 获取身份证号码 + id_card = global_variable.GLOBAL_NAME_TO_ID_MAP.get(sjname) + logging.info(f"id_card: {id_card}") + if not id_card: + logging.error(f"未找到数据员 {sjname} 对应的身份证号") + return None + + return {"name": sjname, "id_card": id_card} + + + # def set_all_points_and_fill_form(self, results_dir, name, user_id, condition_dict): + # """点击设置所有测点按钮并填写表单""" + # try: + # self.logger.info("开始设置所有测点并填写表单...") + + # # 点击"设置所有测点"按钮 + # set_all_points_btn = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + # ) + # set_all_points_btn.click() + # self.logger.info("已点击'设置所有测点'按钮") + + # # 等待表单加载 + # time.sleep(1) + + # # 填写司镜人员姓名 + # name_input = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/sname")) + # ) + # name_input.clear() + # name_input.send_keys(name) + # self.logger.info(f"已填写司镜人员姓名: {name}") + + # # 填写司镜人员身份证号 + # id_card_input = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/scard")) + # ) + # id_card_input.clear() + # id_card_input.send_keys(user_id) + # self.logger.info(f"已填写司镜人员身份证号: {user_id}") + + # # 选择测点状态 + # if not self._select_point_status("正常"): + # self.logger.error("选择测点状态失败") + # return False + + + # # 选择工况信息 + # if not self._select_work_condition(condition_dict): + # self.logger.error("选择工况信息失败") + # return False + + # # 等待操作完成 + # time.sleep(1) + + # # 检查操作结果 + # try: + # # 检查是否有成功提示或错误提示 + # # 这里可以根据实际应用的上传反馈机制进行调整 + # success_indicator = WebDriverWait(self.driver, 10).until( + # EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '成功') or contains(@text, '完成')]")) + # ) + # self.logger.info(f"操作完成: {success_indicator.text}") + # except TimeoutException: + # self.logger.warning("未检测到明确的操作成功提示,但操作已完成") + + # return True + + # except TimeoutException: + # self.logger.error("等待设置所有测点表单元素可点击超时") + # return False + # except Exception as e: + # self.logger.error(f"设置所有测点并填写表单时出错: {str(e)}") + # # 保存错误截图 + # error_screenshot_file = os.path.join( + # results_dir, + # f"form_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # ) + # self.driver.save_screenshot(error_screenshot_file) + # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + # return False + + def _select_point_status(self, status="正常"): + logging.info(f"开始选择测点状态: {status}") + """选择测点状态""" + try: + # 点击"选择测点状态"按钮 + status_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/all_point_pstate_sp")) + ) + status_button.click() + + # 选择指定的状态 + status_option = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, f"//android.widget.TextView[@resource-id='android:id/text1' and @text='{status}']")) + ) + status_option.click() + return True + except TimeoutException: + self.logger.error("等待测点状态选择超时") + return False + except Exception as e: + self.logger.error(f"选择测点状态时出错: {str(e)}") + return False + + # def _select_work_condition(self, condition_dict: Dict[str, List[str]]): + # logging.info(f"开始选择工况信息: {condition_dict}") + # """选择工况信息""" + # # 所有可能的工况选择按钮ID + # condition_button_ids = [ + # "com.bjjw.cjgc:id/all_point_workinfo_sp_qiaoliang", # 梁桥 + # "com.bjjw.cjgc:id/all_point_workinfo_sp_luji", # 路基 + # "com.bjjw.cjgc:id/all_point_workinfo_sp_handong", # 涵洞 + # "com.bjjw.cjgc:id/all_point_workinfo_sp_suidao" # 隧道 + # ] + # try: + # # 遍历所有可能的工况选择按钮 + # for button_id in condition_button_ids: + # try: + # # 点击"选择工况信息"按钮 + # condition_button = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, button_id)) + # ) + # condition_button.click() + # logging.info(f"成功点击工况选择按钮: {button_id}") + + # # 选择指定的工况选项 + # if self._select_condition_option(condition): + # logging.info(f"成功选择工况: {condition} 对于按钮: {button_id}") + # else: + # logging.warning(f"未能选择工况: {condition} 对于按钮: {button_id}") + # # 继续尝试其他按钮,不立即返回失败 + + # except TimeoutException: + # logging.debug(f"未找到或无法点击工况选择按钮: {button_id},继续尝试下一个") + # continue + # except Exception as e: + # logging.warning(f"点击按钮 {button_id} 时出错: {str(e)},继续尝试下一个") + # continue + + # return True + # except TimeoutException: + # self.logger.error("等待工况信息选择超时") + # return False + # except Exception as e: + # self.logger.error(f"选择工况信息时出错: {str(e)}") + # return False + + # def _select_condition_option(self, condition, max_scroll_attempts=5): + # """选择具体的工况选项""" + # try: + # # 滑动查找指定的工况选项 + # for attempt in range(max_scroll_attempts): + # try: + # # 尝试查找并点击指定的工况选项 + # condition_option = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.XPATH, + # f"//android.widget.TextView[@resource-id='android:id/text1' and @text='{condition}']")) + # ) + # condition_option.click() + # logging.info(f"成功选择工况选项: {condition}") + # return True + + # except TimeoutException: + # logging.debug(f"第 {attempt + 1} 次查找未找到工况选项: {condition},尝试滑动") + # # 执行滑动操作 + # self._scroll_to_find_condition() + + # logging.error(f"经过 {max_scroll_attempts} 次滑动仍未找到工况选项: {condition}") + # return False + + # except Exception as e: + # logging.error(f"选择工况选项时出错: {str(e)}") + # return False + + # def set_all_points_and_fill_form(self, results_dir, name, user_id, condition_dict): + # """点击设置所有测点按钮并填写表单""" + # try: + # self.logger.info("开始设置所有测点并填写表单...") + + # # 点击"设置所有测点"按钮 + # set_all_points_btn = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + # ) + # set_all_points_btn.click() + # self.logger.info("已点击'设置所有测点'按钮") + + # # 等待表单加载 + # time.sleep(1) + + # # 填写司镜人员姓名 + # name_input = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/sname")) + # ) + # name_input.clear() + # name_input.send_keys(name) + # self.logger.info(f"已填写司镜人员姓名: {name}") + + # # 填写司镜人员身份证号 + # id_card_input = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/scard")) + # ) + # id_card_input.clear() + # id_card_input.send_keys(user_id) + # self.logger.info(f"已填写司镜人员身份证号: {user_id}") + + # # 选择测点状态 + # if not self._select_point_status("正常"): + # self.logger.error("选择测点状态失败") + # return False + + # # 选择工况信息 - 传入condition_dict + # if not self._select_work_condition(condition_dict): + # self.logger.error("选择工况信息失败") + # return False + + # # 等待操作完成 + # time.sleep(1) + + # # 检查操作结果 + # try: + # # 检查是否有成功提示或错误提示 + # success_indicator = WebDriverWait(self.driver, 10).until( + # EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '成功') or contains(@text, '完成')]")) + # ) + # self.logger.info(f"操作完成: {success_indicator.text}") + # except TimeoutException: + # self.logger.warning("未检测到明确的操作成功提示,但操作已完成") + + # return True + # except TimeoutException: + # self.logger.error("等待设置所有测点表单元素可点击超时") + # return False + # except Exception as e: + # self.logger.error(f"设置所有测点并填写表单时出错: {str(e)}") + # return False + + def set_all_points_and_fill_form(self, results_dir, name, user_id, main_condition_dict, minor_conditions_list): + """点击设置所有测点按钮并填写表单""" + try: + self.logger.info("开始设置所有测点并填写表单...") + + # 点击"设置所有测点"按钮 + set_all_points_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + ) + set_all_points_btn.click() + self.logger.info("已点击'设置所有测点'按钮") + + # 等待表单加载 + time.sleep(1) + + # 填写司镜人员姓名 + name_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/sname")) + ) + name_input.clear() + name_input.send_keys(name) + self.logger.info(f"已填写司镜人员姓名: {name}") + + # 填写司镜人员身份证号 + id_card_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/scard")) + ) + id_card_input.clear() + id_card_input.send_keys(user_id) + self.logger.info(f"已填写司镜人员身份证号: {user_id}") + + # 选择测点状态 + if not self._select_point_status("正常"): + self.logger.error("选择测点状态失败") + return False + + # 选择工况信息 - 现在传入两个参数 + if not self._select_work_condition(main_condition_dict, minor_conditions_list): + self.logger.error("选择工况信息失败") + return False + + # 等待操作完成 + time.sleep(1) + + # 检查操作结果 + try: + success_indicator = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '成功') or contains(@text, '完成')]")) + ) + self.logger.info(f"操作完成: {success_indicator.text}") + except TimeoutException: + self.logger.warning("未检测到明确的操作成功提示,但操作已完成") + + return True + except TimeoutException: + self.logger.error("等待设置所有测点表单元素可点击超时") + return False + except Exception as e: + self.logger.error(f"设置所有测点并填写表单时出错: {str(e)}") + return False + + # def _select_work_condition(self, condition_dict: Dict[str, List[str]]): + # """根据工况字典选择工况信息 + + # Args: + # condition_dict: 工况字典,格式为 {work_type: [workinfoname1, workinfoname2, ...]} + # work_type: 工点类型编码(1-隧道,2-区间路基,3-桥, 4-涵洞) + # """ + # self.logger.info(f"开始选择工况信息: {condition_dict}") + + # # 工点类型编码与界面控件ID的映射 + # work_type_mapping = { + # "1": { # 隧道 + # "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_suidao", + # "name": "隧道" + # }, + # "2": { # 区间路基 + # "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_luji", + # "name": "路基" + # }, + # "3": { # 桥 + # "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_qiaoliang", + # "name": "梁桥" + # }, + # "4": { # 涵洞 + # "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_handong", + # "name": "涵洞" + # } + # } + + # try: + # success_count = 0 + # total_count = len(condition_dict) + + # # 遍历condition_dict中的每个工点类型 + # for work_type, workinfo_names in condition_dict.items(): + # # 确保work_type是字符串类型,与映射表匹配 + # work_type_str = str(work_type).strip() + # if work_type_str not in work_type_mapping: + # self.logger.warning(f"未知的工点类型编码: {work_type_str},跳过") + # continue + + # mapping = work_type_mapping[work_type_str] + # button_id = mapping["button_id"] + # work_type_name = mapping["name"] + + # # 获取该工点类型的第一个工况名称(主要工况) + # if workinfo_names: + # workinfo_name = workinfo_names[0] # 使用第一个工况名称 + # self.logger.info(f"为{work_type_name}({work_type_str})选择工况: {workinfo_name}") + + # try: + # # 点击工况选择按钮 + # condition_button = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, button_id)) + # ) + # condition_button.click() + # self.logger.info(f"成功点击{work_type_name}工况选择按钮") + + # # 选择具体的工况选项 + # if self._select_condition_option(workinfo_name): + # self.logger.info(f"成功为{work_type_name}选择工况: {workinfo_name}") + # success_count += 1 + # else: + # self.logger.warning(f"未能为{work_type_name}选择工况: {workinfo_name}") + + # except TimeoutException: + # self.logger.error(f"等待{work_type_name}工况选择按钮超时") + # except Exception as e: + # self.logger.error(f"点击{work_type_name}工况按钮时出错: {str(e)}") + + # # 如果有多个工况名称,处理次要工况 + # if len(workinfo_names) > 1: + # self.logger.info(f"{work_type_name}有{len(workinfo_names)-1}个次要工况需要处理: {workinfo_names[1:]}") + # self._handle_minor_work_conditions(work_type_str, workinfo_names[1:]) + # else: + # self.logger.warning(f"工点类型{work_type_name}({work_type_str})没有可用的工况名称") + + # self.logger.info(f"工况选择完成: 成功{success_count}/{total_count}个工点类型") + # return success_count > 0 # 只要有一个成功就返回True + + # except Exception as e: + # self.logger.error(f"选择工况信息时发生未知错误: {str(e)}") + # return False + + def _select_work_condition(self, main_condition_dict: Dict[str, List[str]], minor_conditions_list: List[Dict]): + """根据主要工况字典和次要工况列表选择工况信息""" + self.logger.info("开始选择工况信息") + self.logger.info(f"主要工况: {main_condition_dict}") + self.logger.info(f"次要工况数量: {len(minor_conditions_list)}") + + # 工点类型编码与界面控件ID的映射 + work_type_mapping = { + "1": { # 隧道 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_suidao", + "name": "隧道" + }, + "2": { # 区间路基 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_luji", + "name": "路基" + }, + "3": { # 桥 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_qiaoliang", + "name": "梁桥" + }, + "4": { # 涵洞 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_handong", + "name": "涵洞" + } + } + + try: + success_count = 0 + + # 第一步:为每个工点类型选择主要工况 + for work_type, workinfo_names in main_condition_dict.items(): + work_type_str = str(work_type).strip() + if work_type_str not in work_type_mapping: + self.logger.warning(f"未知的工点类型编码: {work_type_str},跳过") + continue + + mapping = work_type_mapping[work_type_str] + button_id = mapping["button_id"] + work_type_name = mapping["name"] + + if workinfo_names: + workinfo_name = workinfo_names[0] # 主要工况 + self.logger.info(f"为{work_type_name}({work_type_str})选择主要工况: {workinfo_name}") + + try: + # 点击工况选择按钮 + condition_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, button_id)) + ) + condition_button.click() + self.logger.info(f"成功点击{work_type_name}工况选择按钮") + + # 选择主要的工况选项 + if self._select_condition_option(workinfo_name): + self.logger.info(f"成功为{work_type_name}选择主要工况: {workinfo_name}") + success_count += 1 + else: + self.logger.warning(f"未能为{work_type_name}选择主要工况: {workinfo_name}") + + except TimeoutException: + self.logger.error(f"等待{work_type_name}工况选择按钮超时") + except Exception as e: + self.logger.error(f"点击{work_type_name}工况按钮时出错: {str(e)}") + + # 第二步:如果有次要工况,滑动页面处理 + if minor_conditions_list: + self.logger.info(f"开始处理 {len(minor_conditions_list)} 个次要工况") + minor_success_count = self._handle_minor_work_conditions(minor_conditions_list) + self.logger.info(f"次要工况处理完成: 成功 {minor_success_count}/{len(minor_conditions_list)} 个") + else: + self.logger.info("没有次要工况需要处理") + + self.logger.info(f"工况选择完成: 主要工况成功{success_count}/{len(main_condition_dict)}个") + return success_count > 0 # 只要有一个主要工况成功就返回True + + except Exception as e: + self.logger.error(f"选择工况信息时发生未知错误: {str(e)}") + return False + + # # 选择次要工况信息 + # def _select_minor_conditions_option(self, option_name: str, work_type_name: str) -> bool: + # """根据测点名字选择对应的下拉列表中的选项""" + # try: + # # 找到选项元素 + # option = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/point_pstate_sp")) + # ) + # option.click() + # # 选择次要的工况选项 + # if self._select_condition_option(option_name): + # self.logger.info(f"成功为{option_name}选择次要工况: {work_type_name}") + # success_count += 1 + # return True + # except TimeoutException: + # self.logger.error(f"未找到选项: {option_name}") + # return False + # except Exception as e: + # self.logger.error(f"选择选项时出错: {str(e)}") + # return False + + def _handle_minor_work_conditions(self, minor_conditions_list: List[Dict]) -> int: + """处理次要工况:滑动页面,找到对应测点并设置工况""" + success_count = 0 + processed_points = set() # 记录已处理的测点,避免重复处理 + logging.info(f"要处理的次要工况:{minor_conditions_list}") + + # 提取所有需要处理的测点ID + target_point_ids = {condition['point_id'] for condition in minor_conditions_list} + self.logger.info(f"目标测点ID: {list(target_point_ids)}") + + # 点击"关闭设置所有"按钮 + close_set_all_points_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + ) + close_set_all_points_btn.click() + self.logger.info("已点击'关闭设置所有测点'按钮") + + try: + max_scroll_attempts = 20 # 最大滑动次数防止无限循环 + scroll_attempt = 0 + previous_points = set() # 记录上一页的测点 + remaining_conditions = minor_conditions_list.copy() + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次滑动处理次要工况...") + + # 获取当前页面的测点 + current_points = set(self.collect_points_on_page()) + self.logger.info(f"当前页面共找到 {len(current_points)} 个测点: {list(current_points)}") + + # 检查是否有新测点 + if current_points == previous_points: + self.logger.info("当前页面没有新的测点,停止滑动") + break + + # 检查当前页面是否有目标测点 + current_target_points = current_points & target_point_ids + if not current_target_points: + self.logger.info(f"当前页面没有目标测点,继续滑动查找...") + # 滑动到下一页 + if not self.swipe_down(): + self.logger.warning("滑动失败,停止处理") + break + # 等待页面稳定 + time.sleep(1) + previous_points = current_points + continue + + self.logger.info(f"当前页面找到目标测点: {list(current_target_points)}") + + # 如果当前测点列表的测点名,在minor_conditions_list中,就点击选择测点对应工况 + + # 处理当前页面的次要工况 + page_success_count = self._process_minor_conditions_on_current_page( + current_points, remaining_conditions, processed_points + ) + success_count += page_success_count + + # 更新剩余待处理的工况列表 + remaining_conditions = [ + condition for condition in remaining_conditions + if condition['point_id'] not in processed_points + ] + + self.logger.info(f"剩余待处理工况: {len(remaining_conditions)} 个") + if remaining_conditions: + remaining_points = [cond['point_id'] for cond in remaining_conditions] + self.logger.info(f"剩余测点: {remaining_points}") + + # 更新已处理的测点记录 + previous_points = current_points + + # 如果所有次要工况都已处理完成,提前退出 + if len(processed_points) >= len(minor_conditions_list): + self.logger.info(f"所有 {len(minor_conditions_list)} 个次要工况已处理完成") + break + + # 滑动到下一页 + self.logger.info("滑动到下一页继续查找...") + if not self.swipe_down(): + self.logger.warning("滑动失败,停止处理") + break + + # 等待页面稳定 + time.sleep(1) + + self.logger.info(f"次要工况处理完成: 成功 {success_count}/{len(minor_conditions_list)} 个") + return success_count + + except Exception as e: + self.logger.error(f"处理次要工况时出错: {str(e)}") + return success_count + + def _process_minor_conditions_on_current_page(self, current_points: set, minor_conditions_list: List[Dict], processed_points: set) -> int: + """处理当前页面上的次要工况""" + page_success_count = 0 + + try: + self.logger.info("开始处理当前页面上的次要工况...") + + # 1. 先定位所有测点的根容器 + point_containers = self.driver.find_elements( + AppiumBy.ID, + "com.bjjw.cjgc:id/layout_popup_top" + ) + self.logger.info(f"当前页面找到 {len(point_containers)} 个测点容器") + + if not point_containers: + self.logger.warning("当前页面未找到任何测点容器") + return 0 + + # 2. 构建当前页面测点的映射关系:测点名称 -> 容器元素 + point_container_map = {} + for container in point_containers: + try: + name_element = container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/improve_point_name" + ) + point_name = name_element.text.strip() + if point_name and point_name in current_points: + point_container_map[point_name] = container + self.logger.debug(f"映射测点: {point_name}") + except NoSuchElementException: + continue + except Exception as e: + self.logger.debug(f"解析测点容器时出错: {str(e)}") + continue + + # 3. 遍历需要处理的次要工况列表 + for minor_condition in minor_conditions_list: + point_id = minor_condition['point_id'] + workinfoname = minor_condition['workinfoname'] + work_type = minor_condition['work_type'] + + # 检查是否已经处理过 + if point_id in processed_points: + self.logger.debug(f"测点 {point_id} 已处理过,跳过") + continue + + # 检查是否在当前页面 + if point_id not in current_points: + self.logger.debug(f"测点 {point_id} 不在当前页面") + continue + + # 检查是否有对应的容器 + if point_id not in point_container_map: + self.logger.warning(f"测点 {point_id} 在当前页面但未找到对应容器") + continue + + try: + self.logger.info(f"开始处理测点 {point_id} 的次要工况: {workinfoname}") + + # 4. 在当前测点的容器内查找工况选择按钮 + container = point_container_map[point_id] + workinfo_button = container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/point_workinfo_sp" + ) + + # 验证按钮是否可见和可用 + if workinfo_button.is_displayed() and workinfo_button.is_enabled(): + workinfo_button.click() + self.logger.info(f"已点击测点 {point_id} 的工况选择按钮") + + # 选择对应的工况选项 + if self._select_minor_conditions_option(workinfoname, work_type): + page_success_count += 1 + processed_points.add(point_id) + self.logger.info(f"成功为测点 {point_id} 设置次要工况: {workinfoname}") + else: + self.logger.warning(f"为测点 {point_id} 选择工况选项失败") + else: + self.logger.warning(f"测点 {point_id} 的工况选择按钮不可用") + + except NoSuchElementException: + self.logger.warning(f"未找到测点 {point_id} 的工况选择按钮") + except Exception as e: + self.logger.error(f"处理测点 {point_id} 时出错: {str(e)}") + + self.logger.info(f"当前页面成功处理 {page_success_count} 个测点的次要工况") + return page_success_count + + + except Exception as e: + self.logger.error(f"处理当前页面次要工况时发生异常: {str(e)}") + return page_success_count + + def _select_minor_conditions_option(self, option_name: str, work_type_name: str) -> bool: + """根据工况名称选择对应的下拉列表中的选项""" + try: + self.logger.info(f"开始选择次要工况选项: {option_name}") + + # 方法1: 通过文本精确匹配 + try: + option_xpath = f"//android.widget.TextView[@text='{option_name}']" + option_element = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, option_xpath)) + ) + option_element.click() + self.logger.info(f"通过文本匹配成功选择工况: {option_name}") + return True + except TimeoutException: + self.logger.debug(f"通过文本 '{option_name}' 未找到工况选项") + + # 方法2: 通过列表项ID查找 + try: + list_items = self.driver.find_elements(AppiumBy.ID, "android:id/text1") + for item in list_items: + if item.text == option_name: + item.click() + self.logger.info(f"通过列表项成功选择工况: {option_name}") + return True + except Exception as e: + self.logger.debug(f"通过列表项查找失败: {str(e)}") + + # 方法3: 滑动查找 + max_scroll_attempts = 3 + for attempt in range(max_scroll_attempts): + try: + option_element = self.driver.find_element( + AppiumBy.XPATH, + f"//android.widget.TextView[@text='{option_name}']" + ) + option_element.click() + self.logger.info(f"通过滑动后查找成功选择工况: {option_name}") + return True + except NoSuchElementException: + self.logger.debug(f"第 {attempt + 1} 次滑动查找未找到选项: {option_name}") + # 执行滑动 + self._scroll_condition_options() + except Exception as e: + self.logger.debug(f"滑动查找时出错: {str(e)}") + break + + self.logger.error(f"所有方法都无法找到工况选项: {option_name}") + return False + + except TimeoutException: + self.logger.error(f"等待工况选项可点击超时: {option_name}") + return False + except Exception as e: + self.logger.error(f"选择工况选项时出错: {str(e)}") + return False + + def _scroll_condition_options(self): + """滑动工况选项列表""" + try: + # 直接使用固定的坐标值 + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + start_x = 540 # 假设屏幕宽度为 1080,取中间值 + start_y = 1400 # 假设屏幕高度为 2000,取 70% 位置 + end_y = 600 # 假设屏幕高度为 2000,取 30% 位置 + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 500) + self.logger.debug("执行滑动操作查找更多工况选项") + time.sleep(1) # 等待滑动完成 + + except Exception as e: + self.logger.error(f"滑动工况选项列表时出错: {str(e)}") + + # def _handle_minor_work_conditions(self, minor_conditions_list: List[Dict]) -> int: + # """处理次要工况:滑动页面,找到对应测点并设置工况""" + # success_count = 0 + # processed_points = set() # 记录已处理的测点,避免重复处理 + # logging.info(f"要处理的次要工况:{minor_conditions_list}") + + # try: + + # while True: + # points_on_page_before = [] + # # 获取当前页面的测点 + # points_on_page_after = self.collect_points_on_page() + # self.logger.info(f"当前页面共找到 {len(points_on_page_after)} 个测点") + # if points_on_page_after == points_on_page_before: + # self.logger.info("当前页面没有新的测点,停止滑动") + # break + # else: + # points_on_page_before = points_on_page_after + # # 如果当前页面测点在次要工况字典中,则进行点击"com.bjjw.cjgc:id/point_workinfo_sp",选择对应的工况 + # _select_minor_conditions_option + + + + + # self.swipe_down() + + # # # 滑动收集页面中的所有测点 + # # all_points_on_page = self.collect_all_points_on_page() + # # self.logger.info(f"页面中共找到 {len(all_points_on_page)} 个测点") + + # # # 为每个次要工况找到对应的测点并设置工况 + # # for minor_condition in minor_conditions_list: + # # point_id = minor_condition['point_id'] + # # target_workinfoname = minor_condition['workinfoname'] + # # work_type = minor_condition['work_type'] + + # # if point_id in processed_points: + # # continue + + # # # 在页面中查找对应的测点 + # # target_point = None + # # for point_data in all_points_on_page: + # # if point_data.get('point_id') == point_id: + # # target_point = point_data + # # break + + # # if target_point: + # # # 找到测点,设置工况 + # # if self._set_single_point_work_condition(target_point, target_workinfoname, work_type): + # # success_count += 1 + # # processed_points.add(point_id) + # # self.logger.info(f"成功为测点 {point_id} 设置次要工况: {target_workinfoname}") + # # else: + # # self.logger.warning(f"为测点 {point_id} 设置次要工况失败") + # # else: + # # self.logger.warning(f"未在页面中找到测点 {point_id},可能不在当前视图内") + # # return success_count + + # except Exception as e: + # self.logger.error(f"处理次要工况时出错: {str(e)}") + # return success_count + + def collect_points_on_page(self) -> List[str]: + """ + 收集当前页面中存在的测点ID列表 + 要求:必须同时存在"com.bjjw.cjgc:id/improve_point_name"和"com.bjjw.cjgc:id/point_workinfo_sp"才返回 + """ + point_ids = [] + try: + self.logger.info("开始收集当前页面中的测点ID...") + + # 1. 先定位所有测点的根容器(RelativeLayout),确保在同一容器内查找子元素 + point_containers = self.driver.find_elements( + AppiumBy.ID, + "com.bjjw.cjgc:id/layout_popup_top" # 测点根容器的resource-id + ) + self.logger.info(f"找到 {len(point_containers)} 个测点根容器") + + for container in point_containers: + try: + # 2. 在当前根容器内查找测点名称元素 + name_element = container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/improve_point_name" + ) + point_name = name_element.text + if not point_name: + self.logger.debug("测点名称为空,跳过") + continue + + # 3. 在当前根容器内查找工况按钮(按实际层级定位) + # 层级:RelativeLayout -> LinearLayout -> LinearLayout -> Button + workinfo_button = container.find_element( + AppiumBy.XPATH, + ".//android.widget.LinearLayout/android.widget.LinearLayout/android.widget.Button[@resource-id='com.bjjw.cjgc:id/point_workinfo_sp']" + ) + + # 4. 验证按钮是否可见 + if workinfo_button.is_displayed(): + point_ids.append(point_name) + self.logger.debug(f"找到有效测点: {point_name}") + + except NoSuchElementException as e: + # 若名称或工况按钮不存在,跳过当前容器 + self.logger.debug(f"测点容器中缺少必要元素: {str(e)}") + continue + except Exception as e: + self.logger.debug(f"解析测点容器时出错: {str(e)}") + continue + + self.logger.info(f"当前页面共找到 {len(point_ids)} 个有效测点: {point_ids}") + return point_ids + + except Exception as e: + self.logger.error(f"收集页面测点ID时出错: {str(e)}") + return [] + + def collect_all_points_on_page(self) -> List[Dict]: + """滑动收集页面中所有测点的信息""" + all_points = [] + seen_point_names = set() + max_scroll_attempts = 50 + scroll_attempt = 0 + + self.logger.info("开始滑动收集页面中所有测点...") + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次滑动收集...") + + # 获取当前屏幕的测点 + current_points = self._get_current_screen_points_detail() + + if not current_points: + self.logger.info("当前屏幕没有测点数据") + break + + # 添加新发现的测点 + new_points_count = 0 + for point in current_points: + point_name = point.get('point_name') + if point_name and point_name not in seen_point_names: + all_points.append(point) + seen_point_names.add(point_name) + new_points_count += 1 + + self.logger.info(f"本次获取到 {len(current_points)} 个测点,其中 {new_points_count} 个是新测点") + + if new_points_count == 0: + self.logger.info("没有发现新测点,停止滑动") + break + + # 滑动到下一页 + if not self.swipe_up(): + self.logger.warning("滑动失败,停止收集") + break + + time.sleep(0.5) + + self.logger.info(f"共收集到 {len(all_points)} 个测点的详细信息") + return all_points + + def _get_current_screen_points_detail(self) -> List[Dict]: + """获取当前屏幕测点的详细信息""" + points = [] + + try: + # 查找所有测点容器 + point_containers = self.driver.find_elements( + AppiumBy.XPATH, + "//android.widget.LinearLayout[@resource-id='com.bjjw.cjgc:id/layout_popup_top']" + ) + + for container in point_containers: + try: + point_info = {} + + # 获取测点名称(作为point_id的替代) + name_element = container.find_element( + AppiumBy.ID, "com.bjjw.cjgc:id/improve_point_name" + ) + point_name = name_element.text + point_info['point_name'] = point_name + point_info['point_id'] = point_name # 使用名称作为ID,或者根据实际情况调整 + + # 获取测点元素引用,用于后续操作 + point_info['element'] = container + + # 获取当前工况信息 + try: + workinfo_element = container.find_element( + AppiumBy.ID, "com.bjjw.cjgc:id/point_workinfo_sp" + ) + point_info['current_workinfo'] = workinfo_element.text + point_info['workinfo_element'] = workinfo_element + except NoSuchElementException: + point_info['current_workinfo'] = "" + point_info['workinfo_element'] = None + + points.append(point_info) + + except Exception as e: + self.logger.debug(f"解析单个测点信息时出错: {str(e)}") + continue + + except Exception as e: + self.logger.error(f"获取当前屏幕测点详细信息时出错: {str(e)}") + + return points + + def _set_single_point_work_condition(self, point_data: Dict, workinfo_name: str, work_type: str) -> bool: + """为单个测点设置工况信息""" + try: + point_name = point_data.get('point_name') + self.logger.info(f"开始为测点 {point_name} 设置工况: {workinfo_name}") + + # 使用保存的元素引用点击工况选择按钮 + workinfo_element = point_data.get('workinfo_element') + if workinfo_element: + workinfo_element.click() + time.sleep(1) # 等待选项弹出 + + # 选择指定的工况 + if self._select_condition_option(workinfo_name): + self.logger.info(f"成功为测点 {point_name} 设置工况: {workinfo_name}") + return True + else: + self.logger.warning(f"为测点 {point_name} 选择工况选项失败") + return False + else: + self.logger.warning(f"未找到测点 {point_name} 的工况选择按钮") + return False + + except Exception as e: + self.logger.error(f"为测点 {point_name} 设置工况时出错: {str(e)}") + return False + + def _select_condition_option(self, condition_name: str) -> bool: + """选择具体的工况选项 + + Args: + condition_name: 工况名称 + + Returns: + bool: 是否选择成功 + """ + try: + self.logger.info(f"开始选择工况选项: {condition_name}") + + # 方法1: 通过文本查找并点击 + try: + option_xpath = f"//android.widget.TextView[@text='{condition_name}']" + option_element = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, option_xpath)) + ) + option_element.click() + self.logger.info(f"通过文本定位成功选择工况: {condition_name}") + return True + except TimeoutException: + self.logger.debug(f"通过文本'{condition_name}'未找到工况选项") + + # 方法2: 通过列表项查找 + try: + # 假设工况选项在列表中,点击第一个可用的选项 + list_item = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/text1")) + ) + list_item.click() + self.logger.info("通过列表项选择工况") + return True + except TimeoutException: + self.logger.debug("未找到列表项形式的工况选项") + + # 方法3: 尝试点击屏幕特定位置(备选方案) + try: + # 直接使用固定的屏幕中央坐标 + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + x = 540 # 假设屏幕宽度为 1080,取中间值 + y = 1000 # 假设屏幕高度为 2000,取中间值 + + # 点击屏幕中央(假设选项在中间) + self.driver.tap([(x, y)]) + self.logger.info("通过点击屏幕中央选择工况") + return True + except Exception as e: + self.logger.debug(f"点击屏幕中央失败: {str(e)}") + + self.logger.error(f"所有方法都无法选择工况选项: {condition_name}") + return False + + except Exception as e: + self.logger.error(f"选择工况选项时出错: {str(e)}") + return False + + def _scroll_to_find_condition(self): + """滑动查找工况选项的辅助方法""" + try: + # 直接使用固定的坐标值 + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + start_x = 540 # 假设屏幕宽度为 1080,取中间值 + start_y = 1400 # 假设屏幕高度为 2000,取 70% 位置 + end_y = 600 # 假设屏幕高度为 2000,取 30% 位置 + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 500) + logging.debug("执行滑动操作") + + except Exception as e: + logging.error(f"滑动操作时出错: {str(e)}") + + + def aging_down_data(self, breakpoint_name=None, retry_count=0): + """跳转上传配置页面,根据断点名称点击上传按钮,检查是否在上传配置页面 + ->变化量属性,执行下载操作""" + try: + self.logger.info("开始执行上传配置页面操作aging") + + # # 跳转到上传配置页面 + # if not self.go_upload_config_page(): + # self.logger.error("跳转上传配置页面失败") + # return False + # 跳转到上传配置页面 + if not go_main_click_tabber_button(self.driver, self.device_id, "com.bjjw.cjgc:id/img_2_layout"): + logging.error(f"设备 {self.device_id} 跳转到上传配置页面失败") + return False + + # 根据断点名称点击上传按钮 + if not self.click_upload_by_breakpoint_name(breakpoint_name): + self.logger.error("点击上传按钮失败") + return False + + if not self.handle_upload_dialog(): + self.logger.error("处理上传对话框失败") + return False + + self.logger.info("开始执行上传配置页面测试") + + # 检查是否在上传配置页面 + if not self.is_on_upload_config_page(): + self.logger.info("不在上传配置页面,尝试导航...") + return False + + # 检查是否有变化量属性 + if not self.check_change_amount_on_page(): + self.logger.info("页面中缺少变化量属性,执行下载操作") + + # 执行下载操作 + if not self.execute_download_operation(): + self.logger.error("下载操作执行失败") + return False + time.sleep(1) + self.logger.info("已返回上传配置页面") + + # 如果是第一次重试,再次调用aging_down_data + if retry_count < 1: + self.logger.info(f"第{retry_count+1}次重试,再次执行aging_down_data流程") + return self.aging_down_data(breakpoint_name, retry_count + 1) + else: + # 第二次仍然缺少变化量属性,返回错误 + self.logger.error("已执行两次下载操作,页面仍然缺少变化量属性") + return False + else: + self.logger.info("页面中包含变化量属性,继续执行后续操作") + return True + except Exception as e: + self.logger.error(f"设备执行上传重复下载数据操作失败:{e}") + return False + + def click_save_upload_and_handle_dialogs(self): + """点击保存上传并处理弹窗""" + try: + self.logger.info("开始点击保存上传并处理弹窗") + + # 点击保存上传按钮 + save_upload_btn = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/improve_save_btn")) + ) + save_upload_btn.click() + self.logger.info("已点击保存上传按钮") + + # 处理警告弹窗 + time.sleep(1) + if not self.handle_warning_dialog(): + self.logger.error("处理警告弹窗失败") + return False + + return True + + except TimeoutException: + self.logger.error("点击保存上传按钮超时") + return False + except Exception as e: + self.logger.error(f"点击保存上传并处理弹窗时出错: {str(e)}") + return False + + def handle_warning_dialog(self): + """处理警告弹窗""" + try: + self.logger.info("检查并处理警告弹窗") + + # 等待弹窗出现 + warning_dialog = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/parentPanel")) + ) + + # 获取弹窗文本确认是目标弹窗 + alert_title = warning_dialog.find_element(AppiumBy.ID, "android:id/alertTitle") + alert_message = warning_dialog.find_element(AppiumBy.ID, "android:id/message") + + self.logger.info(f"检测到弹窗 - 标题: {alert_title.text}, 消息: {alert_message.text}") + + # 根据业务逻辑选择"是"或"否" + # 这里选择"是"来上传本次数据 + yes_button = warning_dialog.find_element(AppiumBy.ID, "android:id/button1") + yes_button.click() + self.logger.info("已点击'是'按钮确认上传") + # no_button = warning_dialog.find_element(AppiumBy.ID, "android:id/button3") + # no_button.click() + # self.driver.back() + # self.logger.info("已点击'否'按钮取消上传和返回按钮") + + return True + + except TimeoutException: + self.logger.info("未检测到警告弹窗,继续流程") + return True # 没有弹窗也是正常情况 + except Exception as e: + self.logger.error(f"处理警告弹窗时出错: {str(e)}") + return False + + def wait_for_upload_completion(self): + """等待上传完成""" + try: + # time.sleep(2) + self.logger.info("开始等待上传完成") + + # 等待弹窗显示 + upload_list = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/customPanel")) + ) + #等待弹窗消失 + WebDriverWait(self.driver, 20).until( + EC.invisibility_of_element_located((AppiumBy.ID, "android:id/customPanel")) + ) + + self.logger.info("已返回到上传列表页面,上传已完成") + return True + + except TimeoutException: + self.logger.error("等待上传完成超时") + return False + except Exception as e: + self.logger.error(f"等待上传完成时出错: {str(e)}") + return False + + # def parse_work_conditions(self, work_conditions: Optional[Dict[str, Dict]]) -> List[Dict[str, str]]: + # """ + # 解析工况信息,获取所有work_type和对应的workinfoname + + # Args: + # work_conditions: 从get_work_conditions_by_linecode获取的工况数据 + + # Returns: + # 返回工况列表,格式为 [{"work_type": "1", "workinfoname": "墩台混凝土施工"}, ...] + # """ + # condition_list = [] + + # if not work_conditions: + # logging.warning("工况数据为空") + # return condition_list + + # try: + # # 使用集合来去重,避免重复的工况组合 + # unique_conditions = set() + + # for point_id, condition_data in work_conditions.items(): + # work_type = condition_data.get('work_type', '') + # workinfoname = condition_data.get('workinfoname', '') + + # # 过滤空值 + # if work_type and workinfoname: + # # 创建唯一标识符 + # condition_key = f"{work_type}_{workinfoname}" + + # if condition_key not in unique_conditions: + # unique_conditions.add(condition_key) + # condition_list.append({ + # "work_type": work_type, + # "workinfoname": workinfoname + # }) + + # logging.info(f"解析出 {len(condition_list)} 种不同的工况组合") + # return condition_list + + # except Exception as e: + # logging.error(f"解析工况信息时发生错误: {str(e)}") + # return [] + + def parse_work_conditions(self, work_conditions): + """ + 解析工况信息,区分主要工况和次要工况 + 返回: (主要工况字典, 次要工况列表) + """ + if not work_conditions: + logging.warning("工况数据为空") + return {}, [] + + try: + # 统计每个work_type下各个workinfoname的出现次数 + work_type_stats = {} + point_work_conditions = {} # 记录每个测点的工况信息 + + for point_id, condition_data in work_conditions.items(): + work_type = str(condition_data.get('work_type', '')) + workinfoname = condition_data.get('workinfoname', '') + sjname = condition_data.get('sjName', '') + + # 记录测点工况信息 + point_work_conditions[point_id] = { + 'work_type': work_type, + 'workinfoname': workinfoname, + 'sjname': sjname + } + + # 统计出现次数 + if work_type and workinfoname: + if work_type not in work_type_stats: + work_type_stats[work_type] = {} + + if workinfoname in work_type_stats[work_type]: + work_type_stats[work_type][workinfoname] += 1 + else: + work_type_stats[work_type][workinfoname] = 1 + + # 分离主要工况和次要工况 + main_condition_dict = {} # 主要工况字典 {work_type: [主要workinfoname]} + minor_conditions_list = [] # 次要工况列表 [{point_id, work_type, workinfoname}] + + # 定义阈值:出现次数少于这个值的认为是次要工况 + minor_threshold = 3 # 可以根据实际情况调整 + + for work_type, workinfoname_counts in work_type_stats.items(): + if workinfoname_counts: + # 按出现次数从多到少排序 + sorted_workinfonames = sorted(workinfoname_counts.items(), key=lambda x: x[1], reverse=True) + + # 主要工况:取出现次数最多的 + if sorted_workinfonames: + main_workinfoname = sorted_workinfonames[0][0] + main_condition_dict[work_type] = [main_workinfoname] + + logging.info(f"work_type {work_type} 的主要工况: '{main_workinfoname}' (出现{sorted_workinfonames[0][1]}次)") + + # 次要工况:收集所有出现次数较少的工况及其对应测点 + for workinfoname, count in sorted_workinfonames: + if count <= minor_threshold: + # 找到使用这个次要工况的所有测点 + for point_id, point_info in point_work_conditions.items(): + if (point_info['work_type'] == work_type and + point_info['workinfoname'] == workinfoname): + minor_conditions_list.append({ + 'point_id': point_id, + 'work_type': work_type, + 'workinfoname': workinfoname, + 'sjname': point_info['sjname'], + 'count': count + }) + + logging.info(f"解析结果: 主要工况 {len(main_condition_dict)} 种,次要工况 {len(minor_conditions_list)} 个测点") + + # 打印次要工况详情用于调试 + for minor_condition in minor_conditions_list: + logging.info(f"次要工况 - 测点 {minor_condition['point_id']}: work_type={minor_condition['work_type']}, workinfoname='{minor_condition['workinfoname']}'") + + return main_condition_dict, minor_conditions_list + + except Exception as e: + logging.error(f"解析工况信息时发生错误: {str(e)}") + return {}, [] + + def get_work_type_name(work_type: str) -> str: + """ + 根据工点类型编码获取类型名称 + + Args: + work_type: 工点类型编码(1-隧道,2-区间路基,3-桥, 4-涵洞) + + Returns: + 工点类型名称 + """ + work_type_mapping = { + "1": "隧道", + "2": "区间路基", + "3": "桥", + "4": "涵洞" + } + return work_type_mapping.get(work_type, f"未知类型({work_type})") + + def upload_config_page_manager(self, results_dir, breakpoint_name=None, line_num=None): + """执行上传配置页面管理操作""" + try: + # 保存参数为实例属性 + self.results_dir = results_dir + self.breakpoint_name = breakpoint_name + self.line_num = line_num + self.logger.info("开始执行上传配置页面操作manager") + + # 跳转到上传配置页面 + # if not self.go_upload_config_page(): + # self.logger.error("跳转上传配置页面失败") + # return False + # 跳转到上传配置页面 + if not go_main_click_tabber_button(self.driver, self.device_id, "com.bjjw.cjgc:id/img_2_layout"): + logging.error(f"设备 {self.device_id} 跳转到测量页面失败") + return False + + # 根据断点名称点击上传按钮 + if not self.click_upload_by_breakpoint_name(breakpoint_name): + self.logger.error("点击上传按钮失败") + return False + + if not self.handle_upload_dialog(): + self.logger.error("处理上传对话框失败") + return False + + self.logger.info("开始执行上传配置页面测试") + + # 检查是否在上传配置页面 + if not self.is_on_upload_config_page(): + self.logger.info("不在上传配置页面,尝试导航...") + return False + + + # 检查是否有变化量属性,有就执行下面代码,没有就执行重新下载函数 + # 直接检查页面中是否有"变化量"属性 + if not self.check_change_amount_on_page(): + self.logger.info("页面中缺少变化量属性,执行下载操作") + # 执行下载操作 + if not self.execute_download_operation(): + self.logger.error("下载操作执行失败") + return False + # 下载操作完成后,点击上传导航按钮跳转到上传页面执行更多下载操作并检查状态 + if not self.aging_down_data(breakpoint_name): + self.logger.error("三次下载都失败,属性不存在变量") + return False + else: + self.logger.info("页面中包含变化量属性,继续执行后续操作") + + user_id = global_variable.GLOBAL_USERNAME + if user_id is None: + self.logger.error("获取用户ID失败") + return False + + max_variation = apis.get_user_max_variation(user_id) + if max_variation is None: + self.logger.error("获取用户最大变化量失败") + return False + + # # 循环滑动收集所有测点数据 + # logging.info("准备循环滑动收集所有测点数据") + # all_point_data = self.collect_all_point_data(results_dir) + + # # 保存测试结果 + # result_file = os.path.join(results_dir, f"{self.line_num}_{datetime.now().strftime('%Y%m%d')}.txt") + # with open(result_file, 'w', encoding='utf-8') as f: + # f.write(f"测试时间: {datetime.now().strftime('%Y-%m-%d')}\n") + # f.write(f"获取到的测点数: {len(all_point_data)}\n\n") + # for i, point in enumerate(all_point_data, 1): + # f.write(f"测点 {i}: {point.get('point_name', '未知')}\n") + # parsed = point.get('parsed_data', {}) + # # if parsed: + # # f.write(f" 本期数值: {parsed.get('current_value', 'N/A')}\n") + # # f.write(f" 上期数值: {parsed.get('previous_value', 'N/A')}\n") + # # f.write(f" 变化量: {parsed.get('change_amount', 'N/A')}\n") + # # f.write(f" 测量时间: {parsed.get('measurement_time', 'N/A')}\n") + # f.write(f"完整数据:\n{point.get('point_value', 'N/A')}\n\n") + + # self.logger.info(f"测试结果已保存到: {result_file}") + + # 给ai接口发送文件,等待接口返回是否能上传数据 + # 不能就点击手机返回按钮,能就继续填写表单 + # if not self.check_ai_upload_permission(result_file): + # self.logger.info("AI接口返回不允许上传,点击返回按钮") + # self.driver.back() + # return True # 返回True继续下一个断点的上传配置。 + + if not self.collect_check_all_point_data(max_variation): + self.logger.error(f"断点 '{breakpoint_name}' 上传失败") + self.driver.back() + return False # 返回False继续下一个断点的上传配置。 + + + # 获取线路的所有工况信息 + work_conditions = apis.get_work_conditions_by_linecode(self.line_num) + # work_conditions = {'1962527': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}, + # '0299815Z2': {'sjName': '王顺', 'workinfoname': '冬休', 'work_type': 2}, + # '0299820H1': {'sjName': '王顺', 'workinfoname': '架桥机(运梁车) 首次通过后', 'work_type': 4}, + # '0431248D1': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 4}, + # '0431248D2': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 4}, + # '0299815Z1': {'sjName': '王顺', 'workinfoname': '架桥机(运梁车) 首次通过前', 'work_type': 2}, + # '0431289D2': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 4}, + # '0431330D1': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}, + # '0431330D2': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}, + # '0431370D1': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}} + self.logger.info(f"获取线路工况信息成功: {work_conditions}") + if not work_conditions: + self.logger.error("获取工况信息失败") + return False + + # 提取人员姓名和身份证 + if not self._load_user_data(): + self.logger.error("加载用户数据失败") + return False + + # 获取第一个数据员姓名和身份证号 + user_info = self.get_first_sjname_and_id(self.line_num, work_conditions) + self.logger.info(f"获取到的第一个数据员姓名和身份证号为:{user_info}") + + if not user_info: + self.logger.error(f"无法获取线路 '{self.line_num}' 的数据员信息") + return False + + # # 解析为需要的列表格式 + # condition_list = self.parse_work_conditions(work_conditions) + # self.logger.info(f"设备获取到的工作条件列表为:{condition_list}") + + # # 转换为condition_dict格式(如果需要) + # condition_dict = {} + # for condition in condition_list: + # work_type = condition['work_type'] + # workinfoname = condition['workinfoname'] + # if work_type not in condition_dict: + # condition_dict[work_type] = [] + # if workinfoname not in condition_dict[work_type]: + # condition_dict[work_type].append(workinfoname) + + # self.logger.info(f"设备获取到的工作条件字典为:{condition_dict}") + + # # 设置所有测点并填写表单 + # if not self.set_all_points_and_fill_form(results_dir, user_info.get("name"), user_info.get("id_card"), condition_dict): + # self.logger.error("设置所有测点并填写表单失败") + # return False + + # 解析工况信息,现在返回两个值:主要工况字典和次要工况列表 + main_condition_dict, minor_conditions_list = self.parse_work_conditions(work_conditions) + self.logger.info(f"主要工况: {main_condition_dict}") + self.logger.info(f"次要工况数量: {len(minor_conditions_list)}") + # 设置所有测点并填写表单 - 传入两个参数 + if not self.set_all_points_and_fill_form(results_dir, user_info.get("name"), user_info.get("id_card"), main_condition_dict, minor_conditions_list): + self.logger.error("设置所有测点并填写表单失败") + return False + + + + # # 表达填写完成,点击"保存上传"并处理弹窗 + # if not self.click_save_upload_and_handle_dialogs(): + # self.logger.error("点击保存上传并处理弹窗失败") + # return False + + # 等待上传,查看loading弹窗。没有就下一个 + if not self.wait_for_upload_completion(): + self.logger.error("等待上传完成失败") + return False + + self.logger.info("上传配置页面操作执行完成") + # 把上传成功的断点写入全局变量GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST + global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST.append(breakpoint_name) + + return True + + except Exception as e: + self.logger.error(f"执行上传配置页面操作时出错: {str(e)}") + # # 保存错误截图 + # error_screenshot_file = os.path.join( + # results_dir, + # f"upload_config_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # ) + # self.driver.save_screenshot(error_screenshot_file) + # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + return False \ No newline at end of file diff --git a/permissions.py b/permissions.py new file mode 100644 index 0000000..a225c3b --- /dev/null +++ b/permissions.py @@ -0,0 +1,279 @@ +# 权限处理 +import subprocess +import logging +import time +import os + +# 配置日志 +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s") + +def check_device_connection(device_id: str) -> bool: + """检查设备连接状态""" + try: + check_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.product.model"] + result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + logging.info(f"设备 {device_id} 连接正常,型号: {result.stdout.strip()}") + return True + else: + logging.error(f"设备 {device_id} 连接失败: {result.stderr.strip()}") + return False + except subprocess.TimeoutExpired: + logging.error(f"设备 {device_id} 连接超时") + return False + except Exception as e: + logging.error(f"检查设备 {device_id} 时发生错误: {str(e)}") + return False + +def is_package_installed(device_id: str, package_name: str) -> bool: + """检查包是否已安装""" + try: + check_cmd = ["adb", "-s", device_id, "shell", "pm", "list", "packages", package_name] + result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10) + return result.returncode == 0 and package_name in result.stdout + except Exception as e: + logging.error(f"检查包 {package_name} 时发生错误: {str(e)}") + return False + + +def grant_single_permission(device_id: str, package: str, permission: str) -> bool: + """ + 为单个包授予单个权限 + :return: 是否成功授予 + """ + try: + grant_cmd = [ + "adb", "-s", device_id, + "shell", "pm", "grant", package, + permission + ] + result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) + + if result.returncode == 0: + logging.info(f"设备 {device_id}:已成功授予 {package} 权限: {permission}") + return True + else: + error_msg = result.stderr.strip() + logging.warning(f"设备 {device_id}:授予 {package} 权限 {permission} 失败: {error_msg}") + + # 尝试使用root权限 + if "security" in error_msg.lower() or "permission" in error_msg.lower(): + logging.info(f"设备 {device_id}:尝试使用root权限授予 {package} 权限") + + # 重启adb为root模式 + root_cmd = ["adb", "-s", device_id, "root"] + subprocess.run(root_cmd, capture_output=True, text=True, timeout=10) + time.sleep(2) # 等待root权限生效 + + # 再次尝试授予权限 + result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) + if result.returncode == 0: + logging.info(f"设备 {device_id}:使用root权限成功授予 {package} 权限: {permission}") + return True + else: + logging.error(f"设备 {device_id}:即使使用root权限也无法授予 {package} 权限 {permission}: {result.stderr.strip()}") + return False + else: + return False + + except subprocess.CalledProcessError as e: + logging.error(f"设备 {device_id}:ADB 命令执行失败,返回码 {e.returncode}") + logging.error(f"标准输出:{e.stdout.strip()}") + logging.error(f"错误输出:{e.stderr.strip()}") + return False + except Exception as e: + logging.error(f"设备 {device_id}:处理 {package} 时发生未知错误:{str(e)}") + return False + + +# def grant_appium_permissions(device_id: str) -> bool: +# """ +# 为 Appium UiAutomator2 服务授予 WRITE_SECURE_SETTINGS 权限 +# :param device_id: 设备 ID(可通过 `adb devices` 查看) +# :return: 权限授予是否成功 +# """ +# # 首先检查设备连接 +# if not check_device_connection(device_id): +# return False + +# packages_to_grant = [ +# "io.appium.settings", +# "io.appium.uiautomator2.server", +# "io.appium.uiautomator2.server.test" +# ] +# # 添加其他可能需要的权限 +# permissions_to_grant = [ +# "android.permission.WRITE_SECURE_SETTINGS", +# "android.permission.CHANGE_CONFIGURATION", # 备选权限 +# "android.permission.DUMP", # 调试权限 +# ] + +# success_count = 0 +# total_attempted = 0 + +# # 检查并授予权限 +# for package in packages_to_grant: +# if not is_package_installed(device_id, package): +# logging.warning(f"设备 {device_id}:包 {package} 未安装,跳过权限授予") +# continue + +# for permission in permissions_to_grant: +# total_attempted += 1 +# result = grant_single_permission(device_id, package, permission) +# if result: +# success_count += 1 + +# try: +# grant_cmd = [ +# "adb", "-s", device_id, +# "shell", "pm", "grant", package, +# "android.permission.WRITE_SECURE_SETTINGS" +# ] +# result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) + +# if result.returncode == 0: +# logging.info(f"设备 {device_id}:已成功授予 {package} 权限") +# else: +# logging.warning(f"设备 {device_id}:授予 {package} 权限失败: {result.stderr.strip()}") +# # 尝试使用root权限 +# if "security" in result.stderr.lower() or "permission" in result.stderr.lower(): +# logging.info(f"设备 {device_id}:尝试使用root权限授予 {package} 权限") +# root_cmd = ["adb", "-s", device_id, "root"] +# subprocess.run(root_cmd, capture_output=True, text=True, timeout=10) +# time.sleep(2) # 等待root权限生效 + +# # 再次尝试授予权限 +# result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) +# if result.returncode == 0: +# logging.info(f"设备 {device_id}:使用root权限成功授予 {package} 权限") +# else: +# logging.error(f"设备 {device_id}:即使使用root权限也无法授予 {package} 权限: {result.stderr.strip()}") + +# except subprocess.CalledProcessError as e: +# logging.error(f"设备 {device_id}:ADB 命令执行失败,返回码 {e.returncode}") +# logging.error(f"标准输出:{e.stdout.strip()}") +# logging.error(f"错误输出:{e.stderr.strip()}") +# except Exception as e: +# logging.error(f"设备 {device_id}:处理 {package} 时发生未知错误:{str(e)}") + +# # 最终验证 +# logging.info(f"设备 {device_id}:权限授予过程完成,建议重启设备或Appium服务使更改生效") +# return True + +def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool: + """ + 修复版:为 Appium 授予权限(使用正确的方法) + """ + logging.info(f"设备 {device_id}:开始设置Appium权限") + + # 1. 使用系统设置命令(替代原来的pm grant尝试) + logging.info("使用系统设置命令...") + system_commands = [ + ["adb", "-s", device_id, "shell", "settings", "put", "global", "window_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "transition_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "animator_duration_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "system", "screen_off_timeout", "86400000"], + ] + + success_count = 0 + for cmd in system_commands: + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功: {' '.join(cmd[3:])}") + else: + logging.warning(f" 失败: {' '.join(cmd[3:])}") + except: + logging.warning(f" 异常: {' '.join(cmd[3:])}") + + # 2. 授予可自动授予的权限 + logging.info("授予基础权限...") + grantable = [ + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.ACCESS_WIFI_STATE", + ] + + for perm in grantable: + cmd = ["adb", "-s", device_id, "shell", "pm", "grant", "io.appium.settings", perm] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功授予: {perm.split('.')[-1]}") + else: + logging.debug(f" 跳过: {perm.split('.')[-1]}") + + # 3. 返回结果 + logging.info(f"设置完成,成功项数: {success_count}") + + if require_all: + return success_count == (len(system_commands) + len(grantable)) + else: + return success_count > 0 # 只要有成功项就返回True +def check_appium_compatibility(device_id: str) -> dict: + """ + 检查Appium兼容性 + :return: 兼容性报告字典 + """ + try: + # 获取Android版本 + version_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.build.version.release"] + result = subprocess.run(version_cmd, capture_output=True, text=True, timeout=10) + android_version = result.stdout.strip() if result.returncode == 0 else "未知" + + report = { + "device_id": device_id, + "android_version": android_version, + "compatibility": "unknown", + "notes": [], + "suggestions": [] + } + + try: + version_num = float(android_version.split('.')[0]) + + if version_num >= 11: + report["compatibility"] = "limited" + report["notes"].append("Android 11+ 对WRITE_SECURE_SETTINGS权限限制非常严格") + report["suggestions"].append("使用--no-reset参数启动Appium") + report["suggestions"].append("设置autoGrantPermissions=false") + + elif version_num >= 10: + report["compatibility"] = "moderate" + report["notes"].append("Android 10 限制了WRITE_SECURE_SETTINGS权限") + report["suggestions"].append("可尝试使用root权限的设备") + + elif version_num >= 9: + report["compatibility"] = "good" + report["notes"].append("Android 9 兼容性较好") + + else: + report["compatibility"] = "excellent" + report["notes"].append("Android 8或以下版本完全兼容") + + except (ValueError, IndexError): + report["notes"].append("无法解析Android版本") + + return report + + except Exception as e: + logging.error(f"检查兼容性时出错: {str(e)}") + return {"device_id": device_id, "error": str(e)} + + +# 使用示例 +if __name__ == "__main__": + # 获取设备ID(示例) + devices_cmd = ["adb", "devices"] + result = subprocess.run(devices_cmd, capture_output=True, text=True) + + if result.returncode == 0: + lines = result.stdout.strip().split('\n')[1:] # 跳过第一行标题 + for line in lines: + if line.strip() and "device" in line: + device_id = line.split('\t')[0] + logging.info(f"找到设备: {device_id}") + grant_appium_permissions(device_id) + else: + logging.error("无法获取设备列表,请确保ADB已正确安装且设备已连接") \ No newline at end of file diff --git a/test/__pycache__/protocol.cpython-312.pyc b/test/__pycache__/protocol.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be1869125b45e6b6ccc0145bfd224ffc70450bb0 GIT binary patch literal 1285 zcmah|&1(}u6rah?rb*LPgO;|IHVs7~rC0?cDy2{~9#n!-M2w*!&1~D0d@(yIq--Hz z4=E8E_0V|Bwe;ej;N43@4wj`id+99_PrcMPv)Nds;Ded>-pucP&71d4)A|w6;>UOC zPcDRh2*n|9SLn=vu!|_t5yd)QCe*b>VlLWG6&+IoOO-ch*+bo1YD}ShJ){Saq~s0x zEJBk!X}Ousi!7JIP7vTqcXh^P9NxP#(SBE^cmt#b6mmt=XJ-h7u%#HqYzQ+*8W{pL zaeDLd!}tr9U(-|Eh^N+8QV9_<&UJ&w3oOs`seI;Ep=7CGdFLMM10!K*_+cxfBj2S8 zuRf@)H+^FVWUK>+$Wl^SYI&)?o=)kBG`0E)$x6r+c|Tn=RVdjPWdS)zE@DVB(F%56#kn=R9=RyVMkx-4svB^!NlbEK zES0*QSdgPil~Kf|DsrM4n81U*;3m>M~1ab?~U?R`{xg z4Z{t`6Oxz^E#FcxouLVdW5aMFUe)*v8?=95u==bVIV9f7+a?+QS9r_z-CL82#~!VsR|m@TBH+uN*mkUr>*1CbY?5F(a^w>ZW)H{^^FVfN5VpTYruXIU!VP>m z*loI~MJ9|bbzMhPaas-SAW;VY5uHf?mp}@+EG3=eR{t`~XYEIa@e+~k@v@)rav{kK zJ<*#8HUS>TRIonPSZ(=c4# int: + return sum(data) & 0xFFFF + + +def build_frame(cmd: int, device_id: int, payload: bytes = b"") -> bytes: + frame = bytearray() + frame += b'\x23\xA9' # 帧头 + frame += b'\x00\x00' # 长度占位 + frame += device_id.to_bytes(4, 'little') + frame += bytes([cmd]) + frame += payload + + length = len(frame) + 2 # 加上校验和 + frame[2:4] = length.to_bytes(2, 'big') + + s = sum16(frame) + frame += s.to_bytes(2, 'big') + return bytes(frame) + + +def parse_device_id(frame: bytes) -> int: + return int.from_bytes(frame[4:8], 'little') diff --git a/test/server.py b/test/server.py new file mode 100644 index 0000000..9ac693e --- /dev/null +++ b/test/server.py @@ -0,0 +1,41 @@ +import socket +import threading + +HOST = "127.0.0.1" +PORT = 9000 + +devices = {} # device_id -> socket + +def handle_client(conn): + while True: + data = conn.recv(1024) + if not data: + break + + # 前 4 字节当 device_id(小端) + device_id = int.from_bytes(data[4:8], "little") + print("[SERVER] recv for device:", device_id, data.hex(" ")) + + if device_id in devices: + devices[device_id].send(data) + print("[SERVER] forwarded to device", device_id) + +def accept_loop(): + s = socket.socket() + s.bind((HOST, PORT)) + s.listen() + print("[SERVER] listening") + + while True: + conn, _ = s.accept() + + # 第一次 recv 认为是设备注册 + first = conn.recv(1024) + if first.startswith(b"DEVICE"): + device_id = int(first.split(b":")[1]) + devices[device_id] = conn + print(f"[SERVER] device {device_id} registered") + else: + threading.Thread(target=handle_client, args=(conn,), daemon=True).start() + +accept_loop() diff --git a/test/test_play_data.py b/test/test_play_data.py new file mode 100644 index 0000000..7c60010 --- /dev/null +++ b/test/test_play_data.py @@ -0,0 +1,149 @@ +import time +import requests +import pandas as pd +from io import BytesIO + + +class CheckStation: + def __init__(self): + self.station_num = 0 + self.last_data = None + + def get_measure_data(self): + # 模拟获取测量数据 + pass + + def add_transition_point(self): + # 添加转点逻辑 + print("添加转点") + return True + + def get_excel_from_url(self, url): + """ + 从URL获取Excel文件并解析为字典 + Excel只有一列数据(A列),每行是站点值 + + Args: + url: Excel文件的URL地址 + + Returns: + dict: 解析后的站点数据字典 {行号: 值},失败返回None + """ + try: + print(f"正在从URL获取数据: {url}") + response = requests.get(url, timeout=30) + response.raise_for_status() # 检查请求是否成功 + + # 使用pandas读取Excel数据,指定没有表头,只读第一个sheet + excel_data = pd.read_excel( + BytesIO(response.content), + header=None, # 没有表头 + sheet_name=0, # 只读取第一个sheet + dtype=str # 全部作为字符串读取 + ) + + station_dict = {} + + # 解析Excel数据:使用行号+1作为站点编号,A列的值作为站点值 + print("解析Excel数据(使用行号作为站点编号)...") + for index, row in excel_data.iterrows(): + station_num = index + 1 # 行号从1开始作为站点编号 + station_value = str(row[0]).strip() if pd.notna(row[0]) else "" + + if station_value: # 只保存非空值 + station_dict[station_num] = station_value + + print(f"成功解析Excel,共{len(station_dict)}条数据") + return station_dict + + except requests.exceptions.RequestException as e: + print(f"请求URL失败: {e}") + return None + except Exception as e: + print(f"解析Excel失败: {e}") + return None + + def check_station_exists(self, station_data: dict, station_num: int) -> str: + """ + 根据站点编号检查该站点的值是否以Z开头 + + Args: + station_data: 站点数据字典 {编号: 值} + station_num: 要检查的站点编号 + + Returns: + str: 如果站点存在且以Z开头返回"add",否则返回"pass" + """ + if station_num not in station_data: + print(f"站点{station_num}不存在") + return "error" + + value = station_data[station_num] + str_value = str(value).strip() + is_z = str_value.upper().startswith('Z') + + result = "add" if is_z else "pass" + print(f"站点{station_num}: {value} -> {result}") + return result + + + def run(self): + last_station_num = 0 + + url = f"https://database.yuxindazhineng.com/team-bucket/69378c5b4f42d83d9504560d/前测点表/20260309/CDWZQ-2标-龙家沟左线大桥-0-11号墩-平原.xlsx" + station_data = self.get_excel_from_url(url) + print(station_data) + station_quantity = len(station_data) + over_station_num = 0 + over_station_list = [] + while over_station_num < station_quantity: + try: + # 键盘输出线路编号 + station_num_input = input("请输入线路编号:") + if not station_num_input.isdigit(): # 检查输入是否为数字 + print("输入错误:请输入一个整数") + continue + station_num = int(station_num_input) # 转为整数 + + if station_num in over_station_list: + print("已处理该站点,跳过") + continue + + if last_station_num == station_num: + print("输入与上次相同,跳过处理") + continue + last_station_num = station_num + + result = self.check_station_exists(station_data, station_num) + if result == "error": + print("处理错误:站点不存在") + # 错误处理逻辑,比如记录日志、发送警报等 + elif result == "add": + print("执行添加操作") + # 添加转点 + if not self.add_transition_point(): + print("添加转点失败") + # 可以决定是否继续循环 + continue + over_station_num += 1 + else: # result == "pass" + print("跳过处理") + over_station_num += 1 + + over_station_list.append(station_num) + + # 可以添加适当的延时,避免CPU占用过高 + # time.sleep(1) + + except KeyboardInterrupt: + print("程序被用户中断") + break + except Exception as e: + print(f"发生错误: {e}") + time.sleep(20) + # 错误处理,可以继续循环或退出 + print(f"已处理{over_station_num}个站点") + +if __name__ == "__main__": + monitor = StationMonitor() + monitor.run() \ No newline at end of file