From e17bed9a6c7c4c3cb9c7e8d29442cfd44ca04c61 Mon Sep 17 00:00:00 2001 From: chuyiwen Date: Sat, 26 Jul 2025 10:05:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=89=80=E6=9C=89=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=BF=9E=E6=8E=A5=E7=8A=B6?= =?UTF-8?q?=E6=80=81=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=8E=A5=E5=8F=A3=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/api_server.cpython-313.pyc | Bin 28501 -> 59312 bytes .../__pycache__/cache_manager.cpython-313.pyc | Bin 15008 -> 18779 bytes .../__pycache__/snap7_client.cpython-313.pyc | Bin 6613 -> 6582 bytes gateway/api_server.py | 749 +++++++++++++++++- gateway/cache_manager.py | 356 ++++++--- gateway/data_parser.py | 1 + gateway/snap7_client.py | 8 +- 7 files changed, 954 insertions(+), 160 deletions(-) diff --git a/gateway/__pycache__/api_server.cpython-313.pyc b/gateway/__pycache__/api_server.cpython-313.pyc index ca99335cdd00a270f493c410fa62ddefb9910987..260078de010e4939e7624dcab1220eb2b183c0b7 100644 GIT binary patch literal 59312 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 diff --git a/gateway/__pycache__/cache_manager.cpython-313.pyc b/gateway/__pycache__/cache_manager.cpython-313.pyc index f4dff97eaf9fe2bcfce158fcfd9ad4dbbf85c530..0539d0b316b381a6fa3767ab548e88af21ce621b 100644 GIT binary patch literal 18779 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 BtVI9- literal 15008 zcmd5j3s76vl~2;sUw{CC#9spOkuewy*cg9e#~1@PIObUigd|!S2~2{8^8`tZleC#_ zQ{31Anz)7}jYZSOWRu2pc6LjgbmOE>N}TRguX2j2csib0V<$VaQ~cS|ab|b+ocr{i zge>f&)7j~@Iq!bY{hi-E_v(5=f`)>yj{C=b- |A7f1(Ba7S3Wyw|7>beXpjiGZ zVI?FjWu+!6pJJq$6eFvV@-0|dkHkiQiHX)OK8uIpa_YJZ(#NO=D8#}_83`+6q%6%O zGjt~Pu!NN}@=S_VFbaT5MhQ^Gr~s-NH9!rc0jOoP0CkKGU;>i>Fp)_Fn8YLj)H8ZE zxkqM83F)>vyY{(ToE~S7oBdJ_9S@oS+_Q7*%>`-(3UC0B45}tx+Xk+z$o@_r`qx& zwS<^35;Fx$Djz%s`I`CRAI+b9ZvMy*V-`@O*~0dC>ojHvUHzW!-kv)1m)|{l<(F^I zpL*wupHGs41ASeN{rr~HnPDr$<)451)l2{O9I4|xaG=h7Y3AMe(=T3maq7y{bMvQ1 z=HEL8@jtyc4DizF=Xtzee(&fPzx?H&-aD-M5@*!V%Ho3C^dW6jHz5T-n2-$chU8sH z6d^t9?q=QIeU4twfSY~H*%wl>gC0+>rzfNy*vGn^E{xJU`@0?q(R~ONe0E6N=kyLZ z1`oKL1MW}?_CqqD50ATR!0iet`}%u&2%;YMz)py}*^mPB;H&BN^bR=45Nvda_PYDJ zS=>_=x0%KL^x_O~1w$+A>mIm=?SB}sdLQV4-X3=z6u$(8u#n{k4u(_?m6!4+2D5UojwzTAx#v}R7t(X)(u;qWUObf;NMB28sB|Mqhm$^2Wn72u zs4kILAb~WB9CH03$UO%9(gumxz(|l$QmMw5D9R(TOBgApJyN9dHj&#Rt?bfbNRU#I z-(pLVd)gF{Tp`A{g+AzTpASG!GCK$*(kI<1avbEzF;C=CrjW>)vIZEG9P&q|D*VLNHzCd&yYgz1sytCK$p>@z{mRCPdKaXo{|D7=Xc_yq0t7DJSt`z3(e2tr9T*nrcIGjtO=mM(kv4jG8dB2JqggrWR{<8U$o*Vob=P z!15AQGvG^)uf8=(fj?iNx^hc9}VPV@FWi#tuUENjLXDsh`MM59q0(jas^&h~=U9HZ<}(uCBaO~4{Wwz5S4 zhiEYL%|i+^>TmNg#oT20{sFq(tUk zh856IVacjU++MUJiNPWoEun-ZvkGM{F<+LHRYPO%QP759!6c@P^o{fl_bo_E5>^MZ z@=mOuSnqr6v#j#bt--u}--^L)MW52Vp_#7bN;b^w;Y#j4vxX~a4(_`DmVBtO^TLi@zg>53-KXoP=(!TesGTuk zlWMM{@zTzFe%o}e>C+upbQf20*BKR8(zK|hR#d%_v5-iuD92udyB>^-cu@xRE?P@Y z?wH&$XI;x#*UngHo|t)fRxvv;+wI@;u)p?^Icwil3uP%=ET^(<6B}U-PjtN85iBX4 z+WwZ#M+b{bry3@ogvfTgk3OlH)J)ZK=IZG^oVjkMHejy5DyM8U&{1yjOC4A5qOw=r zXp~UNX~#7`(2Q5lWmRxl73cMp3oQ~VDHXTyNZ+CvMs+IV<{}Kg>Sq~Wff;EnyH3e) z>!ulqH9|KOU)}qAx5>0cia(1<@IYT+SDZMF2{fR*h@lSFz zO{EE+6qiE$T#6Ffp39I!&2zbk=bS~;T&X@+s>XP=W~WkiZqufnQrT~$a)>XnrNM&c zkE#C&kvrMaP6_B*IHZCz1#0u56L^&m@$6+H$FYx?z`(JSq1U0KN3^m*+o@uzK{B2V zEZBo)v>;Gu#e_Dnq+E7S(8RFd#=CH5F4Cg-?TOCXj6m8#cI4r zrh7fz{clK!#Pz}&Q{l7THmt}GB<$NOC#{^2@9h8pLQ3MVM zMZZfg3Z@$8QVY4%!eDYC#IGu8qXwN83mQtDFrq!Iol|9Ss*LlhtRT2CNZQ1yO#Y0D z&s3GS9=dRw@Q88!ZW!R52n7wWSVDoV5=jN{go9|z-A~NbI5pbv+!PaleCTx&vim!rvqEUW%l$Bn88rl-|GSQnYo8`cDcI;`}z*vN}T-% z4ghf~dEz9bNI%@)>j|lPy$-NY9&;0#WY z-hqM&syzh_A2_VXq0^`qKMc3n2K~hmh~Yyl@y&S@@u`Zhg9m&@LXPe*M@H&V?Xq^_ zj7ew{V?xf71GKUX${AVRi*WGA)-<$6R|N6r!9ho5R{(A-1;Z~2YaAg9-gQM(jW`D+ z5dI>~3&-hQ5=JSwAKI>KXLJpYUNk+28<^4RznuRn9?{mIMEoZ{VqNgg+le3U_p;~=&?G3#$87j~2t z?k_8JnF|~13R~(5nUK5{n?(G6Z_uPFZ)jVvxfMVwC;(!Q6H{Cc$B6yjC|i6DR0*lT z%h-*~9a4km-PwoHu;m?6dI$IKcd`eG^-azvAvK;Vh=Y(NW_QH0N3j;q@YdpCA&e+w zTEenXB~M}N{{aA;n_?JN1{0D;I-ctQ6ZvG_WZhgr6<1L8`+}OWq)}DSS~RKz(pL&GbYZ~>^BsFQEpl> zwL4%e4_a1FC@y8>oXB`7Sv!)r8`rrO>x;GIv}74GL_*hX?&n z91J{s@FxQ&pZf7r{+h<$Sbtl7uH4_@3N*XV7k7hqZ?kl}G-$T^?w!h#g= zoT1Ry8!%K{&!S3dz-qOw0dq3ZxUiDa8;27Y%OHNFBb>VFMIs;BnzFT2_N!9O);eVu z$`B!lhSDoUru;4BiCt15ddTBRkEM=9L36o)(-<(q7+Ja03q}Cnp$QUh=3A%DR-t3m=`yM*X^$CN>7d+R`A2~gL;>4Bj|Lf&%KNC`el5}+U4|-ff zX?RSB12;LQqwXZxHYs7`kaEA<>xHY-Rw5$UdjK7~R4hc8pJkvFeElM;vn#NADgxwj z!tzj#?na)K!Rboz{7Whk*2Nt&lR&)up(Nx^!tMNRLMZzd-i;~+c}aOs0|5RFvo3i5 z>&EJiZyMX=E19}Gkh*FvwTervnr;fDZW^xtW2!NjU*@md%^BMkmB0muI~H^laIvM9 zv((O6HgJ{=Gwo*{4Op7|y4+xr#kV_&U67Q zc!PJs8mJ>JU>A|CV?rAoEn)+svrE8tpFsThiFWl;|169usZ;d(+ttF3LoF$e8ho7+ zk9vzb?)XW`*YW1|NWubAPXy%r(PNip{)rcmUz~auOr%zuwDo4<7W7<-B9RC>%s1>J z*P~`QhM4=i&3jRddZQepxXQ#O$=gCjkc4JKX-!dnPthAXq;CV~s=3?Q+Xt@EZV<#F z$*P-DaD*mA6@d3MDORqcN@A-Jpr?>U13M&VogR2{AWBod!lqAyt?*4` zvayUvKA5l~AIm(lh7M3c0kxxwJAat!!#vAZ_h% z1Gq60)&z|?$Dba18t!M;b46>xQMvD(+}R>--97%c_JFZtl)jv5d}XVTez|EXJ&?WX zQVw`gUwv>~60BPL^R?qNif1sQ?hlwMu7Y>3^wNgS?>C)pdUwZY{hYzd8LYl`&QLye zkTa~iP`lxM^=b9HS}ZE%45d?zoS`OISTR~ZZW?PvSF2&XZn2oxEi0&^YN4Q(m|epi zf4NakoN_W4wq?~QJ=#8&D4zRx?_BkC`*ib+^Nf1t&}{zf?pg5E-E&^Q8!w}udW)G( ze)Ha`Mt@F?-&k{AU$a<4`ub|295CKoTm`A4pjR4UAC6Wx=wu&gbO`gZ5U#&Fuf9rp z)+$F>rD;&h&u*-3pyeOcNFe^v8X78nOv@quv08?4ou(mk8Ev%p?^GLsTqAmjq1OW$ zI^%01Fw;7futX$8& zsf^+OQER2$qP6CqotXdbPeE%nuufPf;rdR_-`Q0{78DI76NT361?A&(mGyi24u*AF z1G^Vnk2 zVJI=WG@e>RJ;sWR1bJ7Dm>_)XPYd6arHj5Zv8(qB*)NU94ODrsiYGJi3 zq4wrt6{JwKMa^xQ+*EX0y(Mcfw zv4)09ADiSD&yiu=tZ67$c7^=}fEX?P@ImYx5Wq2N6}(uqOF+zqz&yqmg~W^pkAf`x zmd82z@*6 z(6?O^)Au{K9jn%^uq&h9bc%-xMprI+Wnl&v_=B5WYNg;ECAz>aAAaY{?~U+Bf+&v# z5s^p*Y!84|VpS8lYonuT;cVCk5m*p-0D$vjCC1wkkaB_wu@C_TPas!docKu<@N(Jh zak0IS1J99ry#qEaG1@~a*8S+9+dJUJHxJ&yK8VO5D++f6EwSWLcal+h5p%?X51a6s zz0QHIeGb$LF;Xs4@L$l-O9A&CtXZzJnUuhl8t2% zpxq$oepq>m9Yg?Qi}p1V3}gh@1J4q;eG!)654BpJ@QIR-xh#k(F~aV7e+13%%oA`D zd~mqoLaJ#z6^@o!fmC>Vd=Va-I3^rZ?PrPtIlB(G4sVZ7u|SenwF;y~1rV)14$)r7 zwfL+r_eN^Ln--wh@+g9hCMA(zp9eCAU;2OxeFxPn@yN{RG7k%7n3fpAo$wVDG1eIg#>&%a19r6$eStR%EwX1g!sKjJdxxzXXJuV;ZcAq z9|b!J@!m^Bx$@<=xbhX?%BRDwe1)Bk*E^$$JoU%xNnv)*G+;CtTm#a)Qy;vIVG$B< zwvp4EP+t%WpvtIt{>11Dgz&W`xNoB7icaO20N0kfs2YZtVn~uVD{;J+(Y*;H6XpZg zns6p7m&KdcZLTcjVUJu=)J5eNCtth#>L|Qjyz18j`2qU0 zi1uxbdp%Kz#f0T|W2_ATyk1eU&d2%YL=EHW*t8)nB6JYkEc-N8U-mA;i>26%wIjF} z0Niqd;Sw!w2nwQs#rqBN%LuOu>IpI@>Y9V}>>}febIlP|T7{CwM6~g|YQ@*T=`iK_ zEtP)b>c9C-$3;{23Dtzkmo>FFV5%H$`eTym4@MA5zQ-n4POAgPxtb4`=W9A2`U_hXM_U&KEoxY~BrO-ITIe2%YekD4+_d7uM9Tk$sT5r3^m5 z(rn3x&)GGaZL|^$DU>F}&f?#blAR=*osI=$yKs}pL(s?r5u2nHeq#tfyTYHQz|XEU z4#)m}*I*yUbq>d)gU-HiNs`0S-OG9h`g%QXPd^kZ9S&E2m&3tkB3k_I3jYLZ8^&4? zbRc*L0h)ZgYJ*D%iUre}VxIvJN^>~y7Oc0+;T#xXd-o244sT!{`#F`kNH@tPDc2~3r0xF!SMR4{ diff --git a/gateway/__pycache__/snap7_client.cpython-313.pyc b/gateway/__pycache__/snap7_client.cpython-313.pyc index 78258b424e89a0fc274ace735fe777e81908d1e0..fb99def12206bd2eabd10d887343261f6111cc93 100644 GIT binary patch delta 470 zcmca=yv>;RGcPX}0}#ZTw`45a$ScLi7&rN*@Yl(z{K~8z3=F}{lYRNkd09dk`51y( zLz#gj+vJJ-s`~6e7KbsrDI-WT1A_vC978Y@2%0j%MADf;S&G48!Az6i@~cZT*)q#B zq%kToM2kSwFfgz)BpNX=xu!(N(3ZALix&at@0yg4m**AhtZ0 zvo8B#O=b{V7fc#(x#%z-w$gA>XFj6N>Y}sxpfEc#qt)b-VpfVaKog6A0m4vZ3?wvJ vizGm7TM%IfA{;iWh~Hvla|E&7CbLKeF-lAhmo#JapWG)Y!5BDsxuhNdPOoeb delta 512 zcmdmHeASruGcPX}0}yClY|d!i$ScLiSTT8*unMEoG-fwt z1R2l3puhk#qL?X|83aw4U@|HUp)4@b$#?nHrI~G+<<~&wMRuw3n#+b$cu|bo&bjIXffmxFs z1QppngPl4#QSb$$!elQY3tp)iArtK`@F;#@;o(%8+#%G&t+XIyrrmd51|jK*lLdt( z8I>k$3#-N55C*Hf%A@%E13NRP(ywAqpeqiF@;F;C98`2PbXI3REW`=q9F}1Ov1P$* zbuMR9_QQtEAaPSLX~E@U$$W%K$Hj#Ch#n(|ZNlnexp|Q=J2RU#&_qq1$r}V!CSMj4 zRI~+hi;O^oF-U^7NCL#R0}=Kh!f`XJ_$@{@ClK3x@>7W*pi?a*%@_kF=Sxa322Jje G)B^x-H)zNJ diff --git a/gateway/api_server.py b/gateway/api_server.py index 1ffd84e..cf1d149 100644 --- a/gateway/api_server.py +++ b/gateway/api_server.py @@ -61,18 +61,11 @@ class APIServer: summary[plc_name] = {} for area_name, area in areas.items(): last_update = self.cache_manager.last_update[plc_name][area_name] - plc_last_connected = self.cache_manager.plc_last_connected.get(plc_name, 0) - - # 确定状态 - if plc_last_connected == 0: - status = "never_connected" - elif time.time() - plc_last_connected > 5: - status = "disconnected" - else: - status = area["status"] + plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown") summary[plc_name][area_name] = { - "status": status, + "status": area["status"], + "plc_connection_status": plc_status, "last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never", "size": area["size"], "type": area["type"] @@ -101,10 +94,14 @@ class APIServer: .status-connected { color: green; font-weight: bold; } .status-disconnected { color: red; } .status-never-connected { color: orange; } + .plc-connected { background-color: #d4edda; } + .plc-disconnected { background-color: #f8d7da; } + .plc-never-connected { background-color: #fff3cd; } .api-section { margin-top: 30px; } .api-endpoint { background-color: #f9f9f9; padding: 10px; margin: 5px 0; border-radius: 4px; } .config-link { margin-top: 20px; padding: 10px; background-color: #e9f7fe; border-radius: 4px; } .footer { margin-top: 40px; padding-top: 10px; border-top: 1px solid #ddd; color: #777; } + .doc-link { margin-top: 10px; padding: 10px; background-color: #e9f7fe; border-radius: 4px; } @@ -113,7 +110,16 @@ class APIServer: """ for plc_name, areas in summary.items(): - html += f"

PLC: {plc_name}

" + plc_status = self.cache_manager.plc_connection_status.get(plc_name, "unknown") + plc_class = "" + if plc_status == "connected": + plc_class = "plc-connected" + elif plc_status == "disconnected": + plc_class = "plc-disconnected" + else: + plc_class = "plc-never-connected" + + html += f'

PLC: {plc_name} (Status: {plc_status})

' html += """ @@ -121,6 +127,7 @@ class APIServer: + """ @@ -146,6 +153,7 @@ class APIServer: + """ @@ -186,6 +194,11 @@ class APIServer: + +
Type Size (bytes) StatusPLC Connection Last Update
{area['type']} {area['size']} {status_text}{area['plc_connection_status']} {area['last_update']}
+ + + + + + + + + + + + +
参数描述
plc_namePLC名称(如PLC1)
area_name区域名称(如DB100_Read)
+ +

响应示例

+
+ { + "status": "connected", + "plc_connection_status": "connected", + "last_update": 1698754321.456, + "last_update_formatted": "2023-10-30 14:12:01", + "size": 4000, + "type": "read" + } +
+ + +

Data API

+ +
+

Single Read

+
+ GET + /api/read//// +
+

从指定区域读取数据。

+ +

路径参数

+ + + + + + + + + + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如PLC1)
area_name区域名称(如DB100_Read)
offset起始偏移量(字节)
length读取长度(字节)
+ +

响应示例

+
+ { + "status": "success", + "plc_name": "PLC1", + "area_name": "DB100_Read", + "offset": 0, + "length": 4, + "data": [0, 0, 123, 45], + "plc_connection_status": "connected", + "last_update": 1698754321.456, + "last_update_formatted": "2023-10-30 14:12:01" + } +
+
+ +
+

Single Write

+
+ POST + /api/write/// +
+

向指定区域写入数据。

+ +

路径参数

+ + + + + + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如PLC1)
area_name区域名称(如DB100_Write)
offset起始偏移量(字节)
+ +

请求体

+

原始二进制数据

+ +

响应示例

+
+ { + "status": "success", + "plc_name": "PLC1", + "area_name": "DB100_Write", + "offset": 0, + "length": 4, + "plc_connection_status": "connected", + "last_update": 1698754350.789, + "last_update_formatted": "2023-10-30 14:12:30" + } +
+
+ +
+

Batch Read

+
+ POST + /api/batch_read +
+

批量读取多个区域的数据。

+ +

请求体

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段类型必需描述
plc_namestringPLC名称(与配置中一致)
area_namestring区域名称(与配置中一致)
offsetnumber起始偏移量(字节),默认为0
lengthnumber读取长度(字节),不提供则读取整个区域
+ +

请求示例

+
+ [ + { + "plc_name": "PLC1", + "area_name": "DB100_Read", + "offset": 0, + "length": 4 + }, + { + "plc_name": "PLC1", + "area_name": "DB202_Params", + "offset": 10, + "length": 2 + } + ] +
+ +

响应示例

+
+ [ + { + "plc_name": "PLC1", + "area_name": "DB100_Read", + "status": "success", + "plc_connection_status": "connected", + "last_update": 1698754321.456, + "last_update_formatted": "2023-10-30 14:12:01", + "offset": 0, + "length": 4, + "data": [0, 0, 123, 45] + }, + { + "plc_name": "PLC1", + "area_name": "DB202_Params", + "status": "success", + "plc_connection_status": "connected", + "last_update": 1698754322.123, + "last_update_formatted": "2023-10-30 14:12:02", + "offset": 10, + "length": 2, + "data": [255, 0] + } + ] +
+
+ +
+

Batch Write

+
+ POST + /api/batch_write +
+

批量写入多个区域的数据。

+ +

请求体

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
字段类型必需描述
plc_namestringPLC名称(与配置中一致)
area_namestring区域名称(与配置中一致)
offsetnumber起始偏移量(字节)
dataarray要写入的数据(字节数组)
+ +

请求示例

+
+ [ + { + "plc_name": "PLC1", + "area_name": "DB100_Write", + "offset": 0, + "data": [1, 2, 3, 4] + }, + { + "plc_name": "PLC1", + "area_name": "DB202_Params", + "offset": 10, + "data": [255, 0] + } + ] +
+ +

响应示例

+
+ [ + { + "plc_name": "PLC1", + "area_name": "DB100_Write", + "status": "success", + "plc_connection_status": "connected", + "last_update": 1698754350.789, + "last_update_formatted": "2023-10-30 14:12:30", + "offset": 0, + "length": 4 + }, + { + "plc_name": "PLC1", + "area_name": "DB202_Params", + "status": "success", + "plc_connection_status": "connected", + "last_update": 1698754351.234, + "last_update_formatted": "2023-10-30 14:12:31", + "offset": 10, + "length": 2 + } + ] +
+
+ +
+

Parsed Data

+
+ GET + /api/data// +
+

获取解析后的数据(如果配置了结构)。

+ +

路径参数

+ + + + + + + + + + + + + +
参数描述
plc_namePLC名称(如PLC1)
area_name区域名称(如DB100_Read)
+ +

响应示例(配置了解析结构)

+
+ { + "parsed": { + "temperature": 25.5, + "pressure": 100, + "status": true + }, + "raw_data": [0, 0, 128, 65, 0, 100], + "status": "connected", + "plc_connection_status": "connected", + "last_update": 1698754321.456, + "last_update_formatted": "2023-10-30 14:12:01" + } +
+ +

响应示例(未配置解析结构)

+
+ { + "raw_data": [0, 0, 128, 65, 0, 100], + "status": "connected", + "plc_connection_status": "connected", + "last_update": 1698754321.456, + "last_update_formatted": "2023-10-30 14:12:01" + } +
+
+ +

Configuration API

+ +
+

Get Configuration

+
+ GET + /api/config +
+

获取当前配置。

+ +

认证要求

+

需要Basic Auth认证

+ +

响应示例

+
+ { + "plcs": [ + { + "name": "PLC1", + "ip": "192.168.0.10", + "rack": 0, + "slot": 1, + "areas": [ + { + "name": "DB100_Read", + "type": "read", + "db_number": 100, + "offset": 0, + "size": 4000, + "structure": [ + {"name": "temperature", "type": "real", "offset": 0}, + {"name": "pressure", "type": "int", "offset": 4} + ] + } + ] + } + ] + } +
+
+ +
+

Validate Configuration

+
+ POST + /api/config/validate +
+

验证配置是否有效。

+ +

认证要求

+

需要Basic Auth认证

+ +

请求体

+

要验证的配置JSON

+ +

响应示例(有效)

+
+ { + "valid": true + } +
+ +

响应示例(无效)

+
+ { + "valid": false, + "message": "Invalid configuration: 'ip' is a required property" + } +
+
+ +
+

Save Configuration

+
+ POST + /api/config +
+

保存配置。

+ +

查询参数

+ + + + + + + + + +
参数描述
reload是否立即重载配置(true/false)
+ +

认证要求

+

需要Basic Auth认证

+ +

请求体

+

要保存的配置JSON

+ +

响应示例

+
+ { + "success": true, + "message": "Configuration saved and reload requested" + } +
+
+ + + + + """ + return render_template_string(html) + # =========================== # 数据访问API # =========================== @@ -495,16 +1114,27 @@ class APIServer: @self.app.route("/api/read////", methods=["GET"], endpoint="single_read") def single_read(plc_name, area_name, offset, length): """从指定区域读取数据""" - data, error = self.cache_manager.read_area(plc_name, area_name, offset, length) + data, error, plc_status, update_time = self.cache_manager.read_area(plc_name, area_name, offset, length) if error: - return jsonify({"status": "error", "message": error}), 400 + return jsonify({ + "status": "error", + "plc_name": plc_name, + "area_name": area_name, + "message": error, + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never" + }), 400 return jsonify({ "status": "success", "plc_name": plc_name, "area_name": area_name, "offset": offset, "length": length, - "data": list(data) + "data": list(data), + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) }) # 单个写入接口 @@ -513,17 +1143,35 @@ class APIServer: """向指定区域写入数据""" data = request.data if not data: - return jsonify({"status": "error", "message": "No data provided"}), 400 + # 如果没有提供数据,返回错误 + return jsonify({ + "status": "error", + "message": "No data provided", + "plc_connection_status": self.cache_manager.plc_connection_status.get(plc_name, "unknown"), + "last_update": 0, + "last_update_formatted": "N/A" + }), 400 - success, error = self.cache_manager.write_area(plc_name, area_name, offset, data) + success, error, plc_status, update_time = self.cache_manager.write_area(plc_name, area_name, offset, data) if error: - return jsonify({"status": "error", "message": error}), 400 + return jsonify({ + "status": "error", + "plc_name": plc_name, + "area_name": area_name, + "message": error, + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never" + }), 400 return jsonify({ "status": "success", "plc_name": plc_name, "area_name": area_name, "offset": offset, - "length": len(data) + "length": len(data), + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) }) # 批量读取接口 @@ -531,32 +1179,85 @@ class APIServer: def batch_read(): """批量读取多个区域的数据""" try: + # 确保是JSON请求 + if not request.is_json: + return jsonify({ + "status": "error", + "message": "Request must be JSON (Content-Type: application/json)", + "plc_connection_status": "unknown", + "last_update": 0, + "last_update_formatted": "N/A" + }), 400 + requests = request.json if not isinstance(requests, list): - return jsonify({"status": "error", "message": "Request must be a JSON array"}), 400 + return jsonify({ + "status": "error", + "message": "Request must be a JSON array", + "plc_connection_status": "unknown", + "last_update": 0, + "last_update_formatted": "N/A" + }), 400 + + # 添加详细日志 + self.logger.info(f"Received batch read request: {json.dumps(requests)}") + results = self.cache_manager.batch_read(requests) return jsonify(results) except Exception as e: - return jsonify({"status": "error", "message": str(e)}), 400 + self.logger.error(f"Batch read error: {str(e)}", exc_info=True) + return jsonify({ + "status": "error", + "message": f"Internal server error: {str(e)}", + "plc_connection_status": "unknown", + "last_update": 0, + "last_update_formatted": "N/A" + }), 500 # 批量写入接口 @self.app.route("/api/batch_write", methods=["POST"], endpoint="batch_write") def batch_write(): """批量写入多个区域的数据""" try: + if not request.is_json: + return jsonify({ + "status": "error", + "message": "Request must be JSON (Content-Type: application/json)", + "plc_connection_status": "unknown", + "last_update": 0, + "last_update_formatted": "N/A" + }), 400 + requests = request.json if not isinstance(requests, list): - return jsonify({"status": "error", "message": "Request must be a JSON array"}), 400 + return jsonify({ + "status": "error", + "message": "Request must be a JSON array", + "plc_connection_status": "unknown", + "last_update": 0, + "last_update_formatted": "N/A" + }), 400 + + self.logger.info(f"Received batch write request: {json.dumps(requests)}") + results = self.cache_manager.batch_write(requests) return jsonify(results) except Exception as e: - return jsonify({"status": "error", "message": str(e)}), 400 + self.logger.error(f"Batch write error: {str(e)}", exc_info=True) + return jsonify({ + "status": "error", + "message": f"Internal server error: {str(e)}", + "plc_connection_status": "unknown", + "last_update": 0, + "last_update_formatted": "N/A" + }), 500 # 区域状态检查 @self.app.route("/api/status//", methods=["GET"], endpoint="area_status") def area_status(plc_name, area_name): """获取区域状态""" - return jsonify(self.cache_manager.get_area_status(plc_name, area_name)) + status = self.cache_manager.get_area_status(plc_name, area_name) + return jsonify(status) # 获取解析后的数据 @self.app.route("/api/data//", methods=["GET"], endpoint="get_parsed_data") diff --git a/gateway/cache_manager.py b/gateway/cache_manager.py index 9815846..2ff30c6 100644 --- a/gateway/cache_manager.py +++ b/gateway/cache_manager.py @@ -22,8 +22,9 @@ class CacheManager: self.running = False self.lock = threading.Lock() self.thread = None - self.last_update = {} - self.plc_last_connected = {} # 跟踪PLC最后连接时间 + self.last_update = {} # 区域级最后更新时间 + self.plc_last_connected = {} # PLC级最后连接时间 + self.plc_connection_status = {} # PLC连接状态 self.logger = logging.getLogger("CacheManager") self.init_cache() @@ -34,6 +35,7 @@ class CacheManager: self.cache[plc_name] = {} self.last_update[plc_name] = {} self.plc_last_connected[plc_name] = 0 # 初始化为0(未连接) + self.plc_connection_status[plc_name] = "never_connected" for area in plc["areas"]: name = area["name"] @@ -60,34 +62,39 @@ class CacheManager: # 检查PLC连接状态 plc_connected = client.connected + # 更新PLC连接状态 + with self.lock: + if plc_connected: + self.plc_last_connected[plc_name] = time.time() + self.plc_connection_status[plc_name] = "connected" + else: + if self.plc_last_connected[plc_name] == 0: + self.plc_connection_status[plc_name] = "never_connected" + else: + self.plc_connection_status[plc_name] = "disconnected" + + # 刷新所有可读区域 for area in plc["areas"]: if area["type"] in ["read", "read_write"]: name = area["name"] try: data = client.read_db(area["db_number"], area["offset"], area["size"]) - # 验证数据有效性 - if data and len(data) == area["size"]: - with self.lock: + + # 更新区域状态基于PLC连接状态和读取结果 + with self.lock: + if plc_connected and data and len(data) == area["size"]: self.cache[plc_name][name]["data"] = bytearray(data) self.cache[plc_name][name]["status"] = "connected" self.last_update[plc_name][name] = time.time() - # 更新PLC连接时间 - self.plc_last_connected[plc_name] = time.time() - else: - with self.lock: - self.cache[plc_name][name]["status"] = "disconnected" - self.logger.warning(f"PLC {plc_name} area {name} returned invalid data") + else: + self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name] + # 如果之前有数据,保留旧数据但标记状态 + if self.last_update[plc_name][name] > 0: + self.logger.info(f"PLC {plc_name} area {name} disconnected but keeping last valid data") except Exception as e: with self.lock: - self.cache[plc_name][name]["status"] = "disconnected" - self.logger.warning(f"PLC {plc_name} area {name} disconnected: {e}") - - # 更新所有区域的PLC连接状态 - if not plc_connected: - with self.lock: - for area in plc["areas"]: - name = area["name"] - self.cache[plc_name][name]["status"] = "disconnected" + self.cache[plc_name][name]["status"] = self.plc_connection_status[plc_name] + self.logger.warning(f"Error updating status for {plc_name}/{name}: {e}") time.sleep(self.refresh_interval) except Exception as e: @@ -122,6 +129,16 @@ class CacheManager: self.thread = None self.logger.info("Cache manager stopped") + def get_plc_connection_status(self, plc_name): + """获取PLC连接状态""" + with self.lock: + return self.plc_connection_status.get(plc_name, "unknown") + + def get_last_update_time(self, plc_name, area_name): + """获取区域数据最后更新时间""" + with self.lock: + return self.last_update.get(plc_name, {}).get(area_name, 0) + def get_summary(self): """获取缓存摘要信息""" summary = {} @@ -130,19 +147,18 @@ class CacheManager: summary[plc_name] = {} for area_name, area in areas.items(): last_update = self.last_update[plc_name][area_name] - plc_last_connected = self.plc_last_connected[plc_name] + plc_status = self.plc_connection_status.get(plc_name, "unknown") - # 如果PLC从未连接过,显示特殊状态 - if plc_last_connected == 0: - status = "never_connected" - # 如果PLC断开连接超过5秒,标记为断开 - elif time.time() - plc_last_connected > 5: - status = "disconnected" - else: - status = area["status"] + # 区域状态应与PLC连接状态一致,除非有有效数据 + area_status = area["status"] + if plc_status == "never_connected": + area_status = "never_connected" + elif plc_status == "disconnected" and self.last_update[plc_name][area_name] == 0: + area_status = "disconnected" summary[plc_name][area_name] = { - "status": status, + "status": area_status, + "plc_connection_status": plc_status, "last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never", "size": area["size"], "type": area["type"] @@ -156,18 +172,21 @@ class CacheManager: if not area: return {"status": "not_found", "message": "PLC or area not found"} - # 检查PLC连接状态 - plc_last_connected = self.plc_last_connected.get(plc_name, 0) - if plc_last_connected == 0: - status = "never_connected" - elif time.time() - plc_last_connected > 5: - status = "disconnected" - else: - status = area["status"] - + plc_status = self.plc_connection_status.get(plc_name, "unknown") + last_update = self.last_update.get(plc_name, {}).get(area_name, 0) + + # 区域状态应与PLC连接状态一致,除非有有效数据 + area_status = area["status"] + if plc_status == "never_connected": + area_status = "never_connected" + elif plc_status == "disconnected" and last_update == 0: + area_status = "disconnected" + return { - "status": status, - "last_update": self.last_update[plc_name][area_name], + "status": area_status, + "plc_connection_status": plc_status, + "last_update": last_update, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never", "size": area["size"], "type": area["type"] } @@ -177,12 +196,18 @@ class CacheManager: with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: - return None, "Area not found" + return None, "Area not found", "unknown", 0 if offset + length > area["size"]: - return None, "Offset out of bounds" + return None, "Offset out of bounds", "unknown", 0 client = self.plc_manager.get_plc(plc_name) + plc_status = self.plc_connection_status.get(plc_name, "unknown") + + # 如果PLC未连接,直接返回错误 + if plc_status != "connected": + return None, f"PLC not connected (status: {plc_status})", plc_status, 0 + try: data = client.read_db(area["db_number"], area["offset"] + offset, length) # 验证数据有效性 @@ -190,118 +215,178 @@ class CacheManager: # 更新缓存中的这部分数据 for i in range(length): area["data"][offset + i] = data[i] - self.last_update[plc_name][area_name] = time.time() - self.plc_last_connected[plc_name] = time.time() + update_time = time.time() + self.last_update[plc_name][area_name] = update_time area["status"] = "connected" - return data, None + + return data, None, plc_status, update_time else: - area["status"] = "disconnected" - return None, "Invalid data returned" + area["status"] = plc_status + return None, "Invalid data returned", plc_status, 0 except Exception as e: - area["status"] = "disconnected" + area["status"] = plc_status self.logger.error(f"Read failed for {plc_name}/{area_name}: {e}") - return None, f"Read failed: {str(e)}" + return None, f"Read failed: {str(e)}", plc_status, 0 def write_area(self, plc_name, area_name, offset, data): """单个区域写入""" with self.lock: area = self.cache.get(plc_name, {}).get(area_name) if not area: - return False, "Area not found" + return False, "Area not found", "unknown", 0 if area["type"] not in ["write", "read_write"]: - return False, "Area is read-only" + plc_status = self.plc_connection_status.get(plc_name, "unknown") + return False, "Area is read-only", plc_status, 0 if offset + len(data) > area["size"]: - return False, "Offset out of bounds" + plc_status = self.plc_connection_status.get(plc_name, "unknown") + return False, "Offset out of bounds", plc_status, 0 client = self.plc_manager.get_plc(plc_name) + plc_status = self.plc_connection_status.get(plc_name, "unknown") + + # 如果PLC未连接,直接返回错误 + if plc_status != "connected": + return False, f"PLC not connected (status: {plc_status})", plc_status, 0 + try: success = client.write_db(area["db_number"], area["offset"] + offset, data) if success: # 更新缓存中的这部分数据 for i in range(len(data)): area["data"][offset + i] = data[i] - self.last_update[plc_name][area_name] = time.time() - self.plc_last_connected[plc_name] = time.time() + update_time = time.time() + self.last_update[plc_name][area_name] = update_time area["status"] = "connected (last write)" - return True, None + + return True, None, plc_status, update_time else: - area["status"] = "disconnected" - return False, "Write failed" + area["status"] = plc_status + return False, "Write failed", plc_status, 0 except Exception as e: - area["status"] = "disconnected" + area["status"] = plc_status self.logger.error(f"Write failed for {plc_name}/{area_name}: {e}") - return False, f"Write failed: {str(e)}" + return False, f"Write failed: {str(e)}", plc_status, 0 def batch_read(self, requests): """批量读取""" results = [] - for req in requests: - plc_name = req["plc_name"] - area_name = req["area_name"] - offset = req.get("offset", 0) - length = req.get("length", None) - - area = self.cache.get(plc_name, {}).get(area_name) - if not area: - results.append({ - "plc_name": plc_name, - "area_name": area_name, - "status": "error", - "message": "Area not found" - }) - continue + with self.lock: + for req in requests: + plc_name = req["plc_name"] + area_name = req["area_name"] + offset = req.get("offset", 0) + length = req.get("length", None) - # 如果未指定length,读取整个区域 - if length is None: - length = area["size"] - offset - - data, error = self.read_area(plc_name, area_name, offset, length) - if error: - results.append({ - "plc_name": plc_name, - "area_name": area_name, - "status": "error", - "message": error - }) - else: - results.append({ - "plc_name": plc_name, - "area_name": area_name, - "status": "success", - "offset": offset, - "length": length, - "data": list(data) - }) + # 获取PLC连接状态 + plc_status = self.plc_connection_status.get(plc_name, "unknown") + + # 如果PLC未连接,直接返回错误 + if plc_status != "connected": + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "error", + "plc_connection_status": plc_status, + "last_update": 0, + "last_update_formatted": "N/A", + "message": f"PLC not connected (status: {plc_status})" + }) + continue + + area = self.cache.get(plc_name, {}).get(area_name) + if not area: + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "error", + "plc_connection_status": plc_status, + "last_update": 0, + "last_update_formatted": "N/A", + "message": "Area not found" + }) + continue + + # 如果未指定length,读取整个区域 + if length is None: + length = area["size"] - offset + + data, error, _, update_time = self.read_area(plc_name, area_name, offset, length) + if error: + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "error", + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never", + "message": error + }) + else: + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "success", + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)), + "offset": offset, + "length": length, + "data": list(data) + }) return results def batch_write(self, requests): """批量写入""" results = [] - for req in requests: - plc_name = req["plc_name"] - area_name = req["area_name"] - offset = req["offset"] - data = bytes(req["data"]) - - success, error = self.write_area(plc_name, area_name, offset, data) - if error: - results.append({ - "plc_name": plc_name, - "area_name": area_name, - "status": "error", - "message": error, - "offset": offset - }) - else: - results.append({ - "plc_name": plc_name, - "area_name": area_name, - "status": "success", - "offset": offset, - "length": len(data) - }) + with self.lock: + for req in requests: + plc_name = req["plc_name"] + area_name = req["area_name"] + offset = req["offset"] + data = bytes(req["data"]) + + # 获取PLC连接状态 + plc_status = self.plc_connection_status.get(plc_name, "unknown") + + # 如果PLC未连接,直接返回错误 + if plc_status != "connected": + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "error", + "plc_connection_status": plc_status, + "last_update": 0, + "last_update_formatted": "N/A", + "message": f"PLC not connected (status: {plc_status})", + "offset": offset + }) + continue + + success, error, _, update_time = self.write_area(plc_name, area_name, offset, data) + if error: + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "error", + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)) if update_time > 0 else "Never", + "message": error, + "offset": offset + }) + else: + results.append({ + "plc_name": plc_name, + "area_name": area_name, + "status": "success", + "plc_connection_status": plc_status, + "last_update": update_time, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(update_time)), + "offset": offset, + "length": len(data) + }) return results def get_parsed_data(self, plc_name, area_name): @@ -313,22 +398,29 @@ class CacheManager: if not area: return {"error": "Area not found"} - # 检查PLC连接状态 - plc_last_connected = self.plc_last_connected.get(plc_name, 0) - if plc_last_connected == 0: - status = "never_connected" - elif time.time() - plc_last_connected > 5: - status = "disconnected" - else: - status = area["status"] - + plc_status = self.plc_connection_status.get(plc_name, "unknown") + last_update = self.last_update.get(plc_name, {}).get(area_name, 0) + + # 区域状态应与PLC连接状态一致,除非有有效数据 + area_status = area["status"] + if plc_status == "never_connected": + area_status = "never_connected" + elif plc_status == "disconnected" and last_update == 0: + area_status = "disconnected" + structure = area.get("structure", []) if structure: - return parse_data(area["data"], structure) + parsed = parse_data(area["data"], structure) + parsed["status"] = area_status + parsed["plc_connection_status"] = plc_status + parsed["last_update"] = last_update + parsed["last_update_formatted"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never" + return parsed else: return { "raw_data": list(area["data"]), - "status": status, - "last_update": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.last_update[plc_name][area_name])) - if self.last_update[plc_name][area_name] > 0 else "Never" + "status": area_status, + "plc_connection_status": plc_status, + "last_update": last_update, + "last_update_formatted": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_update)) if last_update > 0 else "Never" } \ No newline at end of file diff --git a/gateway/data_parser.py b/gateway/data_parser.py index 38890b3..0c4e236 100644 --- a/gateway/data_parser.py +++ b/gateway/data_parser.py @@ -1,4 +1,5 @@ from struct import unpack +import time def parse_data(data, structure): """解析结构化数据""" diff --git a/gateway/snap7_client.py b/gateway/snap7_client.py index 3bc94dc..2c7a7fd 100644 --- a/gateway/snap7_client.py +++ b/gateway/snap7_client.py @@ -81,11 +81,11 @@ class Snap7Client: size: 读取字节数 Returns: - bytearray: 读取的数据 + bytearray: 读取的数据,如果失败返回None """ if not self.connected and not self.connect(): self.logger.warning(f"Read failed: not connected to {self.ip}") - return b'\x00' * size + return None # 返回None而不是零填充数据 try: with self.lock: @@ -94,12 +94,12 @@ class Snap7Client: if data is None or len(data) != size: self.connected = False self.logger.error(f"Read DB{db_number} returned invalid data size (expected {size}, got {len(data) if data else 0})") - return b'\x00' * size + return None return data except Exception as e: self.logger.error(f"Read DB{db_number} error: {e}") self.connected = False - return b'\x00' * size + return None def write_db(self, db_number, offset, data): """