From 7369e8da24cf428716a626e095a8172d3a3e6c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=B5=B7=E4=B8=9C?= <535915157@qq.com> Date: Thu, 16 Dec 2021 00:16:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E5=85=A5=E4=BA=86NModbus=E6=BA=90?= =?UTF-8?q?=E7=A0=81=EF=BC=8C=E5=B9=B6=E4=BC=98=E5=8C=96=E5=AE=8C=E5=96=84?= =?UTF-8?q?Modbus=E9=A9=B1=E5=8A=A8=E6=94=AF=E6=8C=81=E5=A4=9A=E7=A7=8D?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vs/IoTGateway/DesignTimeBuild/.dtbcache.v2 | Bin 394265 -> 394265 bytes .vs/IoTGateway/v16/.suo | Bin 60928 -> 76288 bytes .../DriverModbusTCP/DriverModbusTCP.csproj | 12 +- Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs | 107 ++++- .../NModbus4/Data/DataStore.cs | 176 +++++++ .../NModbus4/Data/DataStoreEventArgs.cs | 73 +++ .../NModbus4/Data/DataStoreFactory.cs | 53 ++ .../NModbus4/Data/DiscreteCollection.cs | 124 +++++ .../Data/IModbusMessageDataCollection.cs | 22 + .../NModbus4/Data/ModbusDataCollection.cs | 128 +++++ .../NModbus4/Data/ModbusDataType.cs | 28 ++ .../NModbus4/Data/RegisterCollection.cs | 86 ++++ .../NModbus4/Device/IModbusMaster.cs | 191 ++++++++ .../NModbus4/Device/IModbusSerialMaster.cs | 26 + .../NModbus4/Device/ModbusDevice.cs | 53 ++ .../NModbus4/Device/ModbusIpMaster.cs | 313 ++++++++++++ .../NModbus4/Device/ModbusMaster.cs | 452 ++++++++++++++++++ .../Device/ModbusMasterTcpConnection.cs | 122 +++++ .../NModbus4/Device/ModbusSerialMaster.cs | 175 +++++++ .../NModbus4/Device/ModbusSerialSlave.cs | 153 ++++++ .../NModbus4/Device/ModbusSlave.cs | 271 +++++++++++ .../Device/ModbusSlaveRequestEventArgs.cs | 27 ++ .../NModbus4/Device/ModbusTcpSlave.cs | 203 ++++++++ .../NModbus4/Device/ModbusUdpSlave.cs | 87 ++++ .../NModbus4/Device/TcpConnectionEventArgs.cs | 24 + .../NModbus4/Extensions/Enron/EnronModbus.cs | 163 +++++++ .../NModbus4/GlobalSuppressions.cs | 8 + .../NModbus4/IO/EmptyTransport.cs | 33 ++ .../NModbus4/IO/IStreamResource.cs | 48 ++ .../NModbus4/IO/ModbusAsciiTransport.cs | 70 +++ .../NModbus4/IO/ModbusIpTransport.cs | 163 +++++++ .../NModbus4/IO/ModbusRtuTransport.cs | 142 ++++++ .../NModbus4/IO/ModbusSerialTransport.cs | 67 +++ .../NModbus4/IO/ModbusTransport.cs | 310 ++++++++++++ .../NModbus4/IO/StreamResourceUtility.cs | 27 ++ .../NModbus4/IO/TcpClientAdapter.cs | 70 +++ .../NModbus4/IO/UdpClientAdapter.cs | 158 ++++++ .../NModbus4/InvalidModbusRequestException.cs | 95 ++++ .../NModbus4/Message/AbstractModbusMessage.cs | 77 +++ .../Message/AbstractModbusMessageWithData.cs | 23 + .../Message/DiagnosticsRequestResponse.cs | 53 ++ .../NModbus4/Message/IModbusMessage.cs | 43 ++ .../NModbus4/Message/IModbusRequest.cs | 13 + .../NModbus4/Message/ModbusMessageFactory.cs | 82 ++++ .../NModbus4/Message/ModbusMessageImpl.cs | 117 +++++ .../Message/ReadCoilsInputsRequest.cs | 76 +++ .../Message/ReadCoilsInputsResponse.cs | 50 ++ .../ReadHoldingInputRegistersRequest.cs | 76 +++ .../ReadHoldingInputRegistersResponse.cs | 56 +++ .../ReadWriteMultipleRegistersRequest.cs | 108 +++++ .../Message/SlaveExceptionResponse.cs | 80 ++++ .../Message/WriteMultipleCoilsRequest.cs | 108 +++++ .../Message/WriteMultipleCoilsResponse.cs | 61 +++ .../Message/WriteMultipleRegistersRequest.cs | 99 ++++ .../Message/WriteMultipleRegistersResponse.cs | 61 +++ .../Message/WriteSingleCoilRequestResponse.cs | 69 +++ .../WriteSingleRegisterRequestResponse.cs | 67 +++ .../DriverModbusTCP/NModbus4/Modbus.cs | 60 +++ .../NModbus4/Properties/AssemblyInfo.cs | 29 ++ .../DriverModbusTCP/NModbus4/Resources.cs | 41 ++ .../NModbus4/SerialPortAdapter.cs | 71 +++ .../NModbus4/SlaveException.cs | 173 +++++++ .../NModbus4/Unme.Common/DisposableUtility.cs | 19 + .../NModbus4/Unme.Common/SequenceUtility.cs | 32 ++ .../NModbus4/Utility/DiscriminatedUnion.cs | 122 +++++ .../NModbus4/Utility/ModbusUtility.cs | 217 +++++++++ .../DriverModbusTCP/System.IO.Ports.dll | Bin 0 -> 39536 bytes Plugins/Plugin/DrvierService.cs | 4 +- README.md | 13 +- 69 files changed, 6338 insertions(+), 22 deletions(-) create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStore.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreEventArgs.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreFactory.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DiscreteCollection.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/IModbusMessageDataCollection.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataCollection.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataType.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Data/RegisterCollection.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusMaster.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusSerialMaster.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusDevice.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusIpMaster.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMaster.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMasterTcpConnection.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialMaster.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialSlave.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlave.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlaveRequestEventArgs.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusTcpSlave.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusUdpSlave.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Device/TcpConnectionEventArgs.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Extensions/Enron/EnronModbus.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/GlobalSuppressions.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/EmptyTransport.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/IStreamResource.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusAsciiTransport.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusIpTransport.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusRtuTransport.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusSerialTransport.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusTransport.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/StreamResourceUtility.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/TcpClientAdapter.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/IO/UdpClientAdapter.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/InvalidModbusRequestException.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessage.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessageWithData.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/DiagnosticsRequestResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusMessage.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusRequest.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageFactory.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageImpl.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsRequest.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersRequest.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/SlaveExceptionResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsRequest.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersRequest.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleCoilRequestResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleRegisterRequestResponse.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Modbus.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Properties/AssemblyInfo.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Resources.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/SerialPortAdapter.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/SlaveException.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/DisposableUtility.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/SequenceUtility.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/DiscriminatedUnion.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/ModbusUtility.cs create mode 100644 Plugins/Drivers/DriverModbusTCP/System.IO.Ports.dll diff --git a/.vs/IoTGateway/DesignTimeBuild/.dtbcache.v2 b/.vs/IoTGateway/DesignTimeBuild/.dtbcache.v2 index 9c0d62774fe322daca6ecf0ebbf96652fb26304f..3af6b3defb19742fec440da06dd13b686aa25370 100644 GIT binary patch literal 394265 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�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�|XA> z2mNfm0Ss=dNvWApwJ7{oTarL1Q;#D=oo+@F^qr#P6N&mU}%f##E0yZQJpEBGYm@MZbqUo@&sVG3ByZ( zDnL~M297XL1YmRsgF%4mKn(##c`&F0U?c|vHvmR!FggRM4V)*yunUG+0F13*ECo;( zxIlnG5{!TV7zV)@2jC*$Vu6c+O9U9Kz(@q(Qs6QHh8!@$0AN@FV+H^Q4ya*#F^m=% zPGL;I2+D{Ir}BAJ&hL+=ADvNSC}T2eEM=^mUFT6wv2*?$kkjga-qp}Jh0Dj_JT50` z0nXHN8W!MOteV58!wJ~A6i&0wqi`~HK4pGJEubvOsD+e;8MTPAD5Dlr7H8BF%94y) zN?Dpw%P7k-YB^9m_g=8cE#mrlDy?%@rD z?UzpLB=_(}z(4=eX}8K(${$RB{H4=6%bxkOK%bSWr7Nc>OkdWwhqZLqkfK|r`TGEc zKMM36ym0TQ(lYzGTX=!KGo-M-b@!0WL;Xs?cPzx_)_E@0f3 zkNZHV`|;WJ*ZsqKe8>TzelXO>r@;R5K?jEVp->MB^~1Q_<3Fms7}?(DVq}9unM(bL z*$y#Vb0rg9#eCcyCT17o?!mfvs-`ZH(nnBAru31NQYl@Ua#TtmO*tl|kEN7J>EkG6 zQ+!S}b-9$gD6`BY@2AaNZSz;o)<^uyxSjpm?f=QP|5mGU{3A2&Z}LxNN&!a+90eRBa12mZpe#^Mpd3(MfHT_3 zr9oSB5ZcJmXalqrXbZFx_}}{XZM1yJqxss$2#f*735)~A3ycRQ2uuJb3QPnh3-Av4 z_J6yk%RSS%XNJHGV5Y!KV3xovV79<)V2;2XV6MPiV4lD{V7|b7V1d8_V4=W5V3EKg zV6nhrV2QvIV5z`T;Lp3-=LY!@8~6|#1vUbk1vUd)1hxQM1-1hJ(M3MxT2cM|wW8$R zss8*EAn#7Oe-kot-3$3uYTx2g|8~;XpFgNt{pNt_vF?+6jz7N&zVk_IZfcwpo|j+t z_T4;)o83~AF{}1SEpbQcQ~49Hz?dXjD=c-wXY$8kL1C#AmO0^b`LnU0u*?a`m{ofu z*SiB{{xU9jpw_zswZREL$luHbg$-8V<$*IcnO6!06zT0p`OV>`|IERph5f0HJp*|k zpy1(?-rRUQz#c!nZy;|CL?n7E74g!3fxK-Hk?5_ozn2aOw6@Z#P6F zdMg$6(!qhe6%mo>t#pW&4h`gOiikvSrNg}RYvVfqYX@wW^U5^FNZG8tsa+hYI3m016{ z@soeYp^>{Z>T#C_G$vyedC$tdGwh(vFtWEi4O`B8y6DL8tZ=~ek^2bPTFx^aagDy=bRmP_;Yr$B*4};|Kk+_oU`Q$_L5m`h&CX2}@WC{6{?CfglUvmFS z@)cP{z9y^5Klv)^{tD{xtEbDA)6G@Wsekr5HM?@P^qpNH{Ry4}BS`!T=}q{YpXC06 zYt)RSe~OGEPm|H)88U`EOU9Dt$T;#mk*kuAr^*${&B=4KBFTWAtvMo#ad2*Zid)QN zQtg80)?Wz!yv0oV>k003{fpaA*H7ff`K9oWT4ZEvV?8M}tBkyC%Vb+#v1N)aui7%z zme*{VX3OigOt<9?TV~kurY$pVdCQhrw!Cf2Y+K&3WsWWH+A`Oc_iUMG%lo#>x8(y{ z7TEHkEemb=$d*O6d~C~NTRyR6i7lVnvecH(Y*}W@=e8`jBi>7PnN*Ubg$y6>A3n zz7=aO<#&2xvUgsxdQGx=&7Z$|&Eq`R;w#w5>!ROU!Db(xpJVZMwpL)WlFdJ7{lI_9 zN;doQeEoN;*%aZi{mB00QeIOvCYKR;t#$zS?aFF42jU$>ijsrLA>>eU7&)92BgIJx zas(+!jwGc>X>t@fnjAxpC1r@LNOK%jUhkEq9#8x#G=uf-DZjc*AsOsgF3P% z%y{ywx2Jd~6z~cc^vdV?{hgSHcTxeb{C3W$e6Ljyt>X5FRDOYvx_9=Y?js-drkw0! zdhdcCwMf=GS$5ZoZyCrMxFb6nUbBDQZZtg=Zc?HeXY;#k!?dxcDo?Pa@ z8B7JiFk5OJXPhrrKyXGKW0);QU1wY%7e{bLU1OLnMm=X-DA!GJMm=MgEk=E3TqKuN za7KM&m@P&FXIv~-TX04LW0);QvgS@kC2Q^^YwpMog}KRvJuD<^?#Qbmb*o&X#QpB* zBu9qc+*D`x+vPScU*e6<#xPr6OeAaWm=~7GnmfswJO5p4?u>Fj+E1%d*&ppu=11FX z`6ck1lT63BT|6t7`fZ&}uFk3ZQkSAG?ho>Hwd2&yv zWI-N&{uaBR&`;zlRs|-*(wdt}CO6yTu*MyzZ{>Pj1rO93cc9if;XApKS3zN|6V^H5 zd%4C}L1CQ}loPaE{mcEfRPq?TxvAeh{aI~vA7c|26Z0Qqqxl$S%g;x$AW!7GBUeo` zH?EpykDbm7XMJ()5&)UQxp3H21plu*A)c{S8iDN{rJ8s)W6Poqo=_3M<^Lp_}`J=AYd z-U#&!%8XFINqIBWGbuAe{TAh|P|u>w3iaERw?jRfGCS1oP~Hjk9Lk(fze{;H)N?6w zL;W7*y-?4i%nSAVl=nkDpE5txA5cCB^#aO*P=84IFw_ev3q$=8<)ct9qAUva$CQsl zy_m8%)Spm33H1`nl2Ct2`83o^DN95B8RfH3FQY6A_2-n&L%p1`Jk(!Mz6kXS%8F2b zN%=C=D=8~O{T1b_P_Lq_3ia2NuS30>vO3h?P`(NE8p@hbe@po`)N3hgL;W4)yHKyA ztPAz`lr&vOd%qN+#4BC>uik1LcQMZ=`Gt^(M-uP;aJe4)u?eA49!` zvL)0%QGN>bR?5~;|4jKg)Y~ZA7%NBlCDgm+)g?Nh1bI3|N#*GrN=}~6rR3)6G$oy< zLrR#Z^C)?FI-ioCr+269o~H{@3gziND0}4T!j!^!dQZxpd46f8z3BGJvnw<;`8O*x zox+9XPfh7lQp`xwr_r1)g&9e@N?t8pHIJgp=Oqg{B?~zv3ppLYMe~z|oRWo{{*#5A zMlm*)ks-gjo|9iJ;{Ujsldi^Z>gstEm9Nj>SNNGJ3UfHsbUyg>jG952kx?@#Gc#%y zWmZPbrp%TVp18<+vdj|)fwPaCQv^7%$T>xT%Q+{@JaH<7vmhLenm{dqT0m`q+CUwF zIzU~4x`OrWw@i@ znoqU!>gd{e`n){0h;{OGuN-}Tp010@{rBbQJ~{gS99DCl&GS?Z49%;IMx|u)UjFPSYwzi#&OOltB%VW#~H(HF^+deIdy#2INlg$ zi*bT8%BvHy#tFtSTZ|K(agsVQYn*5dv&E?3jEbs4)~H|%v&A^s8I{z@S>t45m@URB z&Nx+_k~K~-hS_4A=8V(TX<6emW0)<*8O}IUosl)pFoxM;oaKzl>a47BmNCp0qlz=C zve2&ozEm-W*YqT|n*WW{c6m88@pAS)+q7%od}gGj35Gvqnc_m@P&pXZ()I zJF1J@$L*?1_M>z$AH{6>C|#X#hw7R&x*Ef5F}gY9PSq`IbTfw8Vsv-LU8;N5=xz+N z#pvOTyH$^@(Zd*Ki_y~=_o$v(qo*;<7NeIl?p3|AMlWNSEk*jm&-$Bh4pawtNz!obms4DoZj)MNKu|6|?2LGR=KgURTqy-?wST zFk6i2&Ui!K>v6xZryIj;F=ja9O?ey088eJwwiq*=@s_+pDnTX(VLt4XRfA~%mlID zUY6~%&j9^5TEefG|D1{kx)>wj?DJBPn=8P7?Mi=T{MUC}pv!Fk`at>mYypx50rlzr zNSzVDkzZlbo0~qtZ>6eUsut*~F^S$v)xC6fpsU9udMhQ9 zJU9d6ezRN@$k~{J|NKaAk5e+qgELI-I9(OU*`~;GqPNGXg_o`l3gVx;BtASP_ZdO0B(gT_9(-A`-oo+IZ>uKo)?HNc2`}>!lk4SsXqh(OapV zmy!hmlSv+|*dFLUv8M{XeX8{JPn8D(-8UxDTdALy`UkpSOrp2a053fl=m9Z_-bw?# z^iZG&#w2o=-9iS+rBSQ z_GJr@tZA66X_(PV{gdUh0FM4((VLrI=B3XAI4aRwX}Ooa2xM$U>PvJ!JyDo)71Kw}9N_9{T||cj}7?bF& zwAo8P26}T$qPNl(FZ~qgEisASN?X13bD+1zBzh}t^U^PY-WHSSt@Ia9_}HbT5A#{X z@S#6?^^QMnRgV2+^^SkLD#rn`jH5o#Y-Lrf{`#=gIr{L_?_A>T1a>uWXUo~?lTznq z*f303paNhQz%%Pfn#l-Te^v+7lJPt0UnTwzfM_^!1aSEevj-VXD zXc`LRXi8HUEORu45i!S781`};g|RKiQy9Q<0)UGAD}^)B-6))H?oR2>>0=5fj(bu#JKT%Xi?h8H&hz%6a5A?qg)_GOD4doZK;hi$ zKnf>V2T?eSI+((#(jgSihYqE1(sLMPSVj$}49}<$lo1&7ers<{--Ma`pd#%Vr<^GXXSoIP4d;T+K-3TJ{AQx<2`63UW{T1r`(QOhXHGHN+x zc}A_EtjMU9l$9B^in1!BR#R4I)Edf~j9N=sn^Eg1>$s#9MP<}_%KD7jK-rK{8z~zz zYBOas=LaZTGHNSjYesFOY|H4~WX$4jj9E+xq<~z3Tp$z(fqa2{;LjWMTueShF+M~I zff7JTfs#Nefl|Ox0!IPI2pj{H5hw$c6(|do6DS9i7bp*$BybW?QJ^AFNuUyNs=%qh z=>n$%X9}DNR2HZVR28TSR2QfY)DWlv)D)-*)Dox#)E1}>Bx9X92tDO!^aOed^a6Sd z^alC}^a1(`@Gby<^aK2*&mZ={0AQd1f3foiIxq+rEWlshdWgUfV5q=QV3@!#V7S0= zV1&R3V5GoEV3fcpV6?z!V2r>RV64DcV4MJdLGlM9z+Z{{Q3y-`CJIahCJ9UeCJRgk zrU*;{rV302rU^^~rVC65W(dpxW(v#%W(mv!W(&*)<_OFI<_gRO<_XLL<_pXR76>c= z778o`76~i@77Hu}mIy2XmI^EdmI*8amJ2KgRtT&BRtl^HRtc;ERtu~K)(ETt)(Wfz z)(NZwlz;-(3#D{?JqO|Bzt$n`{)x4wb2 zBR7)vTSzBzE9p#bBVEYtq$|0DbR&0??&L1ggWOGel6y!maxdvk?jwE3 z{iH8>fb=8%$pG>o8Au)?gUG{VFnNRwAqAJ?9!4KuhVu6=LHS26>-`p=pI_R0HvA5m zL;l_J-XHMT0wT+NFC@RV)b~;z-`R5CE4cql^1rs^cL|m{^vix93CEZIK8E{b`R_7B zmH;nHjwgTiGT=4&7`w6*cs;zGEeC!XOE)$lJ6jI?a_%p<9Qc*&qd95H{a27?Av?Vu?cH~CVp4>z_kef+IatrB1ZY7<`ZKMmiopdF4 zkZ$Bo(w*EzdXT$GPjV0GMeZfN$$g{`xu5hU50HMOKN&zCBm>DqWDt3n3?`3|A>>gq zlsra;5n1~3aVpDRcI!6P%>h|kzTg~?;cgDd0R05>PoBurBl6fX{o_e9|72vIev0`Y z++Se+$tW{hgh}b6^7PYrY|%%{Y>{X3^cZaJ*kQKFrewAVe+qN6TWT^}B$+LulG!5W zt;XM8!Y7$6!aGg!>%(lR$!w91=6IMb$0M07($gFdv*masvqc7(<6*WOkN=_BBBQHk z%&JGvsL_!VWwhp|#yH{szN|?y7bKYrl2OTA5c`AnnVe53ICo&F`$1dge$YObGYtiW zWlmV`gfHYAL_uM>6IM9kOF27HP*~xFl}`9d&RY}|Rytvo6TX%+8U=+_PDtj0Y;;E= znF|tmh9s+k*ylw?@9&319O%bQBy&L+2U9wb^MnO|sYq{boHw+;cl6PLoI{LA^j12? zOUDLsela4^TPc|f!l6FZAF0yS|5sBVP^}KXpAXndtNc2{!@18Q|x-XE?8xe`#N`1VP%mvZOTo7I&JQK(m zo5+)e-ac8z_$SM=fsDV2Nc2`p=7Pv^U|f-V36abNF{|vo6UbPp$Ww;iK4s?kWA$zz zAgV4SVbgyE6wxL`+?=9?Eiti*9>wAMtL=$v(zI zS4UaKVXW zk_YG88NT1@pE${5G+!9!3CB>zWYk#7*v#*p-SHn@>OPs#u@@t=_mLsm`v64*iU9iy z@ZK|*AOa2qiV7434iPv6I85L$pqM~0poBmPprk-app*db%qBBBnsN}D$rqu_I1U@g4`d_RL^hKj$rkbx*-CyU+sH3uH^$ZnBt>#aE=iM+B?ps3$f4vgayThQij#lpT#s?gS9qT6Y_7+6?%&y5 zk0QA|mw9Xgd6CGxhlx}(@8LB!@8Q@I1?N4ylwJE`l9|o$a-N=)rzgvn*$l79Y=$X$ z`c-B#aK{d_8HW7!dH{dHb)$cCoZndcW{P@FUeXsF`ZL!4y}Jve_>$`an?t3RxI_7= z%pfm#D3`cHxzq{CSTH*#?mL+)U+_S!bq8vlJ5b-tEc$}NIw!1mf|*!fP+0GT4Nmw$ zrrZ}4HdsMF8aiVW7*V7*x8J_*??3aZzJ>i8ta}Esh+4tJC%w7xN2oo1df!0)AdN`$ zRx0A9{Q~)8H6qblX@4&r5Xc|05sBVPf5$;pCHyCTL?C}lM?P_S`-zwI(vgAuogI4ZQo-Vl-KtyJDiCkAq1hloUPrIWl=A&`qc zL?n7ERrJ!yfm{$GBGFr^l9x^i1q56{)-^j2!-rDV92z9G4E?(Ko@5__u9+owu0T#Ad|xL@|k zaH)UhaH-M$8~nS+L5=f!dj9WovD?Z1b9yC^D8B@O2!)LWUP_? z>VNjSvm5*mLUKV_os*KwlSO`E(A!@axheOrc{(MRE{jO?R@&tY%Jz>B4tm4=iO7DU zgMTHH+oc(Vu-022$L`HGw$Y_ke#l*4y<;f;qoEUJ<%9B;%Kz~im{ark-+3=0xfo`9j$sEmm>qzQ0v&-) z0-b=)0-b>_0{l(Gj1QnI&`qEl&|RQA&_jSJ8GpNrVeWVh%w4(k&W*+5lz;U|Hc#U{=$m!iz>aMs+z}r%@N-`9*IWZp@0ssj9PgD6XBV#k<@KyBToLJTYpG&#{eHW^ z1;ov6sl}a9LN4v^jN-;HTZ|)|QBtn@?~EgiVYV1YI-`^<2;ht(jbXMJrJZq!KI|nyZmzJSQ{0YDl{FdMj!rQj%4{)CbH?eiT!S-CGltn> zoZ*Z!WhDn^oM8;J#W>3um1U6!XPjjWv*i&kq>7zP&8YBBfM5JAI@r^Xtn@HdaYDGoYg?lYynDp?WlY>tAV1~ z0vzqNWAfpw28w12-=}Ks`&3<3^A5z#ZmDNGqlWy|TX49?*`guTbV715O?x~Vy5n() zYM4Ew4b71*YRGiV&7-oyn%o$h8+A7XyW(>2%Xzq-wWZ4yGG&hFXVzh9^)v_XsGg=tKY%y9o z;~H74#ThM)VYV2poN=v8@o`2gW0);QYiC?1OT0LvwK2>Vqm47Jm(^dK(Z(2Ni_z8@ zH^{;;&S+~4v&CrVj2mUm7-zIIhS_4Ycg9V!oQyNt8^dfdIymEIS!u=@9gJbN7#*E) zi!4IpjE=@ITa4slnmx_&Fk6mCFL%W5m34RA5$k0Pv&HD`jQeCs9%uA6hS_5DamM|! zT8}gO7{hEa`a0tQS;)s3eT`wZ82y~lU)K0>Mn7YiEye(6JSfZmIAee@%obyyGaiza zft)eW7-owx$QcjIqCw6WWDK*#NG_%+bLPzL54f17ZXVhruCEGVf6N8BxdOEC+SMVP z)j-i~;hQtwefwTex}XS}E;WQ_^NFk6g?&Ui^p%o-DoVYV2Pobj@nlr<(9 z!)!4oJL45KIcrQdhS_3FamK4^O4gWS470_U>WtU8+@^mjPBn(vVoYuOrom}U&K z#hC7lH`Mg3G2Iwui!s9)Z>kwtV}>!z7GtI}-cmEO#!O?FEygToysc(sjakMpTa4Mx zct_378ncaIwiwA^YdIV&(&dg+GT7RVrT$iyA1nCEca1qtEJx-J)c<|3wQ_sVvY46s zU9ZeH!EE_v{O;k{YNPuYn`CV?_c1n_k72fajLpvYQI<_B_=RqEU+5Mm{3Hvixm|2A zyD(dJ@y{Hto$t>F?;gm4aPCVNjK?6Ced4| zjhC(ubeot&Z>6?gN=B0FWF)!1FS+pMBY`aa8u(xLO_x1Y%WnZ=cyEMvun%~#!fh-H`_XWznYypzN@LX)uy=r+sx$x#^ zfh^t}`K>^2ZY<{Pj??FXEbJVS=&iKeOJ4-C=yOD(x6%qPeHqAt&=HB=N-MqezdIOy zy*~t*Ko-3AKf@q5xyK%l#(fZB&AeJm!=$*(nnK{N$F!LWm5V$O4*b?o>DHQPvHL^xYH5% zC#BBK=#%(FD+p8o;$!lE&Szv-t&BP#qYkDVoKc5T4wa!+waj>{UATTGqnL1IaMRKJ zzei`(v6N#o>Nv`A8Ff76_>4M%azaL(NI6kP0r9`c;Ghch6&UJ6Iax;RaJLNPIfb6F zI;T+>kaGrwQ8#B%&XRF5NHTn;3VjtB?n2L)7Y4KdjA~)X3c$D(2BiRuOkvmwz}OQ8 zmH><{Vdx0J_z?z&0E`e}_y@ok5C(VvGVY@qAD|keI4F$Ys7a}rQMD-iS6h-%btrW* zsxGB&M%AO#%c%O4`We-L(jcQ6QW`RFfx?i5#uUaSG@&r~pecnB2F)l8BWO-(&aeOq z0|Hu7TK44e&|K;e|%L<;BiCQ&$fH<`j2x+xS+!%d}d zu5B8H6Kc~boHd(4nZa2w3g^FOQ8>vpn=+e|R+KpzHJ36sqvlcOWz>Ai{ES*aS&&f+ zDGM`d5oJ+EEv78yBoAdtMlGc*&8TIRWf`@cvOJ?!P*!BrO3KQNT18ovQL8DdGinWm z(;;gqYcpycWnD(elZg`;>nZCqY6E3MMs1{U#=*@h*+R$3ga3STNq)dVUzXz*|ealLYYg z5$_iLzxK`q-pXnJ|K*m>>Fgor*_-Q1W->;G6!!`l5|t^DDWVcm)D;;rl#+(iq@+-W zQYs=ORMMb%(x4={QDT2PqKN+Q?^=7U^X!M~@BV*Yzx%)L-ks0+e)jiy_w2Kuv-dvF zv)1~qg`tUv79kAHK(zW`XzGz$)!d4?P0ekX_G;Q={_q7T&*?Me;*~`rFfU+6s~L?M zqlU{&t}rn#VaBT&k9k?m%b1C3CSqPw!-XB!bQrBoc~cFSXJthyZ)=8&EUvFG?_#E@ znTB~!&3l;l)x3|HrDhgpj+!}`xoYNO=Bt^HS*T_qX0e*Zn5Al#VwS5}j#;5*1!k3+ zRha+AxjXxDLUmr*8CIzroqP9Kb2ob~RVsE0&l@Z16rME;^!xGV`w8azxttaCk@|f9 zk%>?K)ryxURC-*7nxYdKUH?l{c%DT5TDCVw*P-iC3v>gj#kL!XEz!yNR>aomCe#K^ zXWGA;iM82Q2c3dWMSn$qL#Ls-=yY@jx&>CNaN45VP&?Ef-Hz@+9Z*N~cXTJ}gziFj zqt56abT7IO-H*DU2hfA)A@nfnin^gkPjY`lA78AR2@o zM}yH26s{VIPW^d6#kz7J8VUC-dJa90UO=PJX!If)i(W$G(0DWf{hk#^GuWT2I+}%_ zjpm>a&|EYR%|{E+LbM1iMp}Wi6#ZHi(noCnrAnmr(3R*abTzsLU5lEd>(KS61-b#Q#7zv9l@mR;CEQw+yEVkTZcP3?d6eD4=RUW%5DXXFw35%`q z*gZ*E6U9haY@NrtBxPL`BVn-(9(yP$8=@Eqi*5ASBT3m9#Yk9elgD}{Wm6O*VX-Y9 z>z$M>QH+Ge#AE%EB2kQl#kP5DKvK3vF%lNr?y>*v5H0RefnHdDi%fF&Lk{W;IUCjDTrbuELP;PF-a+kVk9h< z^w_whB%>G!iv=EgIVnLDBVn<$$0jBv9mPmkY%h;ZO3GeQjD*GZ@z|S5*(ZvTu-LvH zo061$qZkQ`RrlCCNvR&iNLXxYvZVZAnU-uQ@9~L5n3CC@d zGY@1Q%siZVB-5QrkIbW)$1=S#y)*rE`(Xa{WBU_sdq2N@xNaZGzu{~j<+cy^+h5l0 zllb>C+uwHEU-sMI*X?uo_deU_yY27$?TdB$hx}X2_Em2CV!!<}-TpQIK4bevETl3MtRz5 z&oE>?we^fWN9{RW7&cJT0Mih)}VFhQ}hq? z1^NRndOvKy(PIj*dphqT|sCs1~Y&{)SFR z_0T!!Jk${B<=mC%8ssk7^m6S+q?c(opxNH3@KGDM$)BUgUThX^@ zC)$N{rhc8FUuWjm8ToY)EeE2+M$l96Y7laMP1NCs0Vrs^+Em7WO-zerOOHf`+4~(KBcydLF%q#-W$dYv@h%Hkyv!N3+p9 zv;ZweOVM()5`Bc$qIGCJ+JH8qO=t_spsnZ|^ex(szDGOJPpBNvV>eU*RYV1-GAcqb z6h{e^L{-opC_pKcMth>Y&~5?82mJ-V52}jxMb%IR^7|7HKnJ3O(7_ezW;)l+46d6Q zSvNDOZe~v1%!hR|U)Rm-;&sOMiu|7=i8U(d=h3m|`*D2lpXpg&KmIBOCKT9EXu}x* zJ9kLE3g__wSD{qutErD^pr!%lS~b^VZcuXrrnQ>Zm|N7`f@!Cw9j1et4wyUD+=;nc z&E1%L)ZByVqNWSx0W}X`x~l1l>8_?b=210|VtT3Rh3T!PH>R(ezL*jBSV#jIDe9#dF|DJ(3NA~i*rDr%}=Qfg9| zz0~Z5sj8+bWOurk0vo zm^y0eU`{POPwEyDI606rGn2u^XVmhhmgz2oNGv+=u_hGuI>4JG! z&BK^(YPw;1sOf>}rKT6AL`?~1fSLiAL23qJhNu~W8LDO|W|*2`m}k^HgBhu2B<49a z&tXQX8HE|EW-MlcnhBVRY9?YPshNbCtY$K1ikc~ychtOtnXYC!W`>#>nD^DZkD0Az zHfFAxxtRHC=3^GAS%g`lW(j7Qnq`<3YF1!Yt67a%qh<|eotkx+^=j5*K3DTOX0w{j zn2eeXW~-X5m~CpdVRoq5f!V2MC*~(LKViyO#+0wj+e=LaOeHmyFokLgF-2;MFbOpY zOcga%FvV($F?*`n6SKFPy)jkQRK-+NQw?*VngcP1s5t~vT}^e&5o(UW9Hr(cObs

w83qvnyHxSYP8X8hMF0e*=n@mYpxn?+FGDS8?P3rp*4!OCyX{gEmyM~ zvr3INBCS!Q%|z?eXhYBjHMH!|j)U2V*`#I@W{VnakP$W7{E2`0Ui~=>`-6+BtJj6b4CPkxRx-;d>cU->bp$5mmbCq{Nl@jZKm8IwPy zq(Z7xGyWPgrvLi@^Zh`+>y_pO{P`&_hs5~f6VuDf-BX(1qx{8sXOEN=rzA*m#jkf$ zsb9N-J)Au<>07-D4YDn;N5{-LR}I@xt6tC3Y{1m_V9l60Cvza@XA0VYY2d*VV&WD;CuYw19LV{ff;M1Ud+@I@b57_$&JPu|0dtE7 zPm7szMh9~KsGtp)b{;$S9F>}uEK+gXa zv;ouKgRNrbn!tfvA1G)8W}pXej+twQobD1XBFv482=``f8#7lFZhu@|=>BZL4EA9A zn7QI`AXgs>+JG72!45HVMdCoNP875OGt`53#>^Fq1G#!p&<4zt9=tndu4o*{)s2ES zV4m_|mzcTYaUfSe3fh1f>A|isb4BDpu8tJ60rP?fd&SHZlLNVWQqTs>iyj;hGgnj& ze9?k70KVkGAu)5sUQD`*4eH4i=?<4zffSI`E`n;sm? z%6%aaub>T>DIR<|X0G_$Lr<}w4S-WUI4NeX2p!1Pq3+QJ%ybXF88cUm4&>@lK^rhL zJ@|IaTv0lZt4jrKz|8gF^q9HgbRbus3fh2K=)oB=b4BVvu1*!S0kgz|vt#Cp)qz~S zDrf^{r3dH5%oVKzxw=)*2FynuToN-^ybk2*S3w&vpLp=Yn7JZ$;3pQe0dRu{SI5j1 zvjaC+&<4Pb9$XhQSJV#V>RKP#2Fzv;ZityHZU=6*pbdaqJotIcT#-AFt8?9>4Va7v zx5Ug9y92p;SI`E`HV;b7T+us_t9u1)z*JynS=*<(EoQFx9mv(cf;M0ZJorP*vv?w@`)&&J^z*O=?rp3d7w0oBNtuG4NfLZUsW8pG=x~ z%#9`PO>?!JgORXU%44U+O>0mNM)Q!x_VU=7ann}hemdHXxM|9QRXx}sZrYM^ z_DAEA#rE@9E3yd#Z9wh4n{kr#jf&L@3?8S%)w~Iv{-YGmBdYxW)4Ohr^Q-$Y+&3pc;;ZV zfLg4T$DW9r=Fl9B_EC%7?6Ki-(^#5=(RymJwjLW1H%+NI7;UQ-Ywxk=4n|9}#d>*c zZrn6G=U}u$Tdc%m3*)9y+Pxb}4529t4)Ea8xM{S`+1~(%4f5E^xM|YP!3H^Oh{x8% zO@p_4SQ@{%Y05tAP!E02V#7SPDQ=p}-Tr7g=cXwOKI6em+%%`>?2q0HE0-|M_&FHu`gU&< zJhodxw2?mtBVn$Qo&>g@{Qx=@$!Ac34#Qq$JSI`E`WDgc5Ocz4V1JkEq z_cz63MG4cv;PyvngYM4;%sU=TBuocH&i?3;u=|_tu^?f(Cvq_QD=ap{V}DMV&Wjw3 z-VBSq@3E>0)3uR<(br+I*&f?JVLCo?u-Oiq>#>6qrduQjqo2g?ZNA42Pnb@V9E@HR zi!JilF$vS9;*Q)RLukr^OFURJVY*y$_D3I##g=*Oq=e~^$-$O6Y=y^8Nto`M9E|=O zySLRIJ1t>4b8;|xcPzHXV`nBzS5FQ`-;c%Cd8~fIbOgEQN~aJvO<8cg2QNsNj-s6X z(Q{<6&pmcg!gM3$VDu|lY_rEMO_)xm9E@Hki)B34G-0}+axnU&EVk8S%@U@ADhJ!@ zux%c@I$^r6ahEB4rZ3DXgngV8f+u{}NZ zP{MQ*=3sj|Y;TWsO_)x^9BglgRrOf+gy~|;!RT|e4_eJ*k0wk9WDZ7;q{R;OSnq`C zp3K4Mue8`99xF+h&dVH(-b{;C_gMdg>DtV}syplmk3F6+9iKTEJ)w4QM|tdtgy|N| z!RRNoSPhRonJ}HEIT*dD7CYW!!xN@UH3y@Q)nX@kY(&CzxaMFdIjpwFo=ceS*c`03 z!|Hf!RKj%D=3w;R+6O(=V`CGhD>nzDZ`Wd{d2D>bboAz6^!!@vbdSB9Fx|j8*y#?d z=do84rjs}aqu1E(?HrH2kuY7zIoLT4tM9S55~hPW2dnR}1|EAiVY;7lF#4nIgEsWo z`w7!IorBR^ZL!84`ygSuu5&Q@vMqLr#}*_^$94`zPq)RIcx-9HbbIGuO&oT)$5tdv zr+5x_xx<=yY)!&+nde~iq1y+&#$)Rfrb9gkqlevM%{{g;VY=IMu;vbH;jzsL(;1(G z(K~PV*3x5NCrnp;4%X6PZ9G<*Fdg|h7(M%TZ?}4Ed%|?{=V0{vTdbYOb|%aqfE+2B9M;2Q z2Pe&th#ZVD5_WGrJytzw22A8&jG(YsZ;u_FG{Y&}ITu4KxM|9QB_6DqG{Y@&_Q&`O ziw*GDNl7ypBL`!2hQ$VZtWMGl)o>3x*gmWcJpL0NJS}O4a^&ofu^o1Q!#s9+(hT&- z!5H~rv8O$DX3`7;$-$m>*a(lEoiu|)axg}X*u6dPvHD3fgv33G=MAAL3y${S1xYi+ zBxiq&IkDJ_9&4O515n)l7>vSAQx+WW!Hbh-fJ)B(7_nlpi5_c`G{aZi{us)_O;Z+p z-Gf);Yti(`|fXNrF*_Zhrp=fP; zu`OB~rbTPz9>|o>?3VeHEGgce(&Y;W$cM$JX98JOtY04%XVUUfab_=BQ!HzXOEvMA z%s!c_nSJF`!~LUJ`1(}2&z0L$ESup-IPPo1NwM(twQ^fS90|v5Gu*eu!q+zCz6)_A z9JjsrEZJTxJBs-Z^?h-siTmkDWU0)R6qRMJW<+1+x-&DaGdGu%@0aPBttZQrWFC?M zWmA%|=#IZ-hB#;#(C`p6#51Lt(GGeEXk-W)9fDrh5{iA^0S>B#~+L3r=M@x#g=kG<|}H_s+G@dtDD(D z^$Jy9yQ3JYg3@Rov@hBp9gGe`N1&t8v8WcRgZ_%@qWb83)CgUInxbat8q^$Jk8VV* z(JkmUbUW&ZI-$^+N;EVDtof5)DUBqY>y?^gJ4c#-Oq2B{Uwr zj3%N<=nXU(O+oLVspvg46McZ@qlIWOT8fsVmFOe%F3i>Omi_S!6p>t6K zbOCCNE<_ijOHosF1-c4di>^oQQAeZ`k=%{$MGvBfQ4iD;^+7s;h?_LzN&Hi21bQBg zMq|-<^a^?ny@B3B@1SXD2AYZ7WE~6fIw{A8Xf;}gGUyxh9r_;qh{{n>RRL8(l~Ek+ zfzs&Ds4CLQDh@)2p~KNps0ONuPCzH4Q_!jCG;{_!3!RJ3M~%@Xs0q3PU4^biEl^98 zuSzScN;`%svg7_qRa)&|u1e!C0F{AA(6>md(Hi{IYP7cX^#knEz=WnYTxo-;M*E%- z(2VA;#{j&aSEF54;XG+pK`yJnfbM){8iy!fnfAx4Ov?;m6bo+whO#h91;bDkMxkIB zioz%p3`0#AMS{5ylP^r;Amj_vI0)sn?uHJvd|?_*{rSQ)8X)t9X*)Ow|CYkE%xFr| z&WqNim2km!zP*BzKc{5xl%5Uk`guyL*Gl9ssf&>8lj1v9u~k#DZ%V4AWIqbm$nT$$ z15$EeitFryQuVlQY)h1rQhYbpys~#pYNwT zZq#SKQcXV^_Hx5v^Ob6T_5QY~^oYP+%i`ARkW zr#D}z7I}r_E7c+gBwwi(IUxB;HOkU_TbM0T)8aMzfs(IOucrh&Z zWrg`w)ATcfdAVH|OMcZfb3pQyYUXbTFOC1A<<(Bzvz)m-ctE&Pt!`?2@C)T#eRC_f zPS@dg_Ld1(Zc&s~^0T5WSBBMB&PdrZtTXL@*D|bnjIlc_rNve|C;yAJR$31gt}oKs zB3D;*cFNWc{hoC~u12T_wHjI@^v~*pv@Yn^YJz^L9_ZIG(Bt{_soZ{O&8}{6aB8Urt-J7$JKeQz~}NoMu`}zz4ZBOUae`onnHk zs8xu`)hW4#wCu?_w#?xl+M3#0_Oa@ z%J%i+=T-g#OqZOW1=;>i{Vd2=_`lDOrpy(J`}@q*3O7yJ|B;_A*&c@?*LRhlF4-QC z{B+5YKfL+rk|PHsKV5R5CH@o$`p zI6uR%eJ{**9}f8$hV22#&oCVM^YI@s!*G7$VEgL*BTO7zD`kgcOl=$s>5;vRnF=Z7E?(zhdd1-7=tT%RsgaDwfCX zmdump=4muK$S|J!`Z`MH$yb1Bbb zT-5@!7%fH1(Mt3YT8q}9^=Jdyh&G`uD1)}5Z_u}BJNh2&L_eW&jBnZvRY3o*W2)l3 zh7;(Q##ANAhsRVM#N!-{s*v9U6{7&9P#W!t_CkL~d!xUgeNa`jFRF(2L;Irx(1GY6 zbTB#;9fl4^N1&rn4OA1IfKEgwp_9=m=&$H+s4hAkor%suXQOk`dFTSv5H&^@qDxQ{ zbUA8ls1GVZ{n0@5I2w$eKts_mG#ovJov=3XMi%&{#AMjYkvEE9h19 z8k&UOKyRYA&=mAGdIwEK)6siq2AYLtqYuzLv;ZweOVM()0QQbR1wM z1{WQw!y^x6c;w+~Sni&c>@mz0#7gxTmaAtadQ45siE2*7)KXImQ%4QU#pmbHX6b5H zrRHd`G&KuRV_1BerKT|~FU?}o7#5Ibxo8YaMCa$wW+H(69NG;kGrznJL(R{j%|Xb| zq0K?q!As(Qa}Moa7^+I+PcJh|cDCuKR7%Xw?_7i*xU;Fn6;pAuMWc?q-`mO64shY+8bVudcLmAaTw$tS1el$4yBl4VKm{WV3}?3(4h{+5!{ z*fR;Y=ln$G`H9TkpsoBw=K7)6w}KlNdwncFk$HY1b6yfJ`Ikg~B6AK$ej;=IoXAgP zo}b7(Kasg!8OYiK_a`$yk+~j^fA>V@mHe>$b5f?k(4FDYc&I<04VXg<-7oH%xM@&y zAdQO(+JHISgDv8w!O?*+^4Rz| zcOn=Gi*@$c#JF^hVk9hfpU2*a3#F5}zpOt}EoF&+rVHaz!ZUU6SUfjP*+cUC7F`bU z4@=SIkkG^8^;#7;`djSkSU@4;dOsDVDhD*dOVoU#T*yRB7fwLNpPp^H-T5s z2FxFzhIE~OuJseLF7#aS`dn?mutc>P`2fccGet`q%1ZjzLVq5a`?e>Pz91+jWOC^d2~ z_IyFCJs|ml*vQ|)|A>Ov{6yyF)yo_Dzh)xyOhu*+?;*`or)L66f%L1n4sE+GmDx+K zPe}`gwvli{+io!29sw0Ya)WX=hBy+AYiYRRfT|*Csa&fNN5XNf4HpDd9!YEEZVGWE z9M{HhsetMvX`|fDA&!LOZZTXspn^$mQSR0dN5XM!4Yy}Nm6Nnp?zRv|!g1{kw^u+V zl(bW>eTXCBxZ4f)=YVP{xm~$CLL3RlbuirC0Tor!LAj2~Wrq-waNOSw_m_aGEBU)} zcZN6;j_YK&eF7@8q?2-Yg*Xz9yW4P81FE;=Zsj_MI1-M#$8h@wRCvif%H12{NI333 z!&M8Y0+aicyFbK{a9kI|?H5ofCS8lYz<&3UMSH_qgGX3aH4F$CVo#;z&4dh~a7kRPD(S<(>#}Bpf%?a5V!e`(&tc zPlh-WjvHpU69TILWSDZpLmUamJ!QBP11bdNDdnD4F8lr<;kaiEcTzwVp**A9N5y3v z3CE2v+{pozhB89Ak)i#OaNM(oJ0+kRQJz)qIpv;D$qT&xNH}hk;r<#>u_&XI8?D@n zDH#Jt!f|5__qTwmMj5NzOCgSg+35Gj8pgK|}DED%RBjLDL z40mQg1*N>A+{6$^!f~$}?yP_+OL1FALU z4dvbpaU>i!*>L9tRCLN@<=#?mN=n}5_>pkjJBGU;pz2fJQSRLkN5XMa4c9QBGE}B2 zH!Z}GaNKmmH4dmAmFdd87ve}bZieA545%=b8OpsM;z&4drr|CLs6v&Q%FPOKBpf%} za7_X#Rb{qvbCk=zo=G_F1H)Y&P|YeIC^t96k#O8R!!--2c$Im|%@1)T9Jj!5*9TO^ z$^zvUDz_*li#g6D9Jj=9EdnZMWr=c2LmUamEi>E={Lv3&nR3fR90|vLXtvRb*1LL3Rltufrq0oA^;M!B^ij)dbr zHry?o90c;Qa_f})Bqi%PE+icHso`!7gs)GP+YsVNIPNpUwdEutkk6F+M~EZgxQ&Lp zEui{XHY)ddh$G>+FAUd?lZ`;WP;OI*BjLEshHD>C#VnhZ`%<~=>xzWqwixb?fJ$fC zqTE*@j)ddBHe3fzN&@*>xlD*7;W#ne-#IA>M3mdATxm+a;rNko+&07A8BkR%+m!n@ zv_BG#`_6E81yo+kcgk%KaU>kK!*F+V@)F1n<-QMbBpmmH;W`IYaLW(M?Nsi^l>Efc zN5XNt40mrpmAC9tu3Wl|BjLF6X+1B#-+Yx%!|fL0NI33KhU>yfP9T3$u0n_-;kb&1 zdytcyKq@M?dx#_9xJrh5IH2lWDk)bG;z&5I&~V*2849FOxym7qgyV_~_eenXx)doF z3vnbI7dKpwfC_hsE0+jyBpjDCTu)A#0!b=YCB%_%+#ZJO#Yq#F49XQNmwla*a9m)x z-T~G05-68aE}fP=)1{hl++K$3!^st=m<^B@lNH}gE!}aIn zic@Iis)jfcj@#F8139_k)L6M{A&!LO_A}h$oLq6rtK9w}j)db5Fx=pPSpwt$+V+=QfbG1N@ zQLaXaBjLDX4fibPYJnW9+;Jg}gyU)&?)iX;7Nn+f$A>r)jyu6{qd2GI3{|-kLmUam zon*Mt0TVXJNy^mjfvf;)AOzI#fD_1+jk#Jlc!;R$}F_1dSouXX!?LoqEry6b? z=ZJxvs@z`sDOi{UWLL3Rlool$)0w%PObCs(f;z&5|Ji|@moRc#}jrX#a?L^<3CCS&xapkh26ClxSA{qdj=S1$?*&X8B3CPSO^74mxN8kJgLC3Qu2rsi zh$G>+>kKz5U;+}kPPywt90|v@Fx+g;i8=FB?gr&{txz-_$ zgyY&6ZXV~vG!H3vONb-kxZ4c3fOFzNZd0y(h$G>+4u)GCFlmZ(Q10&`j)ddxGThRD ziB;q-k{HfIPM|Ctq7Q)MIKVFYltJ^xJL}Pl5=PNqAT}k zh$G>+#|*ccbLT)FQ?7T2BjLCb!+jJm8H|)D*I&86+0PdejvHvWHJoGfmr%LEA&!LO zo-o|HfJtTK3FU@`I1-L~%5dugCZ3U}lzS$`k#O9zhWnIraX#{udp^XGaNH=vea5*s zALYuu7~)7c?j^%*@luj&DhcXYt- z>)1e!4@%is_IQS?!ylJGYAJVWh$G>CuG0*627e3!IZe5GA&!LO&M{p5K=?XGx${FD z3CA@u+(mpi2GU5miaNKoHu2Zg6 zh$G>+Hio;MHdsCflgq}-Dsj)dc$Hr&WS_a1#UJ>t*H4^UZQ39QV56Cewx!$m`0P=bPn7 zIPP7;O$&stca<~GH_MT5-1~-`Nn1)F?<;4XZl$)=|WnmzTc)lbY zx5RMEXse)EL%9z_90|v*GTdba!q+P0#_z%Y*3ve>{zy3P6T@u?gs)GO`-ko?%aL&0 zCc|y!nmmwA%54ecE3T*m$*@B*4%}+MZvx?KtAgK#K{5{fE;viR3uHT&$BOL;m~kNc zxFoVvhBadQ%J;#A@`GJ{R*ontDc?tGRM79bx>`Tk8Jrj&vrxsX)s{|SWKaEWezwqF z$+#bm#c6+h{C_fQJCMRp~KP9=mb;? z)j_ADP3(IM%Am`+zm2F%An!oB4Dzewb`k$?Q-XHc-7b~E*A8{~OCM-(KmrO&2;6ZVz75=dy(I3;17JlDR(H z&O;5*KCo5MzNi|yfSgVrx`F?@5q*gc;QoQ=d;Bh>(}^C*G?8`C8K^$G2wjbCM7N`R z&_n1^)DI0oBhgD}BASe*p_yndT81tw;BkpM4P`@qjz&nQqP)34|Gw#TJ3n4ho=GMf zPz$T0;Es-Qr)(4)o|~r3P3n8FW{f*!AYMTmFbzC-LX10QAYMTmFc11-?S~4u6B+wM z!rbIBkM%C#P6Q)iu@aBpAWwn0 z@tk;WnzG=t9(=w)o{jF0gvHwVV|PYO+J%lCo|~pDIL_~HLV=8n?vI4UUh&wg1@cN1 zBVn-){$bCGNr%wG;<;(cf>Zqd-YJkN(fyII*ffvLD3EDUjD*EzdTdsK%#30rEH=ku z9~8)(C`Q6!^E@`cK;}g;5*AzFu|);4Ac~Q&*kX?@E0D!ejD*EL^w`P*`7nx+u-F=p ztt*fL$QXpSNF%lO0#$(?V$Tv}pgvGx1 z*pCJBeH0^MvGTsEtYV>*FO0lfNmwl5u{{bU5yePYEb!Q03MGhQBrJBA$BrqK!=e}o zi`DknX@ycdijlBb1CL!)C=H?*35#9ru^S8J>L^CSVr@Luu29-UF%lNL(_{A*%AHY+ zgvEM#tY4w@jAA4#Hr!(`7RvA_M#5q*dFG zaR-$pUODpmC1J4=|A~1+OiDtZn0RiQvfy!kf3+&hanb#euvlG>on2Y#Mlli=Yvi$u zDodj%M#5s3dF<-Sa#<83VX;;oyREXciee-zcDKjwuPk>*F%lMg&|}>z%Y#vjgvADW z?8(Y9Fp80|*b5#TTUlO+Vk9g!!DFvhmI+aegvH+U*xQxmO)&Q=!E@7;1!sEjgUT{9 z1mYF60khD9ODl6H@;V`5_qW1ht1HWjC`Q6!Yd!W!Wmy}=NLcJMk9|>DK8s=`EcUg> zwpNy}!Q8XObJLUszxCjcmF3&${zzD?T#-9RtW+fBiXzXIgvAmb+oMPlQH+Ges(I|t zBB>U|NLcI`j~!nm$3!s_7CYHvb&BNVC`Q6!4Lo*1ku->6BrMj*V;2@lqbNqgVi$Yt z(jvJyijlBbQ;%IyBu%3j35#9nv8#*Z$|y#{V%K`?x+1wYijlC`4IXP*BsWAc5*BOi zv73sdbrd6Eu{IvNrAXRDF%lNL&13D0E;-BBJWoc<|czY);7kS zvi;$8e>PwSd$4_sJ7pkVK^rhbJlG+|oiY%wpbeOz9=tQgoiY%wpbeNOJ$QGFJ7pkV zK^rhnd9X{2J7pkVK^rh5J=itIoiY%wpbeN8JlHG7oiY%wpbeN8Jvbo7oiY%wpbeN= z{ujvXjkBV!91`Xxb3FERj60EE9|?=i_1N<#>QlR6eD4=g&tc}BnzV$ z35zZE*vm0l9K}djY^ld4#bjv|BVn=S9(yw;%cB?xi>>h3$|6}2#Yk9emB-$W$*L$u z!eSqJY)z4T6vaqbY^}#WE|RrTjD*G3dF+!SSr^4fSZuw=rpIJ`6eD4=h5n^9BPI(& zFC{!TO<8b>2WQ7*NeIL%XanYR|G-}qaVPR(B4PKp$zz*~WK$F)VX-egHZLY$Mlli= z`^sZq7s*#qjD*E99+M);L@^Q;+v>3;G1(f$NLcI}k9`=EZ=x6pi+$^{)iL=tijlC` zc8~2SlI>B9gvGx1*t(c}AH_&mY^TS5ERvm3jD*F0^4P8-`6-H#u-In*%ij=_&7ogD zo|~pDxW$8?$7D+g#4Bh6rb4ViwiR$oj60E62?@Ku-908T**%Jpuvme|w#B3%ijlBb zWsm(3lgd$ygvDYW+ZB^o6eD4=gvXLGNklOc7OUd1Jz`QNijlBbvBy#|DUMfviC`Q6!$9Zh`xEvS7NLZ}4$4-q&?I=dVVs$-M6qmYDjD*F`^;j}4=SDFS7Hj0O zi(=9!ijlC`WgfdCCYOP^lPx?qO_xKxQ^BrKNl*lBS|MKKZ<8{>bvGvhKQ`XoqLY;V7} zbK6|@0!sej>ik8>yTX+y%?q^ZYx#JLl}NLZ|y z#~zCdlbhyFpqZcTG;%=NyW^Ih?bN=0y14_=!@rPb$E8Q~g+#*gy*xHIF1?}{35)&z zKHKSS{@vO>F1PV+y(Rm1MSpxv*r$=76xALGU574vNGA9fz-|ed5PboVutzRGDQfry zHNzd2_x;hUnvnOSM~{R(db2&Ye?n$QF%lM=>#>6qGB=8mu-JT$9iGrRP;+1GBrG<| zzbEFzWmf1f7@nJ^ESR4Z)gB0)GBtZhKKFTpsPmCt=@W z%{;ayA{I@IXHLDuKj>vixg`3aB8kd)EUy^*lkP`@{3O&uED8wrd3#yL|b_`NY_>V)XtNZ7sQXH2!n zAwOekm6U=g zM#5r69vhRCq9{hfVo8sUOG+||k+4|cv6qt)L@^Q;OM7f$Qm04Fy(s?o&X-!x{}#@w zBK7z!xE?<|H%-|mQQbd@cal;)`XoqL>;Ni?c3vSjr5i~D_I7hN-kObVv$0(^-kyzj zWMju{yfYg+W#ir1cuzLon~nEp;{)0FU^YISjoq@bdp7pS#-7>OD;xV{W8ZA-myH9m zaZolso{dAYacDL^nT^A<@#$@wse#Asa_$eOC^lW@D8)szW``I`%8)s$X>};HqjUQy=+-#he zjq|f{K{hVT#zon}aaA_1&c=_jaZNU^&Bl+jaa}fk zl8x)L@zZSFkd2M2*w^o8+55*)4n9Esk*-hW8ZYZ~sy;7w*~opu=Qey{Lgwm{@+Fz; z&djuyE2^AXDpQjsf`%v0G5W zvy3gvSp1Qv^5d0S^LL&p?4xE6qqTeu)HJ{}s&JlMSb>mfE0^J$RUl+~%BJ*rvYCnX zn2bkj^`Nz2?=dx6l2@Wei|z)g(XzT>YP4|fDK%Oe_pBN%eQT!=rp0aJG^539uc*=T zv<|vS3(%%$MhncQsnHU$nQF91Y>pZ&1DmHt3%wSo(NeC(YP4AELp55CwMLB=Ty0RJ zB~@Rj(W0rZ)M#1MH)^!->3cO=+El&}qs2=JH3>|hMhlP*Q==tDwbj(dG*Ht3bF~^R z1ZtzE4dzZYcVc>~>4_PxhWZ<-Z7?rkURCodW||r;n^~$x3u88@(bAVM)lkVo-3o?U z6)ICOTAos(M@$P)j?;`5nABCHB_xg1P!mE02u905TB&J;xm(TMmirMhCsO zR?`~OMok;cZE9}A+@au zHH^+=EGCBWmyEW==#b0pYPMs(SEIuwcdB6=B%>ZNI`na~J}4s@8Kj8O!HX4Q7#*Cr zyBZySSfEA+7*|jSd5>qDBY$6|2#qeEX}>!FtE3Ve}ni?JzpvuC5v# zQg^Nz9W>WS4Wr-~;D%w88w1!dIOzW$e1;XF*PwKsyPu;OHD0I9W^W@EN2&}1(*n~^1h%DwlOcym>Fb}JF7}HG+10EROfYC7yz0~x=l&C4e3{W!wGf2%K%n&uYa_VjR zymYLRqZs+ekOs$mXPb0FprHHToTtErATLd_AFqtqOQsiCF@=6E%9&d5n> zPQuhyQyWu9O&!dsYEH$Rrsg!v>1s~L)Kf!OhMc43987&R^)U_9G{7`e(-6~GO=HX@ zYA(SvQPTu-xf*TjZ>EOEdz$Jo+FsvW4Gr)#w_~);y`>r&(WQ-=Hke!0+=^+ZrXA)E zHFsb-s_BU7q^1+5vzpGBd)3^Fc|gqrn67HNV!Em6hUuZE2d1Z*o|xK6jJDX8Xr_eB z05t+C79)E zmSa|_S%q1nhDIq_r)C{ygPIMPjcPVxHmTW!*`j6(M$`yqn;M#MWV@Q}m>p_%V17{Z z17??+U6`Y*V6^q5qW;O$_K^ZL1(+f=G-=R)fzeisK#jIjq}6Ck#9nIl!tA4FAI!dL z_QllGJ!^}=enscWAw`7DX!(*WfGtd!oz9#u8>*Zq|ENNcja6t7fTk57^RdpS^;P;@ zE{r(yOKT|2?8b*dnrD~J+{~3srtR4#U7ER{3zhB_?H%RXdRv<5>7Wvz0U@Yo8N_u+ zmXfV$uu}VY3a}}})vVyS!4=_#lbDdTgARB!`Kw!j$|UTz8pjTuRExG3ht3@YiUe-3r#JJk=#nS zH7#ukZPU_@&@L^v6K-c^6~Z0NU`Oc4x+sJ@)6$91DJ^#s?oP`+gnL-Kgm5oQlMwD_ zw3}+n+!qaIPK^Va*6oip1GC_DQEiVvW zU?~a0=(LO>j7iH_!q~KoBaCBSI>LBXeIUHdq7HPNI$=5sA`sq7%M8K{mOUW6pO%?~nJjKV zn3a~mQM+vGJ!u~Ls}YFX^iAE?tPY)W6cD42h!X}YsUqpnd{^b cPV)|p@hZNe$_1r-lPY^;6V;&l+<*W70UcGl?*IS* literal 394265 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 ON?EjxOs?3S^Zp-yLvi~6 delta 9007 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 X9ZE5C;+y*73*8eN-#_z2L<;`}n$oK3 diff --git a/Plugins/Drivers/DriverModbusTCP/DriverModbusTCP.csproj b/Plugins/Drivers/DriverModbusTCP/DriverModbusTCP.csproj index 5c7c7f5..8891b66 100644 --- a/Plugins/Drivers/DriverModbusTCP/DriverModbusTCP.csproj +++ b/Plugins/Drivers/DriverModbusTCP/DriverModbusTCP.csproj @@ -5,15 +5,19 @@ + false ../../../IoTGateway/bin/Debug/net5.0/drivers - - - - + + + System.IO.Ports.dll + true + + + diff --git a/Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs b/Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs index be7a0f9..629d452 100644 --- a/Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs +++ b/Plugins/Drivers/DriverModbusTCP/ModbusTCP.cs @@ -1,7 +1,9 @@ using Modbus.Device; +using Modbus.Serial; using PluginInterface; using System; using System.Collections.Generic; +using System.IO.Ports; using System.Net; using System.Net.Sockets; @@ -9,12 +11,16 @@ namespace DriverModbusTCP { [DriverSupported("ModbusTCP")] [DriverSupported("ModbusUDP")] - [DriverInfoAttribute("ModbusTCP", "V1.0.0", "Copyright WHD© 2021-12-19")] + [DriverSupported("ModbusRtu")] + [DriverSupported("ModbusAscii")] + [DriverInfoAttribute("ModbusMaster", "V1.0.0", "Copyright WHD© 2021-12-19")] public class ModbusTCP : IDriver { - private TcpClient client = null; - private ModbusIpMaster master = null; - + private TcpClient clientTcp = null; + private UdpClient clientUdp = null; + private SerialPort port = null; + private ModbusMaster master = null; + private SerialPortAdapter adapter = null; #region 配置参数 [ConfigParameter("设备Id")] @@ -23,14 +29,32 @@ namespace DriverModbusTCP [ConfigParameter("PLC类型")] public PLC_TYPE PLCType { get; set; } = PLC_TYPE.S71200; + [ConfigParameter("主站类型")] + public Master_TYPE Master_TYPE { get; set; } = Master_TYPE.Tcp; + [ConfigParameter("IP地址")] public string IpAddress { get; set; } = "127.0.0.1"; [ConfigParameter("端口号")] public int Port { get; set; } = 502; - [ConfigParameter("站号")] - public byte SlaveId { get; set; } = 1; + [ConfigParameter("串口名")] + public string PortName { get; set; } = "COM1"; + + [ConfigParameter("波特率")] + public int BaudRate { get; set; } = 9600; + + [ConfigParameter("数据位")] + public int DataBits { get; set; } = 8; + + [ConfigParameter("校验位")] + public Parity Parity { get; set; } = Parity.None; + + [ConfigParameter("停止位")] + public StopBits StopBits { get; set; } = StopBits.One; + + [ConfigParameter("从站号")] + public byte SlaveAddress { get; set; } = 1; [ConfigParameter("超时时间ms")] public uint Timeout { get; set; } = 3000; @@ -50,7 +74,7 @@ namespace DriverModbusTCP { get { - return client != null && master != null && client.Connected; + return clientTcp != null && master != null && clientTcp.Connected; } } @@ -58,8 +82,47 @@ namespace DriverModbusTCP { try { - client = new TcpClient(IpAddress.ToString(), Port); - master = ModbusIpMaster.CreateIp(client); + switch (Master_TYPE) + { + case Master_TYPE.Tcp: + clientTcp = new TcpClient(IpAddress.ToString(), Port); + master = ModbusIpMaster.CreateIp(clientTcp); + break; + case Master_TYPE.Udp: + clientUdp = new UdpClient(IpAddress.ToString(), Port); + master = ModbusIpMaster.CreateIp(clientUdp); + break; + case Master_TYPE.Rtu: + port = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits); + port.Open(); + adapter = new SerialPortAdapter(port); + master = ModbusSerialMaster.CreateRtu(adapter); + break; + case Master_TYPE.RtuOnTcp: + clientTcp = new TcpClient(IpAddress.ToString(), Port); + master = ModbusSerialMaster.CreateRtu(clientTcp); + break; + case Master_TYPE.RtuOnUdp: + clientUdp = new UdpClient(IpAddress.ToString(), Port); + master = ModbusSerialMaster.CreateRtu(clientUdp); + break; + case Master_TYPE.Ascii: + port = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits); + port.Open(); + adapter = new SerialPortAdapter(port); + master = ModbusSerialMaster.CreateAscii(adapter); + break; + case Master_TYPE.AsciiOnTcp: + clientTcp = new TcpClient(IpAddress.ToString(), Port); + master = ModbusSerialMaster.CreateAscii(clientTcp); + break; + case Master_TYPE.AsciiOnUdp: + clientUdp = new UdpClient(IpAddress.ToString(), Port); + master = ModbusSerialMaster.CreateAscii(clientUdp); + break; + default: + break; + } } catch (Exception) { @@ -72,7 +135,9 @@ namespace DriverModbusTCP { try { - client?.Close(); + clientTcp?.Close(); + clientUdp?.Close(); + port?.Close(); return !IsConnected; } catch (Exception) @@ -86,7 +151,9 @@ namespace DriverModbusTCP { try { - client?.Dispose(); + clientTcp?.Dispose(); + clientUdp?.Dispose(); + port?.Dispose(); master?.Dispose(); } catch (Exception) @@ -158,7 +225,7 @@ namespace DriverModbusTCP private DriverReturnValueModel ReadRegistersBuffers(byte FunCode, DriverAddressIoArgModel ioarg) { DriverReturnValueModel ret = new() { StatusType = VaribaleStatusTypeEnum.Good }; - if (!client.Connected) + if (!clientTcp.Connected) ret.StatusType = VaribaleStatusTypeEnum.Bad; else { @@ -172,9 +239,9 @@ namespace DriverModbusTCP { var rawBuffers = new ushort[] { }; if (FunCode == 3) - rawBuffers = master.ReadHoldingRegisters(SlaveId, startAddress, count); + rawBuffers = master.ReadHoldingRegisters(SlaveAddress, startAddress, count); else if (FunCode == 4) - rawBuffers = master.ReadHoldingRegisters(SlaveId, startAddress, count); + rawBuffers = master.ReadHoldingRegisters(SlaveAddress, startAddress, count); var retBuffers = ChangeBuffersOrder(rawBuffers, ioarg.ValueType); if (ioarg.ValueType.ToString().Contains("Uint16")) @@ -280,4 +347,16 @@ namespace DriverModbusTCP S71200 = 3, S71500 = 4, } + + public enum Master_TYPE + { + Tcp = 0, + Udp = 1, + Rtu = 2, + RtuOnTcp = 3, + RtuOnUdp = 4, + Ascii = 5, + AsciiOnTcp = 6, + AsciiOnUdp = 7, + } } diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStore.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStore.cs new file mode 100644 index 0000000..d038929 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStore.cs @@ -0,0 +1,176 @@ +namespace Modbus.Data +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + + using Unme.Common; + + ///

+ /// Object simulation of device memory map. + /// The underlying collections are thread safe when using the ModbusMaster API to read/write values. + /// You can use the SyncRoot property to synchronize direct access to the DataStore collections. + /// + public class DataStore + { + private readonly object _syncRoot = new object(); + + /// + /// Initializes a new instance of the class. + /// + public DataStore() + { + CoilDiscretes = new ModbusDataCollection { ModbusDataType = ModbusDataType.Coil }; + InputDiscretes = new ModbusDataCollection { ModbusDataType = ModbusDataType.Input }; + HoldingRegisters = new ModbusDataCollection { ModbusDataType = ModbusDataType.HoldingRegister }; + InputRegisters = new ModbusDataCollection { ModbusDataType = ModbusDataType.InputRegister }; + } + + /// + /// Initializes a new instance of the class. + /// + /// List of discrete coil values. + /// List of discrete input values + /// List of holding register values. + /// List of input register values. + internal DataStore( + IList coilDiscretes, + IList inputDiscretes, + IList holdingRegisters, + IList inputRegisters) + { + CoilDiscretes = new ModbusDataCollection(coilDiscretes) { ModbusDataType = ModbusDataType.Coil }; + InputDiscretes = new ModbusDataCollection(inputDiscretes) { ModbusDataType = ModbusDataType.Input }; + HoldingRegisters = new ModbusDataCollection(holdingRegisters) { ModbusDataType = ModbusDataType.HoldingRegister }; + InputRegisters = new ModbusDataCollection(inputRegisters) { ModbusDataType = ModbusDataType.InputRegister }; + } + + /// + /// Occurs when the DataStore is written to via a Modbus command. + /// + public event EventHandler DataStoreWrittenTo; + + /// + /// Occurs when the DataStore is read from via a Modbus command. + /// + public event EventHandler DataStoreReadFrom; + + /// + /// Gets the discrete coils. + /// + public ModbusDataCollection CoilDiscretes { get; } + + /// + /// Gets the discrete inputs. + /// + public ModbusDataCollection InputDiscretes { get; } + + /// + /// Gets the holding registers. + /// + public ModbusDataCollection HoldingRegisters { get; } + + /// + /// Gets the input registers. + /// + public ModbusDataCollection InputRegisters { get; } + + /// + /// An object that can be used to synchronize direct access to the DataStore collections. + /// + public object SyncRoot + { + get { return _syncRoot; } + } + + /// + /// Retrieves subset of data from collection. + /// + /// The collection type. + /// The type of elements in the collection. + internal static T ReadData( + DataStore dataStore, + ModbusDataCollection dataSource, + ushort startAddress, + ushort count, + object syncRoot) + where T : Collection, new() + { + DataStoreEventArgs dataStoreEventArgs; + int startIndex = startAddress + 1; + + if (startIndex < 0 || dataSource.Count < startIndex + count) + { + throw new InvalidModbusRequestException(Modbus.IllegalDataAddress); + } + + U[] dataToRetrieve; + lock (syncRoot) + { + dataToRetrieve = dataSource.Slice(startIndex, count).ToArray(); + } + + T result = new T(); + for (int i = 0; i < count; i++) + { + result.Add(dataToRetrieve[i]); + } + + dataStoreEventArgs = DataStoreEventArgs.CreateDataStoreEventArgs(startAddress, dataSource.ModbusDataType, result); + dataStore.DataStoreReadFrom?.Invoke(dataStore, dataStoreEventArgs); + return result; + } + + /// + /// Write data to data store. + /// + /// The type of the data. + internal static void WriteData( + DataStore dataStore, + IEnumerable items, + ModbusDataCollection destination, + ushort startAddress, + object syncRoot) + { + DataStoreEventArgs dataStoreEventArgs; + int startIndex = startAddress + 1; + + if (startIndex < 0 || destination.Count < startIndex + items.Count()) + { + throw new InvalidModbusRequestException(Modbus.IllegalDataAddress); + } + + lock (syncRoot) + { + Update(items, destination, startIndex); + } + + dataStoreEventArgs = DataStoreEventArgs.CreateDataStoreEventArgs( + startAddress, + destination.ModbusDataType, + items); + + dataStore.DataStoreWrittenTo?.Invoke(dataStore, dataStoreEventArgs); + } + + /// + /// Updates subset of values in a collection. + /// + internal static void Update(IEnumerable items, IList destination, int startIndex) + { + if (startIndex < 0 || destination.Count < startIndex + items.Count()) + { + throw new InvalidModbusRequestException(Modbus.IllegalDataAddress); + } + + int index = startIndex; + + foreach (T item in items) + { + destination[index] = item; + ++index; + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreEventArgs.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreEventArgs.cs new file mode 100644 index 0000000..bed0b04 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreEventArgs.cs @@ -0,0 +1,73 @@ +namespace Modbus.Data +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + + using Utility; + + /// + /// Event args for read write actions performed on the DataStore. + /// + public class DataStoreEventArgs : EventArgs + { + private DataStoreEventArgs(ushort startAddress, ModbusDataType modbusDataType) + { + StartAddress = startAddress; + ModbusDataType = modbusDataType; + } + + /// + /// Type of Modbus data (e.g. Holding register). + /// + public ModbusDataType ModbusDataType { get; } + + /// + /// Start address of data. + /// + public ushort StartAddress { get; } + + /// + /// Data that was read or written. + /// + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] + public DiscriminatedUnion, ReadOnlyCollection> Data { get; private set; } + + internal static DataStoreEventArgs CreateDataStoreEventArgs(ushort startAddress, ModbusDataType modbusDataType, IEnumerable data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + DataStoreEventArgs eventArgs; + + if (typeof(T) == typeof(bool)) + { + var a = new ReadOnlyCollection(data.Cast().ToArray()); + + eventArgs = new DataStoreEventArgs(startAddress, modbusDataType) + { + Data = DiscriminatedUnion, ReadOnlyCollection>.CreateA(a) + }; + } + else if (typeof(T) == typeof(ushort)) + { + var b = new ReadOnlyCollection(data.Cast().ToArray()); + + eventArgs = new DataStoreEventArgs(startAddress, modbusDataType) + { + Data = DiscriminatedUnion, ReadOnlyCollection>.CreateB(b) + }; + } + else + { + throw new ArgumentException("Generic type T should be of type bool or ushort"); + } + + return eventArgs; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreFactory.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreFactory.cs new file mode 100644 index 0000000..c9f0a00 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DataStoreFactory.cs @@ -0,0 +1,53 @@ +namespace Modbus.Data +{ + /// + /// Data story factory. + /// + public static class DataStoreFactory + { + /// + /// Factory method for default data store - register values set to 0 and discrete values set to false. + /// + public static DataStore CreateDefaultDataStore() + { + return CreateDefaultDataStore(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue, ushort.MaxValue); + } + + /// + /// Factory method for default data store - register values set to 0 and discrete values set to false. + /// + /// Number of discrete coils. + /// Number of discrete inputs. + /// Number of holding registers. + /// Number of input registers. + /// New instance of Data store with defined inputs/outputs. + public static DataStore CreateDefaultDataStore(ushort coilsCount, ushort inputsCount, ushort holdingRegistersCount, ushort inputRegistersCount) + { + var coils = new bool[coilsCount]; + var inputs = new bool[inputsCount]; + var holdingRegs = new ushort[holdingRegistersCount]; + var inputRegs = new ushort[inputRegistersCount]; + + return new DataStore(coils, inputs, holdingRegs, inputRegs); + } + + /// + /// Factory method for test data store. + /// + internal static DataStore CreateTestDataStore() + { + DataStore dataStore = new DataStore(); + + for (int i = 1; i < 3000; i++) + { + bool value = i % 2 > 0; + dataStore.CoilDiscretes.Add(value); + dataStore.InputDiscretes.Add(!value); + dataStore.HoldingRegisters.Add((ushort)i); + dataStore.InputRegisters.Add((ushort)(i * 10)); + } + + return dataStore; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DiscreteCollection.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DiscreteCollection.cs new file mode 100644 index 0000000..3f8780b --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/DiscreteCollection.cs @@ -0,0 +1,124 @@ +namespace Modbus.Data +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Linq; + + /// + /// Collection of discrete values. + /// + public class DiscreteCollection : Collection, IModbusMessageDataCollection + { + /// + /// Number of bits per byte. + /// + private const int BitsPerByte = 8; + private readonly List _discretes; + + /// + /// Initializes a new instance of the class. + /// + public DiscreteCollection() + : this(new List()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Array for discrete collection. + public DiscreteCollection(params bool[] bits) + : this((IList)bits) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Array for discrete collection. + public DiscreteCollection(params byte[] bytes) + : this() + { + if (bytes == null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + _discretes.Capacity = bytes.Length * BitsPerByte; + + foreach (byte b in bytes) + { + _discretes.Add((b & 1) == 1); + _discretes.Add((b & 2) == 2); + _discretes.Add((b & 4) == 4); + _discretes.Add((b & 8) == 8); + _discretes.Add((b & 16) == 16); + _discretes.Add((b & 32) == 32); + _discretes.Add((b & 64) == 64); + _discretes.Add((b & 128) == 128); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// List for discrete collection. + public DiscreteCollection(IList bits) + : this(new List(bits)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// List for discrete collection. + internal DiscreteCollection(List bits) + : base(bits) + { + Debug.Assert(bits != null, "Discrete bits is null."); + _discretes = bits; + } + + /// + /// Gets the network bytes. + /// + public byte[] NetworkBytes + { + get + { + byte[] bytes = new byte[ByteCount]; + + for (int index = 0; index < _discretes.Count; index++) + { + if (_discretes[index]) + { + bytes[index / BitsPerByte] |= (byte)(1 << (index % BitsPerByte)); + } + } + + return bytes; + } + } + + /// + /// Gets the byte count. + /// + public byte ByteCount + { + get { return (byte)((Count + 7) / 8); } + } + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return string.Concat("{", string.Join(", ", this.Select(discrete => discrete ? "1" : "0").ToArray()), "}"); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/IModbusMessageDataCollection.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/IModbusMessageDataCollection.cs new file mode 100644 index 0000000..bb815ff --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/IModbusMessageDataCollection.cs @@ -0,0 +1,22 @@ +namespace Modbus.Data +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// Modbus message containing data. + /// + [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public interface IModbusMessageDataCollection + { + /// + /// Gets the network bytes. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + byte[] NetworkBytes { get; } + + /// + /// Gets the byte count. + /// + byte ByteCount { get; } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataCollection.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataCollection.cs new file mode 100644 index 0000000..0a5e4aa --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataCollection.cs @@ -0,0 +1,128 @@ +namespace Modbus.Data +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + + /// + /// A 1 origin collection represetative of the Modbus Data Model. + /// + public class ModbusDataCollection : Collection + { + private bool _allowZeroElement = true; + + /// + /// Initializes a new instance of the class. + /// + public ModbusDataCollection() + { + AddDefault(this); + _allowZeroElement = false; + } + + /// + /// Initializes a new instance of the class. + /// + /// The data. + public ModbusDataCollection(params TData[] data) + : this((IList)data) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The data. + public ModbusDataCollection(IList data) + : base(AddDefault(data.IsReadOnly ? new List(data) : data)) + { + _allowZeroElement = false; + } + + internal ModbusDataType ModbusDataType { get; set; } + + /// + /// Inserts an element into the at the specified + /// index. + /// + /// The zero-based index at which item should be inserted. + /// The object to insert. The value can be null for reference types. + /// + /// index is less than zero.-or-index is greater than + /// . + /// + protected override void InsertItem(int index, TData item) + { + if (!_allowZeroElement && index == 0) + { + throw new ArgumentOutOfRangeException( + nameof(index), + "0 is not a valid address for a Modbus data collection."); + } + + base.InsertItem(index, item); + } + + /// + /// Replaces the element at the specified index. + /// + /// The zero-based index of the element to replace. + /// The new value for the element at the specified index. The value can be null for reference types. + /// + /// index is less than zero.-or-index is greater than + /// . + /// + protected override void SetItem(int index, TData item) + { + if (index == 0) + { + throw new ArgumentOutOfRangeException( + nameof(index), + "0 is not a valid address for a Modbus data collection."); + } + + base.SetItem(index, item); + } + + /// + /// Removes the element at the specified index of the . + /// + /// The zero-based index of the element to remove. + /// + /// index is less than zero.-or-index is equal to or greater than + /// . + /// + protected override void RemoveItem(int index) + { + if (index == 0) + { + throw new ArgumentOutOfRangeException( + nameof(index), + "0 is not a valid address for a Modbus data collection."); + } + + base.RemoveItem(index); + } + + /// + /// Removes all elements from the . + /// + protected override void ClearItems() + { + _allowZeroElement = true; + base.ClearItems(); + AddDefault(this); + _allowZeroElement = false; + } + + /// + /// Adds a default element to the collection. + /// + /// The data. + private static IList AddDefault(IList data) + { + data.Insert(0, default(TData)); + return data; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataType.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataType.cs new file mode 100644 index 0000000..2494a59 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/ModbusDataType.cs @@ -0,0 +1,28 @@ +namespace Modbus.Data +{ + /// + /// Types of data supported by the Modbus protocol. + /// + public enum ModbusDataType + { + /// + /// Read/write register. + /// + HoldingRegister, + + /// + /// Readonly register. + /// + InputRegister, + + /// + /// Read/write discrete. + /// + Coil, + + /// + /// Readonly discrete. + /// + Input + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/RegisterCollection.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/RegisterCollection.cs new file mode 100644 index 0000000..2d54f2d --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Data/RegisterCollection.cs @@ -0,0 +1,86 @@ +namespace Modbus.Data +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + using System.Net; + + using Utility; + + /// + /// Collection of 16 bit registers. + /// + public class RegisterCollection : Collection, IModbusMessageDataCollection + { + /// + /// Initializes a new instance of the class. + /// + public RegisterCollection() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Array for register collection. + public RegisterCollection(byte[] bytes) + : this((IList)ModbusUtility.NetworkBytesToHostUInt16(bytes)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Array for register collection. + public RegisterCollection(params ushort[] registers) + : this((IList)registers) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// List for register collection. + public RegisterCollection(IList registers) + : base(registers.IsReadOnly ? new List(registers) : registers) + { + } + + public byte[] NetworkBytes + { + get + { + var bytes = new MemoryStream(ByteCount); + + foreach (ushort register in this) + { + var b = BitConverter.GetBytes((ushort)IPAddress.HostToNetworkOrder((short)register)); + bytes.Write(b, 0, b.Length); + } + + return bytes.ToArray(); + } + } + + /// + /// Gets the byte count. + /// + public byte ByteCount + { + get { return (byte)(Count * 2); } + } + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + return string.Concat("{", string.Join(", ", this.Select(v => v.ToString()).ToArray()), "}"); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusMaster.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusMaster.cs new file mode 100644 index 0000000..b1448d0 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusMaster.cs @@ -0,0 +1,191 @@ +namespace Modbus.Device +{ + using System; + using System.Threading.Tasks; + + using IO; + + /// + /// Modbus master device. + /// + public interface IModbusMaster : IDisposable + { + /// + /// Transport used by this master. + /// + ModbusTransport Transport { get; } + + /// + /// Reads from 1 to 2000 contiguous coils status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of coils to read. + /// Coils status. + bool[] ReadCoils(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Asynchronously reads from 1 to 2000 contiguous coils status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of coils to read. + /// A task that represents the asynchronous read operation. + Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Reads from 1 to 2000 contiguous discrete input status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of discrete inputs to read. + /// Discrete inputs status. + bool[] ReadInputs(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Asynchronously reads from 1 to 2000 contiguous discrete input status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of discrete inputs to read. + /// A task that represents the asynchronous read operation. + Task ReadInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Reads contiguous block of holding registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// Holding registers status. + ushort[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Asynchronously reads contiguous block of holding registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// A task that represents the asynchronous read operation. + Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Reads contiguous block of input registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// Input registers status. + ushort[] ReadInputRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Asynchronously reads contiguous block of input registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// A task that represents the asynchronous read operation. + Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints); + + /// + /// Writes a single coil value. + /// + /// Address of the device to write to. + /// Address to write value to. + /// Value to write. + void WriteSingleCoil(byte slaveAddress, ushort coilAddress, bool value); + + /// + /// Asynchronously writes a single coil value. + /// + /// Address of the device to write to. + /// Address to write value to. + /// Value to write. + /// A task that represents the asynchronous write operation. + Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value); + + /// + /// Writes a single holding register. + /// + /// Address of the device to write to. + /// Address to write. + /// Value to write. + void WriteSingleRegister(byte slaveAddress, ushort registerAddress, ushort value); + + /// + /// Asynchronously writes a single holding register. + /// + /// Address of the device to write to. + /// Address to write. + /// Value to write. + /// A task that represents the asynchronous write operation. + Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value); + + /// + /// Writes a block of 1 to 123 contiguous registers. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + void WriteMultipleRegisters(byte slaveAddress, ushort startAddress, ushort[] data); + + /// + /// Asynchronously writes a block of 1 to 123 contiguous registers. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + /// A task that represents the asynchronous write operation. + Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] data); + + /// + /// Writes a sequence of coils. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + void WriteMultipleCoils(byte slaveAddress, ushort startAddress, bool[] data); + + /// + /// Asynchronously writes a sequence of coils. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + /// A task that represents the asynchronous write operation. + Task WriteMultipleCoilsAsync(byte slaveAddress, ushort startAddress, bool[] data); + + /// + /// Performs a combination of one read operation and one write operation in a single Modbus transaction. + /// The write operation is performed before the read. + /// + /// Address of device to read values from. + /// Address to begin reading (Holding registers are addressed starting at 0). + /// Number of registers to read. + /// Address to begin writing (Holding registers are addressed starting at 0). + /// Register values to write. + ushort[] ReadWriteMultipleRegisters( + byte slaveAddress, + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + ushort[] writeData); + + /// + /// Asynchronously performs a combination of one read operation and one write operation in a single Modbus transaction. + /// The write operation is performed before the read. + /// + /// Address of device to read values from. + /// Address to begin reading (Holding registers are addressed starting at 0). + /// Number of registers to read. + /// Address to begin writing (Holding registers are addressed starting at 0). + /// Register values to write. + /// A task that represents the asynchronous operation + Task ReadWriteMultipleRegistersAsync( + byte slaveAddress, + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + ushort[] writeData); + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusSerialMaster.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusSerialMaster.cs new file mode 100644 index 0000000..c65fc7e --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/IModbusSerialMaster.cs @@ -0,0 +1,26 @@ +namespace Modbus.Device +{ + using IO; + + /// + /// Modbus Serial Master device. + /// + public interface IModbusSerialMaster : IModbusMaster + { + /// + /// Transport for used by this master. + /// + new ModbusSerialTransport Transport { get; } + + /// + /// Serial Line only. + /// Diagnostic function which loops back the original data. + /// NModbus only supports looping back one ushort value, this is a + /// limitation of the "Best Effort" implementation of the RTU protocol. + /// + /// Address of device to test. + /// Data to return. + /// Return true if slave device echoed data. + bool ReturnQueryData(byte slaveAddress, ushort data); + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusDevice.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusDevice.cs new file mode 100644 index 0000000..4b341c5 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusDevice.cs @@ -0,0 +1,53 @@ +namespace Modbus.Device +{ + using System; + + using IO; + + using Unme.Common; + + /// + /// Modbus device. + /// + public abstract class ModbusDevice : IDisposable + { + private ModbusTransport _transport; + + internal ModbusDevice(ModbusTransport transport) + { + _transport = transport; + } + + /// + /// Gets the Modbus Transport. + /// + public ModbusTransport Transport + { + get { return _transport; } + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// true to release both managed and unmanaged resources; + /// false to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposableUtility.Dispose(ref _transport); + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusIpMaster.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusIpMaster.cs new file mode 100644 index 0000000..32cccee --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusIpMaster.cs @@ -0,0 +1,313 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics.CodeAnalysis; +#if SERIAL + using System.IO.Ports; +#endif + using System.Net.Sockets; + using System.Threading.Tasks; + + using IO; + + /// + /// Modbus IP master device. + /// + [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")] + public class ModbusIpMaster : ModbusMaster + { + /// + /// Modbus IP master device. + /// + /// Transport used by this master. + private ModbusIpMaster(ModbusTransport transport) + : base(transport) + { + } + + /// + /// Modbus IP master factory method. + /// + /// New instance of Modbus IP master device using provided TCP client. + [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")] + public static ModbusIpMaster CreateIp(TcpClient tcpClient) + { + if (tcpClient == null) + { + throw new ArgumentNullException(nameof(tcpClient)); + } + + return CreateIp(new TcpClientAdapter(tcpClient)); + } + + /// + /// Modbus IP master factory method. + /// + /// New instance of Modbus IP master device using provided UDP client. + [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")] + public static ModbusIpMaster CreateIp(UdpClient udpClient) + { + if (udpClient == null) + { + throw new ArgumentNullException(nameof(udpClient)); + } + + if (!udpClient.Client.Connected) + { + throw new InvalidOperationException(Resources.UdpClientNotConnected); + } + + return CreateIp(new UdpClientAdapter(udpClient)); + } + +#if SERIAL + /// + /// Modbus IP master factory method. + /// + /// New instance of Modbus IP master device using provided serial port. + [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")] + public static ModbusIpMaster CreateIp(SerialPort serialPort) + { + if (serialPort == null) + { + throw new ArgumentNullException(nameof(serialPort)); + } + + return CreateIp(new SerialPortAdapter(serialPort)); + } +#endif + + /// + /// Modbus IP master factory method. + /// + /// New instance of Modbus IP master device using provided stream resource. + [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase", Justification = "Breaking change.")] + public static ModbusIpMaster CreateIp(IStreamResource streamResource) + { + if (streamResource == null) + { + throw new ArgumentNullException(nameof(streamResource)); + } + + return new ModbusIpMaster(new ModbusIpTransport(streamResource)); + } + + /// + /// Reads from 1 to 2000 contiguous coils status. + /// + /// Address to begin reading. + /// Number of coils to read. + /// Coils status. + public bool[] ReadCoils(ushort startAddress, ushort numberOfPoints) + { + return base.ReadCoils(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Asynchronously reads from 1 to 2000 contiguous coils status. + /// + /// Address to begin reading. + /// Number of coils to read. + /// A task that represents the asynchronous read operation. + public Task ReadCoilsAsync(ushort startAddress, ushort numberOfPoints) + { + return base.ReadCoilsAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Reads from 1 to 2000 contiguous discrete input status. + /// + /// Address to begin reading. + /// Number of discrete inputs to read. + /// Discrete inputs status. + public bool[] ReadInputs(ushort startAddress, ushort numberOfPoints) + { + return base.ReadInputs(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Asynchronously reads from 1 to 2000 contiguous discrete input status. + /// + /// Address to begin reading. + /// Number of discrete inputs to read. + /// A task that represents the asynchronous read operation. + public Task ReadInputsAsync(ushort startAddress, ushort numberOfPoints) + { + return base.ReadInputsAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Reads contiguous block of holding registers. + /// + /// Address to begin reading. + /// Number of holding registers to read. + /// Holding registers status. + public ushort[] ReadHoldingRegisters(ushort startAddress, ushort numberOfPoints) + { + return base.ReadHoldingRegisters(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Asynchronously reads contiguous block of holding registers. + /// + /// Address to begin reading. + /// Number of holding registers to read. + /// A task that represents the asynchronous read operation. + public Task ReadHoldingRegistersAsync(ushort startAddress, ushort numberOfPoints) + { + return base.ReadHoldingRegistersAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Reads contiguous block of input registers. + /// + /// Address to begin reading. + /// Number of holding registers to read. + /// Input registers status. + public ushort[] ReadInputRegisters(ushort startAddress, ushort numberOfPoints) + { + return base.ReadInputRegisters(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Asynchronously reads contiguous block of input registers. + /// + /// Address to begin reading. + /// Number of holding registers to read. + /// A task that represents the asynchronous read operation. + public Task ReadInputRegistersAsync(ushort startAddress, ushort numberOfPoints) + { + return base.ReadInputRegistersAsync(Modbus.DefaultIpSlaveUnitId, startAddress, numberOfPoints); + } + + /// + /// Writes a single coil value. + /// + /// Address to write value to. + /// Value to write. + public void WriteSingleCoil(ushort coilAddress, bool value) + { + base.WriteSingleCoil(Modbus.DefaultIpSlaveUnitId, coilAddress, value); + } + + /// + /// Asynchronously writes a single coil value. + /// + /// Address to write value to. + /// Value to write. + /// A task that represents the asynchronous write operation. + public Task WriteSingleCoilAsync(ushort coilAddress, bool value) + { + return base.WriteSingleCoilAsync(Modbus.DefaultIpSlaveUnitId, coilAddress, value); + } + + /// + /// Write a single holding register. + /// + /// Address to write. + /// Value to write. + public void WriteSingleRegister(ushort registerAddress, ushort value) + { + base.WriteSingleRegister(Modbus.DefaultIpSlaveUnitId, registerAddress, value); + } + + /// + /// Asynchronously writes a single holding register. + /// + /// Address to write. + /// Value to write. + /// A task that represents the asynchronous write operation. + public Task WriteSingleRegisterAsync(ushort registerAddress, ushort value) + { + return base.WriteSingleRegisterAsync(Modbus.DefaultIpSlaveUnitId, registerAddress, value); + } + + /// + /// Write a block of 1 to 123 contiguous registers. + /// + /// Address to begin writing values. + /// Values to write. + public void WriteMultipleRegisters(ushort startAddress, ushort[] data) + { + base.WriteMultipleRegisters(Modbus.DefaultIpSlaveUnitId, startAddress, data); + } + + /// + /// Asynchronously writes a block of 1 to 123 contiguous registers. + /// + /// Address to begin writing values. + /// Values to write. + /// A task that represents the asynchronous write operation. + public Task WriteMultipleRegistersAsync(ushort startAddress, ushort[] data) + { + return base.WriteMultipleRegistersAsync(Modbus.DefaultIpSlaveUnitId, startAddress, data); + } + + /// + /// Force each coil in a sequence of coils to a provided value. + /// + /// Address to begin writing values. + /// Values to write. + public void WriteMultipleCoils(ushort startAddress, bool[] data) + { + base.WriteMultipleCoils(Modbus.DefaultIpSlaveUnitId, startAddress, data); + } + + /// + /// Asynchronously writes a sequence of coils. + /// + /// Address to begin writing values. + /// Values to write. + /// A task that represents the asynchronous write operation + public Task WriteMultipleCoilsAsync(ushort startAddress, bool[] data) + { + return base.WriteMultipleCoilsAsync(Modbus.DefaultIpSlaveUnitId, startAddress, data); + } + + /// + /// Performs a combination of one read operation and one write operation in a single MODBUS transaction. + /// The write operation is performed before the read. + /// Message uses default TCP slave id of 0. + /// + /// Address to begin reading (Holding registers are addressed starting at 0). + /// Number of registers to read. + /// Address to begin writing (Holding registers are addressed starting at 0). + /// Register values to write. + public ushort[] ReadWriteMultipleRegisters( + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + ushort[] writeData) + { + return base.ReadWriteMultipleRegisters( + Modbus.DefaultIpSlaveUnitId, + startReadAddress, + numberOfPointsToRead, + startWriteAddress, + writeData); + } + + /// + /// Asynchronously performs a combination of one read operation and one write operation in a single Modbus transaction. + /// The write operation is performed before the read. + /// + /// Address to begin reading (Holding registers are addressed starting at 0). + /// Number of registers to read. + /// Address to begin writing (Holding registers are addressed starting at 0). + /// Register values to write. + /// A task that represents the asynchronous operation. + public Task ReadWriteMultipleRegistersAsync( + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + ushort[] writeData) + { + return base.ReadWriteMultipleRegistersAsync( + Modbus.DefaultIpSlaveUnitId, + startReadAddress, + numberOfPointsToRead, + startWriteAddress, + writeData); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMaster.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMaster.cs new file mode 100644 index 0000000..c9ab04e --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMaster.cs @@ -0,0 +1,452 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Threading.Tasks; + + using Data; + using IO; + using Message; + + /// + /// Modbus master device. + /// + public abstract class ModbusMaster : ModbusDevice, IModbusMaster + { + internal ModbusMaster(ModbusTransport transport) + : base(transport) + { + } + + /// + /// Reads from 1 to 2000 contiguous coils status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of coils to read. + /// Coils status. + public bool[] ReadCoils(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000); + + var request = new ReadCoilsInputsRequest( + Modbus.ReadCoils, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadDiscretes(request); + } + + /// + /// Asynchronously reads from 1 to 2000 contiguous coils status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of coils to read. + /// A task that represents the asynchronous read operation. + public Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000); + + var request = new ReadCoilsInputsRequest( + Modbus.ReadCoils, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadDiscretesAsync(request); + } + + /// + /// Reads from 1 to 2000 contiguous discrete input status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of discrete inputs to read. + /// Discrete inputs status. + public bool[] ReadInputs(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000); + + var request = new ReadCoilsInputsRequest( + Modbus.ReadInputs, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadDiscretes(request); + } + + /// + /// Asynchronously reads from 1 to 2000 contiguous discrete input status. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of discrete inputs to read. + /// A task that represents the asynchronous read operation. + public Task ReadInputsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 2000); + + var request = new ReadCoilsInputsRequest( + Modbus.ReadInputs, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadDiscretesAsync(request); + } + + /// + /// Reads contiguous block of holding registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// Holding registers status. + public ushort[] ReadHoldingRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125); + + var request = new ReadHoldingInputRegistersRequest( + Modbus.ReadHoldingRegisters, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadRegisters(request); + } + + /// + /// Asynchronously reads contiguous block of holding registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// A task that represents the asynchronous read operation. + public Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125); + + var request = new ReadHoldingInputRegistersRequest( + Modbus.ReadHoldingRegisters, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadRegistersAsync(request); + } + + /// + /// Reads contiguous block of input registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// Input registers status. + public ushort[] ReadInputRegisters(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125); + + var request = new ReadHoldingInputRegistersRequest( + Modbus.ReadInputRegisters, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadRegisters(request); + } + + /// + /// Asynchronously reads contiguous block of input registers. + /// + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// A task that represents the asynchronous read operation. + public Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + { + ValidateNumberOfPoints("numberOfPoints", numberOfPoints, 125); + + var request = new ReadHoldingInputRegistersRequest( + Modbus.ReadInputRegisters, + slaveAddress, + startAddress, + numberOfPoints); + + return PerformReadRegistersAsync(request); + } + + /// + /// Writes a single coil value. + /// + /// Address of the device to write to. + /// Address to write value to. + /// Value to write. + public void WriteSingleCoil(byte slaveAddress, ushort coilAddress, bool value) + { + var request = new WriteSingleCoilRequestResponse(slaveAddress, coilAddress, value); + Transport.UnicastMessage(request); + } + + /// + /// Asynchronously writes a single coil value. + /// + /// Address of the device to write to. + /// Address to write value to. + /// Value to write. + /// A task that represents the asynchronous write operation. + public Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value) + { + var request = new WriteSingleCoilRequestResponse(slaveAddress, coilAddress, value); + return PerformWriteRequestAsync(request); + } + + /// + /// Writes a single holding register. + /// + /// Address of the device to write to. + /// Address to write. + /// Value to write. + public void WriteSingleRegister(byte slaveAddress, ushort registerAddress, ushort value) + { + var request = new WriteSingleRegisterRequestResponse( + slaveAddress, + registerAddress, + value); + + Transport.UnicastMessage(request); + } + + /// + /// Asynchronously writes a single holding register. + /// + /// Address of the device to write to. + /// Address to write. + /// Value to write. + /// A task that represents the asynchronous write operation. + public Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value) + { + var request = new WriteSingleRegisterRequestResponse( + slaveAddress, + registerAddress, + value); + + return PerformWriteRequestAsync(request); + } + + /// + /// Write a block of 1 to 123 contiguous 16 bit holding registers. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + public void WriteMultipleRegisters(byte slaveAddress, ushort startAddress, ushort[] data) + { + ValidateData("data", data, 123); + + var request = new WriteMultipleRegistersRequest( + slaveAddress, + startAddress, + new RegisterCollection(data)); + + Transport.UnicastMessage(request); + } + + /// + /// Asynchronously writes a block of 1 to 123 contiguous registers. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + /// A task that represents the asynchronous write operation. + public Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] data) + { + ValidateData("data", data, 123); + + var request = new WriteMultipleRegistersRequest( + slaveAddress, + startAddress, + new RegisterCollection(data)); + + return PerformWriteRequestAsync(request); + } + + /// + /// Writes a sequence of coils. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + public void WriteMultipleCoils(byte slaveAddress, ushort startAddress, bool[] data) + { + ValidateData("data", data, 1968); + + var request = new WriteMultipleCoilsRequest( + slaveAddress, + startAddress, + new DiscreteCollection(data)); + + Transport.UnicastMessage(request); + } + + /// + /// Asynchronously writes a sequence of coils. + /// + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + /// A task that represents the asynchronous write operation. + public Task WriteMultipleCoilsAsync(byte slaveAddress, ushort startAddress, bool[] data) + { + ValidateData("data", data, 1968); + + var request = new WriteMultipleCoilsRequest( + slaveAddress, + startAddress, + new DiscreteCollection(data)); + + return PerformWriteRequestAsync(request); + } + + /// + /// Performs a combination of one read operation and one write operation in a single Modbus transaction. + /// The write operation is performed before the read. + /// + /// Address of device to read values from. + /// Address to begin reading (Holding registers are addressed starting at 0). + /// Number of registers to read. + /// Address to begin writing (Holding registers are addressed starting at 0). + /// Register values to write. + public ushort[] ReadWriteMultipleRegisters( + byte slaveAddress, + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + ushort[] writeData) + { + ValidateNumberOfPoints("numberOfPointsToRead", numberOfPointsToRead, 125); + ValidateData("writeData", writeData, 121); + + var request = new ReadWriteMultipleRegistersRequest( + slaveAddress, + startReadAddress, + numberOfPointsToRead, + startWriteAddress, + new RegisterCollection(writeData)); + + return PerformReadRegisters(request); + } + + /// + /// Asynchronously performs a combination of one read operation and one write operation in a single Modbus transaction. + /// The write operation is performed before the read. + /// + /// Address of device to read values from. + /// Address to begin reading (Holding registers are addressed starting at 0). + /// Number of registers to read. + /// Address to begin writing (Holding registers are addressed starting at 0). + /// Register values to write. + /// A task that represents the asynchronous operation. + public Task ReadWriteMultipleRegistersAsync( + byte slaveAddress, + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + ushort[] writeData) + { + ValidateNumberOfPoints("numberOfPointsToRead", numberOfPointsToRead, 125); + ValidateData("writeData", writeData, 121); + + var request = new ReadWriteMultipleRegistersRequest( + slaveAddress, + startReadAddress, + numberOfPointsToRead, + startWriteAddress, + new RegisterCollection(writeData)); + + return PerformReadRegistersAsync(request); + } + + /// + /// Executes the custom message. + /// + /// The type of the response. + /// The request. + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] + [SuppressMessage("Microsoft.Usage", "CA2223:MembersShouldDifferByMoreThanReturnType")] + public TResponse ExecuteCustomMessage(IModbusMessage request) + where TResponse : IModbusMessage, new() + { + return Transport.UnicastMessage(request); + } + + private static void ValidateData(string argumentName, T[] data, int maxDataLength) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (data.Length == 0 || data.Length > maxDataLength) + { + string msg = $"The length of argument {argumentName} must be between 1 and {maxDataLength} inclusive."; + throw new ArgumentException(msg); + } + } + + private static void ValidateNumberOfPoints(string argumentName, ushort numberOfPoints, ushort maxNumberOfPoints) + { + if (numberOfPoints < 1 || numberOfPoints > maxNumberOfPoints) + { + string msg = $"Argument {argumentName} must be between 1 and {maxNumberOfPoints} inclusive."; + throw new ArgumentException(msg); + } + } + + private bool[] PerformReadDiscretes(ReadCoilsInputsRequest request) + { + ReadCoilsInputsResponse response = Transport.UnicastMessage(request); + return response.Data.Take(request.NumberOfPoints).ToArray(); + } + + private Task PerformReadDiscretesAsync(ReadCoilsInputsRequest request) + { + return Task.Factory.StartNew(() => PerformReadDiscretes(request)); + } + + private ushort[] PerformReadRegisters(ReadHoldingInputRegistersRequest request) + { + ReadHoldingInputRegistersResponse response = + Transport.UnicastMessage(request); + + return response.Data.Take(request.NumberOfPoints).ToArray(); + } + + private Task PerformReadRegistersAsync(ReadHoldingInputRegistersRequest request) + { + return Task.Factory.StartNew(() => PerformReadRegisters(request)); + } + + private ushort[] PerformReadRegisters(ReadWriteMultipleRegistersRequest request) + { + ReadHoldingInputRegistersResponse response = + Transport.UnicastMessage(request); + + return response.Data.Take(request.ReadRequest.NumberOfPoints).ToArray(); + } + + private Task PerformReadRegistersAsync(ReadWriteMultipleRegistersRequest request) + { + return Task.Factory.StartNew(() => PerformReadRegisters(request)); + } + + private Task PerformWriteRequestAsync(IModbusMessage request) + where T : IModbusMessage, new() + { + return Task.Factory.StartNew(() => Transport.UnicastMessage(request)); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMasterTcpConnection.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMasterTcpConnection.cs new file mode 100644 index 0000000..b601903 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusMasterTcpConnection.cs @@ -0,0 +1,122 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Threading.Tasks; + + using IO; + using Message; + + /// + /// Represents an incoming connection from a Modbus master. Contains the slave's logic to process the connection. + /// + internal class ModbusMasterTcpConnection : ModbusDevice, IDisposable + { + private readonly TcpClient _client; + private readonly string _endPoint; + private readonly Stream _stream; + private readonly ModbusTcpSlave _slave; + private readonly Task _requestHandlerTask; + + private readonly byte[] _mbapHeader = new byte[6]; + private byte[] _messageFrame; + + public ModbusMasterTcpConnection(TcpClient client, ModbusTcpSlave slave) + : base(new ModbusIpTransport(new TcpClientAdapter(client))) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (slave == null) + { + throw new ArgumentNullException(nameof(slave)); + } + + _client = client; + _endPoint = client.Client.RemoteEndPoint.ToString(); + _stream = client.GetStream(); + _slave = slave; + _requestHandlerTask = Task.Run((Func)HandleRequestAsync); + } + + /// + /// Occurs when a Modbus master TCP connection is closed. + /// + public event EventHandler ModbusMasterTcpConnectionClosed; + + public string EndPoint + { + get { return _endPoint; } + } + + public Stream Stream + { + get { return _stream; } + } + + public TcpClient TcpClient + { + get { return _client; } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _stream.Dispose(); + } + + base.Dispose(disposing); + } + + private async Task HandleRequestAsync() + { + while (true) + { + Debug.WriteLine($"Begin reading header from Master at IP: {EndPoint}"); + + int readBytes = await Stream.ReadAsync(_mbapHeader, 0, 6).ConfigureAwait(false); + if (readBytes == 0) + { + Debug.WriteLine($"0 bytes read, Master at {EndPoint} has closed Socket connection."); + ModbusMasterTcpConnectionClosed?.Invoke(this, new TcpConnectionEventArgs(EndPoint)); + return; + } + + ushort frameLength = (ushort)IPAddress.HostToNetworkOrder(BitConverter.ToInt16(_mbapHeader, 4)); + Debug.WriteLine($"Master at {EndPoint} sent header: \"{string.Join(", ", _mbapHeader)}\" with {frameLength} bytes in PDU"); + + _messageFrame = new byte[frameLength]; + readBytes = await Stream.ReadAsync(_messageFrame, 0, frameLength).ConfigureAwait(false); + if (readBytes == 0) + { + Debug.WriteLine($"0 bytes read, Master at {EndPoint} has closed Socket connection."); + ModbusMasterTcpConnectionClosed?.Invoke(this, new TcpConnectionEventArgs(EndPoint)); + return; + } + + Debug.WriteLine($"Read frame from Master at {EndPoint} completed {readBytes} bytes"); + byte[] frame = _mbapHeader.Concat(_messageFrame).ToArray(); + Debug.WriteLine($"RX from Master at {EndPoint}: {string.Join(", ", frame)}"); + + var request = ModbusMessageFactory.CreateModbusRequest(_messageFrame); + request.TransactionId = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 0)); + + // perform action and build response + IModbusMessage response = _slave.ApplyRequest(request); + response.TransactionId = request.TransactionId; + + // write response + byte[] responseFrame = Transport.BuildMessageFrame(response); + Debug.WriteLine($"TX to Master at {EndPoint}: {string.Join(", ", responseFrame)}"); + await Stream.WriteAsync(responseFrame, 0, responseFrame.Length).ConfigureAwait(false); + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialMaster.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialMaster.cs new file mode 100644 index 0000000..f7b2986 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialMaster.cs @@ -0,0 +1,175 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics.CodeAnalysis; +#if SERIAL + using System.IO.Ports; +#endif + using System.Net.Sockets; + + using Data; + using IO; + using Message; + + /// + /// Modbus serial master device. + /// + public class ModbusSerialMaster : ModbusMaster, IModbusSerialMaster + { + private ModbusSerialMaster(ModbusTransport transport) + : base(transport) + { + } + + /// + /// Gets the Modbus Transport. + /// + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")] + ModbusSerialTransport IModbusSerialMaster.Transport + { + get { return (ModbusSerialTransport)Transport; } + } + +#if SERIAL + /// + /// Modbus ASCII master factory method. + /// + public static ModbusSerialMaster CreateAscii(SerialPort serialPort) + { + if (serialPort == null) + { + throw new ArgumentNullException(nameof(serialPort)); + } + + return CreateAscii(new SerialPortAdapter(serialPort)); + } +#endif + + /// + /// Modbus ASCII master factory method. + /// + public static ModbusSerialMaster CreateAscii(TcpClient tcpClient) + { + if (tcpClient == null) + { + throw new ArgumentNullException(nameof(tcpClient)); + } + + return CreateAscii(new TcpClientAdapter(tcpClient)); + } + + /// + /// Modbus ASCII master factory method. + /// + public static ModbusSerialMaster CreateAscii(UdpClient udpClient) + { + if (udpClient == null) + { + throw new ArgumentNullException(nameof(udpClient)); + } + + if (!udpClient.Client.Connected) + { + throw new InvalidOperationException(Resources.UdpClientNotConnected); + } + + return CreateAscii(new UdpClientAdapter(udpClient)); + } + + /// + /// Modbus ASCII master factory method. + /// + public static ModbusSerialMaster CreateAscii(IStreamResource streamResource) + { + if (streamResource == null) + { + throw new ArgumentNullException(nameof(streamResource)); + } + + return new ModbusSerialMaster(new ModbusAsciiTransport(streamResource)); + } + +#if SERIAL + /// + /// Modbus RTU master factory method. + /// + public static ModbusSerialMaster CreateRtu(SerialPort serialPort) + { + if (serialPort == null) + { + throw new ArgumentNullException(nameof(serialPort)); + } + + return CreateRtu(new SerialPortAdapter(serialPort)); + } +#endif + + /// + /// Modbus RTU master factory method. + /// + public static ModbusSerialMaster CreateRtu(TcpClient tcpClient) + { + if (tcpClient == null) + { + throw new ArgumentNullException(nameof(tcpClient)); + } + + return CreateRtu(new TcpClientAdapter(tcpClient)); + } + + /// + /// Modbus RTU master factory method. + /// + public static ModbusSerialMaster CreateRtu(UdpClient udpClient) + { + if (udpClient == null) + { + throw new ArgumentNullException(nameof(udpClient)); + } + + if (!udpClient.Client.Connected) + { + throw new InvalidOperationException(Resources.UdpClientNotConnected); + } + + return CreateRtu(new UdpClientAdapter(udpClient)); + } + + /// + /// Modbus RTU master factory method. + /// + public static ModbusSerialMaster CreateRtu(IStreamResource streamResource) + { + if (streamResource == null) + { + throw new ArgumentNullException(nameof(streamResource)); + } + + return new ModbusSerialMaster(new ModbusRtuTransport(streamResource)); + } + + /// + /// Serial Line only. + /// Diagnostic function which loops back the original data. + /// NModbus only supports looping back one ushort value, this is a limitation of the "Best Effort" implementation of + /// the RTU protocol. + /// + /// Address of device to test. + /// Data to return. + /// Return true if slave device echoed data. + public bool ReturnQueryData(byte slaveAddress, ushort data) + { + DiagnosticsRequestResponse request; + DiagnosticsRequestResponse response; + + request = new DiagnosticsRequestResponse( + Modbus.DiagnosticsReturnQueryData, + slaveAddress, + new RegisterCollection(data)); + + response = Transport.UnicastMessage(request); + + return response.Data[0] == data; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialSlave.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialSlave.cs new file mode 100644 index 0000000..697207c --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSerialSlave.cs @@ -0,0 +1,153 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Threading.Tasks; +#if SERIAL + using System.IO.Ports; +#endif + using IO; + using Message; + + /// + /// Modbus serial slave device. + /// + public class ModbusSerialSlave : ModbusSlave + { + private ModbusSerialSlave(byte unitId, ModbusTransport transport) + : base(unitId, transport) + { + } + + private ModbusSerialTransport SerialTransport + { + get + { + var transport = Transport as ModbusSerialTransport; + + if (transport == null) + { + throw new ObjectDisposedException("SerialTransport"); + } + + return transport; + } + } + +#if SERIAL + /// + /// Modbus ASCII slave factory method. + /// + public static ModbusSerialSlave CreateAscii(byte unitId, SerialPort serialPort) + { + if (serialPort == null) + { + throw new ArgumentNullException(nameof(serialPort)); + } + + return CreateAscii(unitId, new SerialPortAdapter(serialPort)); + } +#endif + + /// + /// Modbus ASCII slave factory method. + /// + public static ModbusSerialSlave CreateAscii(byte unitId, IStreamResource streamResource) + { + if (streamResource == null) + { + throw new ArgumentNullException(nameof(streamResource)); + } + + return new ModbusSerialSlave(unitId, new ModbusAsciiTransport(streamResource)); + } + +#if SERIAL + /// + /// Modbus RTU slave factory method. + /// + public static ModbusSerialSlave CreateRtu(byte unitId, SerialPort serialPort) + { + if (serialPort == null) + { + throw new ArgumentNullException(nameof(serialPort)); + } + + return CreateRtu(unitId, new SerialPortAdapter(serialPort)); + } +#endif + + /// + /// Modbus RTU slave factory method. + /// + public static ModbusSerialSlave CreateRtu(byte unitId, IStreamResource streamResource) + { + if (streamResource == null) + { + throw new ArgumentNullException(nameof(streamResource)); + } + + return new ModbusSerialSlave(unitId, new ModbusRtuTransport(streamResource)); + } + + /// + /// Start slave listening for requests. + /// + public override async Task ListenAsync() + { + while (true) + { + try + { + try + { + //TODO: remove deleay once async will be implemented in transport level + await Task.Delay(20).ConfigureAwait(false); + + // read request and build message + byte[] frame = SerialTransport.ReadRequest(); + IModbusMessage request = ModbusMessageFactory.CreateModbusRequest(frame); + + if (SerialTransport.CheckFrame && !SerialTransport.ChecksumsMatch(request, frame)) + { + string msg = $"Checksums failed to match {string.Join(", ", request.MessageFrame)} != {string.Join(", ", frame)}."; + Debug.WriteLine(msg); + throw new IOException(msg); + } + + // only service requests addressed to this particular slave + if (request.SlaveAddress != UnitId) + { + Debug.WriteLine($"NModbus Slave {UnitId} ignoring request intended for NModbus Slave {request.SlaveAddress}"); + continue; + } + + // perform action + IModbusMessage response = ApplyRequest(request); + + // write response + SerialTransport.Write(response); + } + catch (IOException ioe) + { + Debug.WriteLine($"IO Exception encountered while listening for requests - {ioe.Message}"); + SerialTransport.DiscardInBuffer(); + } + catch (TimeoutException te) + { + Debug.WriteLine($"Timeout Exception encountered while listening for requests - {te.Message}"); + SerialTransport.DiscardInBuffer(); + } + + // TODO better exception handling here, missing FormatException, NotImplemented... + } + catch (InvalidOperationException) + { + // when the underlying transport is disposed + break; + } + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlave.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlave.cs new file mode 100644 index 0000000..e5890b3 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlave.cs @@ -0,0 +1,271 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Threading.Tasks; + + using Data; + using IO; + using Message; + + /// + /// Modbus slave device. + /// + public abstract class ModbusSlave : ModbusDevice + { + internal ModbusSlave(byte unitId, ModbusTransport transport) + : base(transport) + { + DataStore = DataStoreFactory.CreateDefaultDataStore(); + UnitId = unitId; + } + + /// + /// Raised when a Modbus slave receives a request, before processing request function. + /// + /// The Modbus request was invalid, and an error response the specified exception should be sent. + public event EventHandler ModbusSlaveRequestReceived; + + /// + /// Raised when a Modbus slave receives a write request, after processing the write portion of the function. + /// + /// For Read/Write Multiple registers (function code 23), this method is raised after writing and before reading. + public event EventHandler WriteComplete; + + /// + /// Gets or sets the data store. + /// + public DataStore DataStore { get; set; } + + /// + /// Gets or sets the unit ID. + /// + public byte UnitId { get; set; } + + /// + /// Start slave listening for requests. + /// + public abstract Task ListenAsync(); + + internal static ReadCoilsInputsResponse ReadDiscretes( + ReadCoilsInputsRequest request, + DataStore dataStore, + ModbusDataCollection dataSource) + { + DiscreteCollection data; + ReadCoilsInputsResponse response; + + data = DataStore.ReadData( + dataStore, + dataSource, + request.StartAddress, + request.NumberOfPoints, + dataStore.SyncRoot); + + response = new ReadCoilsInputsResponse( + request.FunctionCode, + request.SlaveAddress, + data.ByteCount, + data); + + return response; + } + + internal static ReadHoldingInputRegistersResponse ReadRegisters( + ReadHoldingInputRegistersRequest request, + DataStore dataStore, + ModbusDataCollection dataSource) + { + RegisterCollection data; + ReadHoldingInputRegistersResponse response; + + data = DataStore.ReadData( + dataStore, + dataSource, + request.StartAddress, + request.NumberOfPoints, + dataStore.SyncRoot); + + response = new ReadHoldingInputRegistersResponse( + request.FunctionCode, + request.SlaveAddress, + data); + + return response; + } + + internal static WriteSingleCoilRequestResponse WriteSingleCoil( + WriteSingleCoilRequestResponse request, + DataStore dataStore, + ModbusDataCollection dataSource) + { + DataStore.WriteData( + dataStore, + new DiscreteCollection(request.Data[0] == Modbus.CoilOn), + dataSource, + request.StartAddress, + dataStore.SyncRoot); + + return request; + } + + internal static WriteMultipleCoilsResponse WriteMultipleCoils( + WriteMultipleCoilsRequest request, + DataStore dataStore, + ModbusDataCollection dataSource) + { + WriteMultipleCoilsResponse response; + + DataStore.WriteData( + dataStore, + request.Data.Take(request.NumberOfPoints), + dataSource, + request.StartAddress, + dataStore.SyncRoot); + + response = new WriteMultipleCoilsResponse( + request.SlaveAddress, + request.StartAddress, + request.NumberOfPoints); + + return response; + } + + internal static WriteSingleRegisterRequestResponse WriteSingleRegister( + WriteSingleRegisterRequestResponse request, + DataStore dataStore, + ModbusDataCollection dataSource) + { + DataStore.WriteData( + dataStore, + request.Data, + dataSource, + request.StartAddress, + dataStore.SyncRoot); + + return request; + } + + internal static WriteMultipleRegistersResponse WriteMultipleRegisters( + WriteMultipleRegistersRequest request, + DataStore dataStore, + ModbusDataCollection dataSource) + { + WriteMultipleRegistersResponse response; + + DataStore.WriteData( + dataStore, + request.Data, + dataSource, + request.StartAddress, + dataStore.SyncRoot); + + response = new WriteMultipleRegistersResponse( + request.SlaveAddress, + request.StartAddress, + request.NumberOfPoints); + + return response; + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Cast is not unneccessary.")] + internal IModbusMessage ApplyRequest(IModbusMessage request) + { + IModbusMessage response; + + try + { + Debug.WriteLine(request.ToString()); + var eventArgs = new ModbusSlaveRequestEventArgs(request); + ModbusSlaveRequestReceived?.Invoke(this, eventArgs); + + switch (request.FunctionCode) + { + case Modbus.ReadCoils: + response = ReadDiscretes( + (ReadCoilsInputsRequest)request, + DataStore, + DataStore.CoilDiscretes); + break; + case Modbus.ReadInputs: + response = ReadDiscretes( + (ReadCoilsInputsRequest)request, + DataStore, + DataStore.InputDiscretes); + break; + case Modbus.ReadHoldingRegisters: + response = ReadRegisters( + (ReadHoldingInputRegistersRequest)request, + DataStore, + DataStore.HoldingRegisters); + break; + case Modbus.ReadInputRegisters: + response = ReadRegisters( + (ReadHoldingInputRegistersRequest)request, + DataStore, + DataStore.InputRegisters); + break; + case Modbus.Diagnostics: + response = request; + break; + case Modbus.WriteSingleCoil: + response = WriteSingleCoil( + (WriteSingleCoilRequestResponse)request, + DataStore, + DataStore.CoilDiscretes); + WriteComplete?.Invoke(this, eventArgs); + break; + case Modbus.WriteSingleRegister: + response = WriteSingleRegister( + (WriteSingleRegisterRequestResponse)request, + DataStore, + DataStore.HoldingRegisters); + WriteComplete?.Invoke(this, eventArgs); + break; + case Modbus.WriteMultipleCoils: + response = WriteMultipleCoils( + (WriteMultipleCoilsRequest)request, + DataStore, + DataStore.CoilDiscretes); + WriteComplete?.Invoke(this, eventArgs); + break; + case Modbus.WriteMultipleRegisters: + response = WriteMultipleRegisters( + (WriteMultipleRegistersRequest)request, + DataStore, + DataStore.HoldingRegisters); + WriteComplete?.Invoke(this, eventArgs); + break; + case Modbus.ReadWriteMultipleRegisters: + ReadWriteMultipleRegistersRequest readWriteRequest = (ReadWriteMultipleRegistersRequest)request; + WriteMultipleRegisters( + readWriteRequest.WriteRequest, + DataStore, + DataStore.HoldingRegisters); + WriteComplete?.Invoke(this, eventArgs); + response = ReadRegisters( + readWriteRequest.ReadRequest, + DataStore, + DataStore.HoldingRegisters); + break; + default: + string msg = $"Unsupported function code {request.FunctionCode}."; + Debug.WriteLine(msg); + throw new InvalidModbusRequestException(Modbus.IllegalFunction); + } + } + catch (InvalidModbusRequestException ex) + { + // Catches the exception for an illegal function or a custom exception from the ModbusSlaveRequestReceived event. + response = new SlaveExceptionResponse( + request.SlaveAddress, + (byte)(Modbus.ExceptionOffset + request.FunctionCode), + ex.ExceptionCode); + } + + return response; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlaveRequestEventArgs.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlaveRequestEventArgs.cs new file mode 100644 index 0000000..0bcde18 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusSlaveRequestEventArgs.cs @@ -0,0 +1,27 @@ +namespace Modbus.Device +{ + using System; + + using Message; + + /// + /// Modbus Slave request event args containing information on the message. + /// + public class ModbusSlaveRequestEventArgs : EventArgs + { + private readonly IModbusMessage _message; + + internal ModbusSlaveRequestEventArgs(IModbusMessage message) + { + _message = message; + } + + /// + /// Gets the message. + /// + public IModbusMessage Message + { + get { return _message; } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusTcpSlave.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusTcpSlave.cs new file mode 100644 index 0000000..6401640 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusTcpSlave.cs @@ -0,0 +1,203 @@ +namespace Modbus.Device +{ + using System; + using System.Collections.Concurrent; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Linq; + using System.Net.Sockets; + using System.Threading.Tasks; +#if TIMER + using System.Timers; +#endif + using IO; + + /// + /// Modbus TCP slave device. + /// + public class ModbusTcpSlave : ModbusSlave + { + private const int TimeWaitResponse = 1000; + private readonly object _serverLock = new object(); + + private readonly ConcurrentDictionary _masters = + new ConcurrentDictionary(); + + private TcpListener _server; +#if TIMER + private Timer _timer; +#endif + private ModbusTcpSlave(byte unitId, TcpListener tcpListener) + : base(unitId, new EmptyTransport()) + { + if (tcpListener == null) + { + throw new ArgumentNullException(nameof(tcpListener)); + } + + _server = tcpListener; + } + +#if TIMER + private ModbusTcpSlave(byte unitId, TcpListener tcpListener, double timeInterval) + : base(unitId, new EmptyTransport()) + { + if (tcpListener == null) + { + throw new ArgumentNullException(nameof(tcpListener)); + } + + _server = tcpListener; + _timer = new Timer(timeInterval); + _timer.Elapsed += OnTimer; + _timer.Enabled = true; + } +#endif + + /// + /// Gets the Modbus TCP Masters connected to this Modbus TCP Slave. + /// + public ReadOnlyCollection Masters + { + get + { + return new ReadOnlyCollection(_masters.Values.Select(mc => mc.TcpClient).ToList()); + } + } + + /// + /// Gets the server. + /// + /// The server. + /// + /// This property is not thread safe, it should only be consumed within a lock. + /// + private TcpListener Server + { + get + { + if (_server == null) + { + throw new ObjectDisposedException("Server"); + } + + return _server; + } + } + + /// + /// Modbus TCP slave factory method. + /// + public static ModbusTcpSlave CreateTcp(byte unitId, TcpListener tcpListener) + { + return new ModbusTcpSlave(unitId, tcpListener); + } + +#if TIMER + /// + /// Creates ModbusTcpSlave with timer which polls connected clients every + /// milliseconds on that they are connected. + /// + public static ModbusTcpSlave CreateTcp(byte unitId, TcpListener tcpListener, double pollInterval) + { + return new ModbusTcpSlave(unitId, tcpListener, pollInterval); + } +#endif + + /// + /// Start slave listening for requests. + /// + public override async Task ListenAsync() + { + Debug.WriteLine("Start Modbus Tcp Server."); + // TODO: add state {stoped, listening} and check it before starting + Server.Start(); + + while (true) + { + TcpClient client = await Server.AcceptTcpClientAsync().ConfigureAwait(false); + var masterConnection = new ModbusMasterTcpConnection(client, this); + masterConnection.ModbusMasterTcpConnectionClosed += OnMasterConnectionClosedHandler; + _masters.TryAdd(client.Client.RemoteEndPoint.ToString(), masterConnection); + } + } + + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// + /// true to release both managed and unmanaged resources; false to release only + /// unmanaged resources. + /// + /// Dispose is thread-safe. + protected override void Dispose(bool disposing) + { + if (disposing) + { + // double-check locking + if (_server != null) + { + lock (_serverLock) + { + if (_server != null) + { + _server.Stop(); + _server = null; + +#if TIMER + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } +#endif + + foreach (var key in _masters.Keys) + { + ModbusMasterTcpConnection connection; + + if (_masters.TryRemove(key, out connection)) + { + connection.ModbusMasterTcpConnectionClosed -= OnMasterConnectionClosedHandler; + connection.Dispose(); + } + } + } + } + } + } + } + + private static bool IsSocketConnected(Socket socket) + { + bool poll = socket.Poll(TimeWaitResponse, SelectMode.SelectRead); + bool available = (socket.Available == 0); + return poll && available; + } + +#if TIMER + private void OnTimer(object sender, ElapsedEventArgs e) + { + foreach (var master in _masters.ToList()) + { + if (IsSocketConnected(master.Value.TcpClient.Client) == false) + { + master.Value.Dispose(); + } + } + } +#endif + private void OnMasterConnectionClosedHandler(object sender, TcpConnectionEventArgs e) + { + ModbusMasterTcpConnection connection; + + if (!_masters.TryRemove(e.EndPoint, out connection)) + { + string msg = $"EndPoint {e.EndPoint} cannot be removed, it does not exist."; + throw new ArgumentException(msg); + } + + Debug.WriteLine($"Removed Master {e.EndPoint}"); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusUdpSlave.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusUdpSlave.cs new file mode 100644 index 0000000..54eacaa --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/ModbusUdpSlave.cs @@ -0,0 +1,87 @@ +namespace Modbus.Device +{ + using System; + using System.Diagnostics; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Threading.Tasks; + + using IO; + using Message; + + using Unme.Common; + + /// + /// Modbus UDP slave device. + /// + public class ModbusUdpSlave : ModbusSlave + { + private readonly UdpClient _udpClient; + + private ModbusUdpSlave(byte unitId, UdpClient udpClient) + : base(unitId, new ModbusIpTransport(new UdpClientAdapter(udpClient))) + { + _udpClient = udpClient; + } + + /// + /// Modbus UDP slave factory method. + /// Creates NModbus UDP slave with default + /// + public static ModbusUdpSlave CreateUdp(UdpClient client) + { + return new ModbusUdpSlave(Modbus.DefaultIpSlaveUnitId, client); + } + + /// + /// Modbus UDP slave factory method. + /// + public static ModbusUdpSlave CreateUdp(byte unitId, UdpClient client) + { + return new ModbusUdpSlave(unitId, client); + } + + /// + /// Start slave listening for requests. + /// + public override async Task ListenAsync() + { + Debug.WriteLine("Start Modbus Udp Server."); + + try + { + while (true) + { + UdpReceiveResult receiveResult = await _udpClient.ReceiveAsync().ConfigureAwait(false); + IPEndPoint masterEndPoint = receiveResult.RemoteEndPoint; + byte[] frame = receiveResult.Buffer; + + Debug.WriteLine($"Read Frame completed {frame.Length} bytes"); + Debug.WriteLine($"RX: {string.Join(", ", frame)}"); + + IModbusMessage request = + ModbusMessageFactory.CreateModbusRequest(frame.Slice(6, frame.Length - 6).ToArray()); + request.TransactionId = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 0)); + + // perform action and build response + IModbusMessage response = ApplyRequest(request); + response.TransactionId = request.TransactionId; + + // write response + byte[] responseFrame = Transport.BuildMessageFrame(response); + Debug.WriteLine($"TX: {string.Join(", ", responseFrame)}"); + await _udpClient.SendAsync(responseFrame, responseFrame.Length, masterEndPoint).ConfigureAwait(false); + } + } + catch (SocketException se) + { + // this hapens when slave stops + if (se.SocketErrorCode != SocketError.Interrupted) + { + throw; + } + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/TcpConnectionEventArgs.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/TcpConnectionEventArgs.cs new file mode 100644 index 0000000..63601af --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Device/TcpConnectionEventArgs.cs @@ -0,0 +1,24 @@ +namespace Modbus.Device +{ + using System; + + internal class TcpConnectionEventArgs : EventArgs + { + public TcpConnectionEventArgs(string endPoint) + { + if (endPoint == null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + if (endPoint == string.Empty) + { + throw new ArgumentException(Resources.EmptyEndPoint); + } + + EndPoint = endPoint; + } + + public string EndPoint { get; set; } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Extensions/Enron/EnronModbus.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Extensions/Enron/EnronModbus.cs new file mode 100644 index 0000000..386883d --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Extensions/Enron/EnronModbus.cs @@ -0,0 +1,163 @@ +namespace Modbus.Extensions.Enron +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + + using Device; + using Utility; + + /// + /// Utility extensions for the Enron Modbus dialect. + /// + public static class EnronModbus + { + /// + /// Read contiguous block of 32 bit holding registers. + /// + /// The Modbus master. + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// Holding registers status + public static uint[] ReadHoldingRegisters32( + this ModbusMaster master, + byte slaveAddress, + ushort startAddress, + ushort numberOfPoints) + { + if (master == null) + { + throw new ArgumentNullException(nameof(master)); + } + + ValidateNumberOfPoints(numberOfPoints, 62); + + // read 16 bit chunks and perform conversion + var rawRegisters = master.ReadHoldingRegisters( + slaveAddress, + startAddress, + (ushort)(numberOfPoints * 2)); + + return Convert(rawRegisters).ToArray(); + } + + /// + /// Read contiguous block of 32 bit input registers. + /// + /// The Modbus master. + /// Address of device to read values from. + /// Address to begin reading. + /// Number of holding registers to read. + /// Input registers status + public static uint[] ReadInputRegisters32( + this ModbusMaster master, + byte slaveAddress, + ushort startAddress, + ushort numberOfPoints) + { + if (master == null) + { + throw new ArgumentNullException(nameof(master)); + } + + ValidateNumberOfPoints(numberOfPoints, 62); + + var rawRegisters = master.ReadInputRegisters( + slaveAddress, + startAddress, + (ushort)(numberOfPoints * 2)); + + return Convert(rawRegisters).ToArray(); + } + + /// + /// Write a single 16 bit holding register. + /// + /// The Modbus master. + /// Address of the device to write to. + /// Address to write. + /// Value to write. + public static void WriteSingleRegister32( + this ModbusMaster master, + byte slaveAddress, + ushort registerAddress, + uint value) + { + if (master == null) + { + throw new ArgumentNullException(nameof(master)); + } + + master.WriteMultipleRegisters32(slaveAddress, registerAddress, new[] { value }); + } + + /// + /// Write a block of contiguous 32 bit holding registers. + /// + /// The Modbus master. + /// Address of the device to write to. + /// Address to begin writing values. + /// Values to write. + public static void WriteMultipleRegisters32( + this ModbusMaster master, + byte slaveAddress, + ushort startAddress, + uint[] data) + { + if (master == null) + { + throw new ArgumentNullException(nameof(master)); + } + + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + if (data.Length == 0 || data.Length > 61) + { + throw new ArgumentException("The length of argument data must be between 1 and 61 inclusive."); + } + + master.WriteMultipleRegisters(slaveAddress, startAddress, Convert(data).ToArray()); + } + + /// + /// Convert the 32 bit registers to two 16 bit values. + /// + private static IEnumerable Convert(uint[] registers) + { + foreach (var register in registers) + { + // low order value + yield return BitConverter.ToUInt16(BitConverter.GetBytes(register), 0); + + // high order value + yield return BitConverter.ToUInt16(BitConverter.GetBytes(register), 2); + } + } + + /// + /// Convert the 16 bit registers to 32 bit registers. + /// + private static IEnumerable Convert(ushort[] registers) + { + for (int i = 0; i < registers.Length; i++) + { + yield return ModbusUtility.GetUInt32(registers[i + 1], registers[i]); + i++; + } + } + + private static void ValidateNumberOfPoints(ushort numberOfPoints, ushort maxNumberOfPoints) + { + if (numberOfPoints < 1 || numberOfPoints > maxNumberOfPoints) + { + string msg = $"Argument numberOfPoints must be between 1 and {maxNumberOfPoints} inclusive."; + throw new ArgumentException(msg); + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/GlobalSuppressions.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/GlobalSuppressions.cs new file mode 100644 index 0000000..446bc02 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +using System.Diagnostics.CodeAnalysis; + +[module: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Modbus")] +[module: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Modbus.Utility")] diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/EmptyTransport.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/EmptyTransport.cs new file mode 100644 index 0000000..72beb66 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/EmptyTransport.cs @@ -0,0 +1,33 @@ +namespace Modbus.IO +{ + using System; + using Message; + + public class EmptyTransport : ModbusTransport + { + internal override byte[] ReadRequest() + { + throw new NotImplementedException(); + } + + internal override IModbusMessage ReadResponse() + { + throw new NotImplementedException(); + } + + internal override byte[] BuildMessageFrame(Message.IModbusMessage message) + { + throw new NotImplementedException(); + } + + internal override void Write(IModbusMessage message) + { + throw new NotImplementedException(); + } + + internal override void OnValidateResponse(IModbusMessage request, IModbusMessage response) + { + throw new NotImplementedException(); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/IStreamResource.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/IStreamResource.cs new file mode 100644 index 0000000..e39c4fb --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/IStreamResource.cs @@ -0,0 +1,48 @@ +namespace Modbus.IO +{ + using System; + + /// + /// Represents a serial resource. + /// Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + public interface IStreamResource : IDisposable + { + /// + /// Indicates that no timeout should occur. + /// + int InfiniteTimeout { get; } + + /// + /// Gets or sets the number of milliseconds before a timeout occurs when a read operation does not finish. + /// + int ReadTimeout { get; set; } + + /// + /// Gets or sets the number of milliseconds before a timeout occurs when a write operation does not finish. + /// + int WriteTimeout { get; set; } + + /// + /// Purges the receive buffer. + /// + void DiscardInBuffer(); + + /// + /// Reads a number of bytes from the input buffer and writes those bytes into a byte array at the specified offset. + /// + /// The byte array to write the input to. + /// The offset in the buffer array to begin writing. + /// The number of bytes to read. + /// The number of bytes read. + int Read(byte[] buffer, int offset, int count); + + /// + /// Writes a specified number of bytes to the port from an output buffer, starting at the specified offset. + /// + /// The byte array that contains the data to write to the port. + /// The offset in the buffer array to begin writing. + /// The number of bytes to write. + void Write(byte[] buffer, int offset, int count); + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusAsciiTransport.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusAsciiTransport.cs new file mode 100644 index 0000000..7f05998 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusAsciiTransport.cs @@ -0,0 +1,70 @@ +namespace Modbus.IO +{ + using System.Diagnostics; + using System.IO; + using System.Text; + + using Message; + using Utility; + + /// + /// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + internal class ModbusAsciiTransport : ModbusSerialTransport + { + internal ModbusAsciiTransport(IStreamResource streamResource) + : base(streamResource) + { + Debug.Assert(streamResource != null, "Argument streamResource cannot be null."); + } + + internal override byte[] BuildMessageFrame(IModbusMessage message) + { + var msgFrame = message.MessageFrame; + + var msgFrameAscii = ModbusUtility.GetAsciiBytes(msgFrame); + var lrcAscii = ModbusUtility.GetAsciiBytes(ModbusUtility.CalculateLrc(msgFrame)); + var nlAscii = Encoding.UTF8.GetBytes(Modbus.NewLine.ToCharArray()); + + var frame = new MemoryStream(1 + msgFrameAscii.Length + lrcAscii.Length + nlAscii.Length); + frame.WriteByte((byte)':'); + frame.Write(msgFrameAscii, 0, msgFrameAscii.Length); + frame.Write(lrcAscii, 0, lrcAscii.Length); + frame.Write(nlAscii, 0, nlAscii.Length); + + return frame.ToArray(); + } + + internal override bool ChecksumsMatch(IModbusMessage message, byte[] messageFrame) + { + return ModbusUtility.CalculateLrc(message.MessageFrame) == messageFrame[messageFrame.Length - 1]; + } + + internal override byte[] ReadRequest() + { + return ReadRequestResponse(); + } + + internal override IModbusMessage ReadResponse() + { + return CreateResponse(ReadRequestResponse()); + } + + internal byte[] ReadRequestResponse() + { + // read message frame, removing frame start ':' + string frameHex = StreamResourceUtility.ReadLine(StreamResource).Substring(1); + + // convert hex to bytes + byte[] frame = ModbusUtility.HexToBytes(frameHex); + Debug.WriteLine($"RX: {string.Join(", ", frame)}"); + + if (frame.Length < 3) + { + throw new IOException("Premature end of stream, message truncated."); + } + + return frame; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusIpTransport.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusIpTransport.cs new file mode 100644 index 0000000..c1346d1 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusIpTransport.cs @@ -0,0 +1,163 @@ +namespace Modbus.IO +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + + using Message; + + using Unme.Common; + + /// + /// Transport for Internet protocols. + /// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + internal class ModbusIpTransport : ModbusTransport + { + private static readonly object _transactionIdLock = new object(); + private ushort _transactionId; + + internal ModbusIpTransport(IStreamResource streamResource) + : base(streamResource) + { + Debug.Assert(streamResource != null, "Argument streamResource cannot be null."); + } + + internal static byte[] ReadRequestResponse(IStreamResource streamResource) + { + // read header + var mbapHeader = new byte[6]; + int numBytesRead = 0; + + while (numBytesRead != 6) + { + int bRead = streamResource.Read(mbapHeader, numBytesRead, 6 - numBytesRead); + + if (bRead == 0) + { + throw new IOException("Read resulted in 0 bytes returned."); + } + + numBytesRead += bRead; + } + + Debug.WriteLine($"MBAP header: {string.Join(", ", mbapHeader)}"); + var frameLength = (ushort)IPAddress.HostToNetworkOrder(BitConverter.ToInt16(mbapHeader, 4)); + Debug.WriteLine($"{frameLength} bytes in PDU."); + + // read message + var messageFrame = new byte[frameLength]; + numBytesRead = 0; + + while (numBytesRead != frameLength) + { + int bRead = streamResource.Read(messageFrame, numBytesRead, frameLength - numBytesRead); + + if (bRead == 0) + { + throw new IOException("Read resulted in 0 bytes returned."); + } + + numBytesRead += bRead; + } + + Debug.WriteLine($"PDU: {frameLength}"); + var frame = mbapHeader.Concat(messageFrame).ToArray(); + Debug.WriteLine($"RX: {string.Join(", ", frame)}"); + + return frame; + } + + internal static byte[] GetMbapHeader(IModbusMessage message) + { + byte[] transactionId = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)message.TransactionId)); + byte[] length = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)(message.ProtocolDataUnit.Length + 1))); + + var stream = new MemoryStream(7); + stream.Write(transactionId, 0, transactionId.Length); + stream.WriteByte(0); + stream.WriteByte(0); + stream.Write(length, 0, length.Length); + stream.WriteByte(message.SlaveAddress); + + return stream.ToArray(); + } + + /// + /// Create a new transaction ID. + /// + internal virtual ushort GetNewTransactionId() + { + lock (_transactionIdLock) + { + _transactionId = _transactionId == ushort.MaxValue ? (ushort)1 : ++_transactionId; + } + + return _transactionId; + } + + internal IModbusMessage CreateMessageAndInitializeTransactionId(byte[] fullFrame) + where T : IModbusMessage, new() + { + byte[] mbapHeader = fullFrame.Slice(0, 6).ToArray(); + byte[] messageFrame = fullFrame.Slice(6, fullFrame.Length - 6).ToArray(); + + IModbusMessage response = CreateResponse(messageFrame); + response.TransactionId = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(mbapHeader, 0)); + + return response; + } + + internal override byte[] BuildMessageFrame(IModbusMessage message) + { + byte[] header = GetMbapHeader(message); + byte[] pdu = message.ProtocolDataUnit; + var messageBody = new MemoryStream(header.Length + pdu.Length); + + messageBody.Write(header, 0, header.Length); + messageBody.Write(pdu, 0, pdu.Length); + + return messageBody.ToArray(); + } + + internal override void Write(IModbusMessage message) + { + message.TransactionId = GetNewTransactionId(); + byte[] frame = BuildMessageFrame(message); + Debug.WriteLine($"TX: {string.Join(", ", frame)}"); + StreamResource.Write(frame, 0, frame.Length); + } + + internal override byte[] ReadRequest() + { + return ReadRequestResponse(StreamResource); + } + + internal override IModbusMessage ReadResponse() + { + return CreateMessageAndInitializeTransactionId(ReadRequestResponse(StreamResource)); + } + + internal override void OnValidateResponse(IModbusMessage request, IModbusMessage response) + { + if (request.TransactionId != response.TransactionId) + { + string msg = $"Response was not of expected transaction ID. Expected {request.TransactionId}, received {response.TransactionId}."; + throw new IOException(msg); + } + } + + internal override bool OnShouldRetryResponse(IModbusMessage request, IModbusMessage response) + { + if (request.TransactionId > response.TransactionId && request.TransactionId - response.TransactionId < RetryOnOldResponseThreshold) + { + // This response was from a previous request + return true; + } + + return base.OnShouldRetryResponse(request, response); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusRtuTransport.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusRtuTransport.cs new file mode 100644 index 0000000..2318b83 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusRtuTransport.cs @@ -0,0 +1,142 @@ +namespace Modbus.IO +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + + using Message; + using Utility; + + /// + /// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + internal class ModbusRtuTransport : ModbusSerialTransport + { + public const int RequestFrameStartLength = 7; + + public const int ResponseFrameStartLength = 4; + + internal ModbusRtuTransport(IStreamResource streamResource) + : base(streamResource) + { + Debug.Assert(streamResource != null, "Argument streamResource cannot be null."); + } + + public static int RequestBytesToRead(byte[] frameStart) + { + byte functionCode = frameStart[1]; + int numBytes; + + switch (functionCode) + { + case Modbus.ReadCoils: + case Modbus.ReadInputs: + case Modbus.ReadHoldingRegisters: + case Modbus.ReadInputRegisters: + case Modbus.WriteSingleCoil: + case Modbus.WriteSingleRegister: + case Modbus.Diagnostics: + numBytes = 1; + break; + case Modbus.WriteMultipleCoils: + case Modbus.WriteMultipleRegisters: + byte byteCount = frameStart[6]; + numBytes = byteCount + 2; + break; + default: + string msg = $"Function code {functionCode} not supported."; + Debug.WriteLine(msg); + throw new NotImplementedException(msg); + } + + return numBytes; + } + + public static int ResponseBytesToRead(byte[] frameStart) + { + byte functionCode = frameStart[1]; + + // exception response + if (functionCode > Modbus.ExceptionOffset) + { + return 1; + } + + int numBytes; + switch (functionCode) + { + case Modbus.ReadCoils: + case Modbus.ReadInputs: + case Modbus.ReadHoldingRegisters: + case Modbus.ReadInputRegisters: + numBytes = frameStart[2] + 1; + break; + case Modbus.WriteSingleCoil: + case Modbus.WriteSingleRegister: + case Modbus.WriteMultipleCoils: + case Modbus.WriteMultipleRegisters: + case Modbus.Diagnostics: + numBytes = 4; + break; + default: + string msg = $"Function code {functionCode} not supported."; + Debug.WriteLine(msg); + throw new NotImplementedException(msg); + } + + return numBytes; + } + + public virtual byte[] Read(int count) + { + byte[] frameBytes = new byte[count]; + int numBytesRead = 0; + + while (numBytesRead != count) + { + numBytesRead += StreamResource.Read(frameBytes, numBytesRead, count - numBytesRead); + } + + return frameBytes; + } + + internal override byte[] BuildMessageFrame(IModbusMessage message) + { + var messageFrame = message.MessageFrame; + var crc = ModbusUtility.CalculateCrc(messageFrame); + var messageBody = new MemoryStream(messageFrame.Length + crc.Length); + + messageBody.Write(messageFrame, 0, messageFrame.Length); + messageBody.Write(crc, 0, crc.Length); + + return messageBody.ToArray(); + } + + internal override bool ChecksumsMatch(IModbusMessage message, byte[] messageFrame) + { + return BitConverter.ToUInt16(messageFrame, messageFrame.Length - 2) == + BitConverter.ToUInt16(ModbusUtility.CalculateCrc(message.MessageFrame), 0); + } + + internal override IModbusMessage ReadResponse() + { + byte[] frameStart = Read(ResponseFrameStartLength); + byte[] frameEnd = Read(ResponseBytesToRead(frameStart)); + byte[] frame = Enumerable.Concat(frameStart, frameEnd).ToArray(); + Debug.WriteLine($"RX: {string.Join(", ", frame)}"); + + return CreateResponse(frame); + } + + internal override byte[] ReadRequest() + { + byte[] frameStart = Read(RequestFrameStartLength); + byte[] frameEnd = Read(RequestBytesToRead(frameStart)); + byte[] frame = Enumerable.Concat(frameStart, frameEnd).ToArray(); + Debug.WriteLine($"RX: {string.Join(", ", frame)}"); + + return frame; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusSerialTransport.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusSerialTransport.cs new file mode 100644 index 0000000..bdbe997 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusSerialTransport.cs @@ -0,0 +1,67 @@ +namespace Modbus.IO +{ + using System.Diagnostics; + using System.IO; + + using Message; + + /// + /// Transport for Serial protocols. + /// Refined Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + public abstract class ModbusSerialTransport : ModbusTransport + { + private bool _checkFrame = true; + + internal ModbusSerialTransport(IStreamResource streamResource) + : base(streamResource) + { + Debug.Assert(streamResource != null, "Argument streamResource cannot be null."); + } + + /// + /// Gets or sets a value indicating whether LRC/CRC frame checking is performed on messages. + /// + public bool CheckFrame + { + get { return _checkFrame; } + set { _checkFrame = value; } + } + + internal void DiscardInBuffer() + { + StreamResource.DiscardInBuffer(); + } + + internal override void Write(IModbusMessage message) + { + DiscardInBuffer(); + + byte[] frame = BuildMessageFrame(message); + Debug.WriteLine($"TX: {string.Join(", ", frame)}"); + StreamResource.Write(frame, 0, frame.Length); + } + + internal override IModbusMessage CreateResponse(byte[] frame) + { + IModbusMessage response = base.CreateResponse(frame); + + // compare checksum + if (CheckFrame && !ChecksumsMatch(response, frame)) + { + string msg = $"Checksums failed to match {string.Join(", ", response.MessageFrame)} != {string.Join(", ", frame)}"; + Debug.WriteLine(msg); + throw new IOException(msg); + } + + return response; + } + + internal abstract bool ChecksumsMatch(IModbusMessage message, byte[] messageFrame); + + internal override void OnValidateResponse(IModbusMessage request, IModbusMessage response) + { + // no-op + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusTransport.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusTransport.cs new file mode 100644 index 0000000..26ecf0c --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/ModbusTransport.cs @@ -0,0 +1,310 @@ +namespace Modbus.IO +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Threading.Tasks; + + using Message; + + using Unme.Common; + + /// + /// Modbus transport. + /// Abstraction - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + public abstract class ModbusTransport : IDisposable + { + private readonly object _syncLock = new object(); + private int _retries = Modbus.DefaultRetries; + private int _waitToRetryMilliseconds = Modbus.DefaultWaitToRetryMilliseconds; + private IStreamResource _streamResource; + + /// + /// This constructor is called by the NullTransport. + /// + internal ModbusTransport() + { + } + + internal ModbusTransport(IStreamResource streamResource) + { + Debug.Assert(streamResource != null, "Argument streamResource cannot be null."); + + _streamResource = streamResource; + } + + /// + /// Number of times to retry sending message after encountering a failure such as an IOException, + /// TimeoutException, or a corrupt message. + /// + public int Retries + { + get { return _retries; } + set { _retries = value; } + } + + /// + /// If non-zero, this will cause a second reply to be read if the first is behind the sequence number of the + /// request by less than this number. For example, set this to 3, and if when sending request 5, response 3 is + /// read, we will attempt to re-read responses. + /// + public uint RetryOnOldResponseThreshold { get; set; } + + /// + /// If set, Slave Busy exception causes retry count to be used. If false, Slave Busy will cause infinite retries + /// + public bool SlaveBusyUsesRetryCount { get; set; } + + /// + /// Gets or sets the number of milliseconds the tranport will wait before retrying a message after receiving + /// an ACKNOWLEGE or SLAVE DEVICE BUSY slave exception response. + /// + public int WaitToRetryMilliseconds + { + get + { + return _waitToRetryMilliseconds; + } + + set + { + if (value < 0) + { + throw new ArgumentException(Resources.WaitRetryGreaterThanZero); + } + + _waitToRetryMilliseconds = value; + } + } + + /// + /// Gets or sets the number of milliseconds before a timeout occurs when a read operation does not finish. + /// + public int ReadTimeout + { + get { return StreamResource.ReadTimeout; } + set { StreamResource.ReadTimeout = value; } + } + + /// + /// Gets or sets the number of milliseconds before a timeout occurs when a write operation does not finish. + /// + public int WriteTimeout + { + get { return StreamResource.WriteTimeout; } + set { StreamResource.WriteTimeout = value; } + } + + /// + /// Gets the stream resource. + /// + internal IStreamResource StreamResource + { + get { return _streamResource; } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + internal virtual T UnicastMessage(IModbusMessage message) + where T : IModbusMessage, new() + { + IModbusMessage response = null; + int attempt = 1; + bool success = false; + + do + { + try + { + lock (_syncLock) + { + Write(message); + + bool readAgain; + do + { + readAgain = false; + response = ReadResponse(); + var exceptionResponse = response as SlaveExceptionResponse; + + if (exceptionResponse != null) + { + // if SlaveExceptionCode == ACKNOWLEDGE we retry reading the response without resubmitting request + readAgain = exceptionResponse.SlaveExceptionCode == Modbus.Acknowledge; + + if (readAgain) + { + Debug.WriteLine($"Received ACKNOWLEDGE slave exception response, waiting {_waitToRetryMilliseconds} milliseconds and retrying to read response."); + Sleep(WaitToRetryMilliseconds); + } + else + { + throw new SlaveException(exceptionResponse); + } + } + else if (ShouldRetryResponse(message, response)) + { + readAgain = true; + } + } + while (readAgain); + } + + ValidateResponse(message, response); + success = true; + } + catch (SlaveException se) + { + if (se.SlaveExceptionCode != Modbus.SlaveDeviceBusy) + { + throw; + } + + if (SlaveBusyUsesRetryCount && attempt++ > _retries) + { + throw; + } + + Debug.WriteLine($"Received SLAVE_DEVICE_BUSY exception response, waiting {_waitToRetryMilliseconds} milliseconds and resubmitting request."); + Sleep(WaitToRetryMilliseconds); + } + catch (Exception e) + { + if (e is FormatException || + e is NotImplementedException || + e is TimeoutException || + e is IOException) + { + Debug.WriteLine($"{e.GetType().Name}, {(_retries - attempt + 1)} retries remaining - {e}"); + + if (attempt++ > _retries) + { + throw; + } + } + else + { + throw; + } + } + } + while (!success); + + return (T)response; + } + + internal virtual IModbusMessage CreateResponse(byte[] frame) + where T : IModbusMessage, new() + { + byte functionCode = frame[1]; + IModbusMessage response; + + // check for slave exception response else create message from frame + if (functionCode > Modbus.ExceptionOffset) + { + response = ModbusMessageFactory.CreateModbusMessage(frame); + } + else + { + response = ModbusMessageFactory.CreateModbusMessage(frame); + } + + return response; + } + + internal void ValidateResponse(IModbusMessage request, IModbusMessage response) + { + // always check the function code and slave address, regardless of transport protocol + if (request.FunctionCode != response.FunctionCode) + { + string msg = $"Received response with unexpected Function Code. Expected {request.FunctionCode}, received {response.FunctionCode}."; + throw new IOException(msg); + } + + if (request.SlaveAddress != response.SlaveAddress) + { + string msg = $"Response slave address does not match request. Expected {response.SlaveAddress}, received {request.SlaveAddress}."; + throw new IOException(msg); + } + + // message specific validation + var req = request as IModbusRequest; + + if (req != null) + { + req.ValidateResponse(response); + } + + OnValidateResponse(request, response); + } + + /// + /// Check whether we need to attempt to read another response before processing it (e.g. response was from previous request) + /// + internal bool ShouldRetryResponse(IModbusMessage request, IModbusMessage response) + { + // These checks are enforced in ValidateRequest, we don't want to retry for these + if (request.FunctionCode != response.FunctionCode) + { + return false; + } + + if (request.SlaveAddress != response.SlaveAddress) + { + return false; + } + + return OnShouldRetryResponse(request, response); + } + + /// + /// Provide hook to check whether receiving a response should be retried + /// + internal virtual bool OnShouldRetryResponse(IModbusMessage request, IModbusMessage response) + { + return false; + } + + /// + /// Provide hook to do transport level message validation. + /// + internal abstract void OnValidateResponse(IModbusMessage request, IModbusMessage response); + + internal abstract byte[] ReadRequest(); + + internal abstract IModbusMessage ReadResponse() + where T : IModbusMessage, new(); + + internal abstract byte[] BuildMessageFrame(IModbusMessage message); + + internal abstract void Write(IModbusMessage message); + + /// + /// Releases unmanaged and - optionally - managed resources + /// + /// + /// true to release both managed and unmanaged resources; false to release only + /// unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposableUtility.Dispose(ref _streamResource); + } + } + + private static void Sleep(int millisecondsTimeout) + { + Task.Delay(millisecondsTimeout).Wait(); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/StreamResourceUtility.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/StreamResourceUtility.cs new file mode 100644 index 0000000..b99473c --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/StreamResourceUtility.cs @@ -0,0 +1,27 @@ +namespace Modbus.IO +{ + using System.Linq; + using System.Text; + + internal static class StreamResourceUtility + { + internal static string ReadLine(IStreamResource stream) + { + var result = new StringBuilder(); + var singleByteBuffer = new byte[1]; + + do + { + if (stream.Read(singleByteBuffer, 0, 1) == 0) + { + continue; + } + + result.Append(Encoding.UTF8.GetChars(singleByteBuffer).First()); + } + while (!result.ToString().EndsWith(Modbus.NewLine)); + + return result.ToString().Substring(0, result.Length - Modbus.NewLine.Length); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/TcpClientAdapter.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/TcpClientAdapter.cs new file mode 100644 index 0000000..ee6e8a8 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/TcpClientAdapter.cs @@ -0,0 +1,70 @@ +namespace Modbus.IO +{ + using System; + using System.Diagnostics; + using System.Net.Sockets; + using System.Threading; + + using Unme.Common; + + /// + /// Concrete Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + internal class TcpClientAdapter : IStreamResource + { + private TcpClient _tcpClient; + + public TcpClientAdapter(TcpClient tcpClient) + { + Debug.Assert(tcpClient != null, "Argument tcpClient cannot be null."); + + _tcpClient = tcpClient; + } + + public int InfiniteTimeout + { + get { return Timeout.Infinite; } + } + + public int ReadTimeout + { + get { return _tcpClient.GetStream().ReadTimeout; } + set { _tcpClient.GetStream().ReadTimeout = value; } + } + + public int WriteTimeout + { + get { return _tcpClient.GetStream().WriteTimeout; } + set { _tcpClient.GetStream().WriteTimeout = value; } + } + + public void Write(byte[] buffer, int offset, int size) + { + _tcpClient.GetStream().Write(buffer, offset, size); + } + + public int Read(byte[] buffer, int offset, int size) + { + return _tcpClient.GetStream().Read(buffer, offset, size); + } + + public void DiscardInBuffer() + { + _tcpClient.GetStream().Flush(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposableUtility.Dispose(ref _tcpClient); + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/UdpClientAdapter.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/UdpClientAdapter.cs new file mode 100644 index 0000000..d0b4ba2 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/IO/UdpClientAdapter.cs @@ -0,0 +1,158 @@ +namespace Modbus.IO +{ + using System; + using System.IO; + using System.Linq; + using System.Net.Sockets; + using System.Threading; + + using Unme.Common; + + /// + /// Concrete Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + internal class UdpClientAdapter : IStreamResource + { + // strategy for cross platform r/w + private const int MaxBufferSize = ushort.MaxValue; + private UdpClient _udpClient; + private readonly byte[] _buffer = new byte[MaxBufferSize]; + private int _bufferOffset; + + public UdpClientAdapter(UdpClient udpClient) + { + if (udpClient == null) + { + throw new ArgumentNullException(nameof(udpClient)); + } + + _udpClient = udpClient; + } + + public int InfiniteTimeout + { + get { return Timeout.Infinite; } + } + + public int ReadTimeout + { + get { return _udpClient.Client.ReceiveTimeout; } + set { _udpClient.Client.ReceiveTimeout = value; } + } + + public int WriteTimeout + { + get { return _udpClient.Client.SendTimeout; } + set { _udpClient.Client.SendTimeout = value; } + } + + public void DiscardInBuffer() + { + // no-op + } + + public int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + "Argument offset must be greater than or equal to 0."); + } + + if (offset > buffer.Length) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + "Argument offset cannot be greater than the length of buffer."); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException( + nameof(count), + "Argument count must be greater than or equal to 0."); + } + + if (count > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException( + nameof(count), + "Argument count cannot be greater than the length of buffer minus offset."); + } + + if (_bufferOffset == 0) + { + _bufferOffset = _udpClient.Client.Receive(_buffer); + } + + if (_bufferOffset < count) + { + throw new IOException("Not enough bytes in the datagram."); + } + + Buffer.BlockCopy(_buffer, 0, buffer, offset, count); + _bufferOffset -= count; + Buffer.BlockCopy(_buffer, count, _buffer, 0, _bufferOffset); + + return count; + } + + public void Write(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + "Argument offset must be greater than or equal to 0."); + } + + if (offset > buffer.Length) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + "Argument offset cannot be greater than the length of buffer."); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException( + nameof(count), + "Argument count must be greater than or equal to 0."); + } + + if (count > buffer.Length - offset) + { + throw new ArgumentOutOfRangeException( + nameof(count), + "Argument count cannot be greater than the length of buffer minus offset."); + } + + _udpClient.Client.Send(buffer.Skip(offset).Take(count).ToArray()); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + DisposableUtility.Dispose(ref _udpClient); + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/InvalidModbusRequestException.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/InvalidModbusRequestException.cs new file mode 100644 index 0000000..b4c0528 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/InvalidModbusRequestException.cs @@ -0,0 +1,95 @@ +namespace Modbus +{ + using System; +#if NET46 + using System.Runtime.Serialization; +#endif + /// + /// An exception that provides the exception code that will be sent in response to an invalid Modbus request. + /// +#if NET46 + [Serializable] +#endif + public class InvalidModbusRequestException : Exception + { + private readonly byte _exceptionCode; + + /// + /// Initializes a new instance of the class with a specified Modbus exception code. + /// + /// The Modbus exception code to provide to the slave. + public InvalidModbusRequestException(byte exceptionCode) + : this(GetMessage(exceptionCode), exceptionCode) + { + } + + /// + /// Initializes a new instance of the class with a specified error message and Modbus exception code. + /// + /// The error message that explains the reason for the exception. + /// The Modbus exception code to provide to the slave. + public InvalidModbusRequestException(string message, byte exceptionCode) + : this(message, exceptionCode, null) + { + } + + /// + /// Initializes a new instance of the class with a specified Modbus exception code and a reference to the inner exception that is the cause of this exception. + /// + /// The Modbus exception code to provide to the slave. + /// The exception that is the cause of the current exception. If the parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception. + public InvalidModbusRequestException(byte exceptionCode, Exception innerException) + : this(GetMessage(exceptionCode), exceptionCode, innerException) + { + } + + /// + /// Initializes a new instance of the class with a specified Modbus exception code and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The Modbus exception code to provide to the slave. + /// The exception that is the cause of the current exception. If the parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception. + public InvalidModbusRequestException(string message, byte exceptionCode, Exception innerException) + : base(message, innerException) + { + _exceptionCode = exceptionCode; + } + +#if NET46 + /// + /// Initializes a new instance of the class with serialized data. + /// + /// The object that holds the serialized object data. + /// The contextual information about the source or destination. + protected InvalidModbusRequestException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _exceptionCode = info.GetByte(nameof(ExceptionCode)); + } +#endif + + /// + /// Gets the Modbus exception code to provide to the slave. + /// + public byte ExceptionCode + { + get { return _exceptionCode; } + } + +#if NET46 + /// Sets the object with the Modbus exception code and additional exception information. + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("ExceptionCode", this._exceptionCode, typeof(byte)); + } +#endif + + private static string GetMessage(byte exceptionCode) + { + return $"Modbus exception code {exceptionCode}."; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessage.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessage.cs new file mode 100644 index 0000000..0af394b --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessage.cs @@ -0,0 +1,77 @@ +namespace Modbus.Message +{ + using System; + + /// + /// Abstract Modbus message. + /// + public abstract class AbstractModbusMessage + { + private readonly ModbusMessageImpl _messageImpl; + + /// + /// Abstract Modbus message. + /// + internal AbstractModbusMessage() + { + _messageImpl = new ModbusMessageImpl(); + } + + /// + /// Abstract Modbus message. + /// + internal AbstractModbusMessage(byte slaveAddress, byte functionCode) + { + _messageImpl = new ModbusMessageImpl(slaveAddress, functionCode); + } + + public ushort TransactionId + { + get { return _messageImpl.TransactionId; } + set { _messageImpl.TransactionId = value; } + } + + public byte FunctionCode + { + get { return _messageImpl.FunctionCode; } + set { _messageImpl.FunctionCode = value; } + } + + public byte SlaveAddress + { + get { return _messageImpl.SlaveAddress; } + set { _messageImpl.SlaveAddress = value; } + } + + public byte[] MessageFrame + { + get { return _messageImpl.MessageFrame; } + } + + public virtual byte[] ProtocolDataUnit + { + get { return _messageImpl.ProtocolDataUnit; } + } + + public abstract int MinimumFrameSize { get; } + + internal ModbusMessageImpl MessageImpl + { + get { return _messageImpl; } + } + + public void Initialize(byte[] frame) + { + if (frame.Length < MinimumFrameSize) + { + string msg = $"Message frame must contain at least {MinimumFrameSize} bytes of data."; + throw new FormatException(msg); + } + + _messageImpl.Initialize(frame); + InitializeUnique(frame); + } + + protected abstract void InitializeUnique(byte[] frame); + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessageWithData.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessageWithData.cs new file mode 100644 index 0000000..d627730 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/AbstractModbusMessageWithData.cs @@ -0,0 +1,23 @@ +namespace Modbus.Message +{ + using Data; + + public abstract class AbstractModbusMessageWithData : AbstractModbusMessage + where TData : IModbusMessageDataCollection + { + internal AbstractModbusMessageWithData() + { + } + + internal AbstractModbusMessageWithData(byte slaveAddress, byte functionCode) + : base(slaveAddress, functionCode) + { + } + + public TData Data + { + get { return (TData)MessageImpl.Data; } + set { MessageImpl.Data = value; } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/DiagnosticsRequestResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/DiagnosticsRequestResponse.cs new file mode 100644 index 0000000..2b74c4a --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/DiagnosticsRequestResponse.cs @@ -0,0 +1,53 @@ +namespace Modbus.Message +{ + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Net; + + using Data; + + using Unme.Common; + + internal class DiagnosticsRequestResponse : AbstractModbusMessageWithData, IModbusMessage + { + public DiagnosticsRequestResponse() + { + } + + public DiagnosticsRequestResponse(ushort subFunctionCode, byte slaveAddress, RegisterCollection data) + : base(slaveAddress, Modbus.Diagnostics) + { + SubFunctionCode = subFunctionCode; + Data = data; + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "May implement addtional sub function codes in the future.")] + public ushort SubFunctionCode + { + get { return MessageImpl.SubFunctionCode.Value; } + set { MessageImpl.SubFunctionCode = value; } + } + + public override string ToString() + { + Debug.Assert( + SubFunctionCode == Modbus.DiagnosticsReturnQueryData, + "Need to add support for additional sub-function."); + + return $"Diagnostics message, sub-function return query data - {Data}."; + } + + protected override void InitializeUnique(byte[] frame) + { + SubFunctionCode = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + Data = new RegisterCollection(frame.Slice(4, 2).ToArray()); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusMessage.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusMessage.cs new file mode 100644 index 0000000..87540d9 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusMessage.cs @@ -0,0 +1,43 @@ +namespace Modbus.Message +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// A message built by the master (client) that initiates a Modbus transaction. + /// + public interface IModbusMessage + { + /// + /// The function code tells the server what kind of action to perform. + /// + byte FunctionCode { get; set; } + + /// + /// Address of the slave (server). + /// + byte SlaveAddress { get; set; } + + /// + /// Composition of the slave address and protocol data unit. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + byte[] MessageFrame { get; } + + /// + /// Composition of the function code and message data. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + byte[] ProtocolDataUnit { get; } + + /// + /// A unique identifier assigned to a message when using the IP protocol. + /// + ushort TransactionId { get; set; } + + /// + /// Initializes a modbus message from the specified message frame. + /// + /// Bytes of Modbus frame. + void Initialize(byte[] frame); + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusRequest.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusRequest.cs new file mode 100644 index 0000000..237e58b --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/IModbusRequest.cs @@ -0,0 +1,13 @@ +namespace Modbus.Message +{ + /// + /// Methods specific to a modbus request message. + /// + public interface IModbusRequest : IModbusMessage + { + /// + /// Validate the specified response against the current request. + /// + void ValidateResponse(IModbusMessage response); + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageFactory.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageFactory.cs new file mode 100644 index 0000000..4e6eff3 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageFactory.cs @@ -0,0 +1,82 @@ +namespace Modbus.Message +{ + using System; + + /// + /// Modbus message factory. + /// + public static class ModbusMessageFactory + { + /// + /// Minimum request frame length. + /// + private const int MinRequestFrameLength = 3; + + /// + /// Create a Modbus message. + /// + /// Modbus message type. + /// Bytes of Modbus frame. + /// New Modbus message based on type and frame bytes. + public static T CreateModbusMessage(byte[] frame) + where T : IModbusMessage, new() + { + IModbusMessage message = new T(); + message.Initialize(frame); + + return (T)message; + } + + /// + /// Create a Modbus request. + /// + /// Bytes of Modbus frame. + /// Modbus request. + public static IModbusMessage CreateModbusRequest(byte[] frame) + { + if (frame.Length < MinRequestFrameLength) + { + string msg = $"Argument 'frame' must have a length of at least {MinRequestFrameLength} bytes."; + throw new FormatException(msg); + } + + IModbusMessage request; + byte functionCode = frame[1]; + + switch (functionCode) + { + case Modbus.ReadCoils: + case Modbus.ReadInputs: + request = CreateModbusMessage(frame); + break; + case Modbus.ReadHoldingRegisters: + case Modbus.ReadInputRegisters: + request = CreateModbusMessage(frame); + break; + case Modbus.WriteSingleCoil: + request = CreateModbusMessage(frame); + break; + case Modbus.WriteSingleRegister: + request = CreateModbusMessage(frame); + break; + case Modbus.Diagnostics: + request = CreateModbusMessage(frame); + break; + case Modbus.WriteMultipleCoils: + request = CreateModbusMessage(frame); + break; + case Modbus.WriteMultipleRegisters: + request = CreateModbusMessage(frame); + break; + case Modbus.ReadWriteMultipleRegisters: + request = CreateModbusMessage(frame); + break; + default: + string msg = $"Unsupported function code {functionCode}"; + throw new ArgumentException(msg, nameof(frame)); + } + + return request; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageImpl.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageImpl.cs new file mode 100644 index 0000000..28166b5 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ModbusMessageImpl.cs @@ -0,0 +1,117 @@ +namespace Modbus.Message +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + + using Data; + + /// + /// Class holding all implementation shared between two or more message types. + /// Interfaces expose subsets of type specific implementations. + /// + internal class ModbusMessageImpl + { + public ModbusMessageImpl() + { + } + + public ModbusMessageImpl(byte slaveAddress, byte functionCode) + { + SlaveAddress = slaveAddress; + FunctionCode = functionCode; + } + + public byte? ByteCount { get; set; } + + public byte? ExceptionCode { get; set; } + + public ushort TransactionId { get; set; } + + public byte FunctionCode { get; set; } + + public ushort? NumberOfPoints { get; set; } + + public byte SlaveAddress { get; set; } + + public ushort? StartAddress { get; set; } + + public ushort? SubFunctionCode { get; set; } + + public IModbusMessageDataCollection Data { get; set; } + + public byte[] MessageFrame + { + get + { + var pdu = ProtocolDataUnit; + var frame = new MemoryStream(1 + pdu.Length); + + frame.WriteByte(SlaveAddress); + frame.Write(pdu, 0, pdu.Length); + + return frame.ToArray(); + } + } + + public byte[] ProtocolDataUnit + { + get + { + List pdu = new List(); + + pdu.Add(FunctionCode); + + if (ExceptionCode.HasValue) + { + pdu.Add(ExceptionCode.Value); + } + + if (SubFunctionCode.HasValue) + { + pdu.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)SubFunctionCode.Value))); + } + + if (StartAddress.HasValue) + { + pdu.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)StartAddress.Value))); + } + + if (NumberOfPoints.HasValue) + { + pdu.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)NumberOfPoints.Value))); + } + + if (ByteCount.HasValue) + { + pdu.Add(ByteCount.Value); + } + + if (Data != null) + { + pdu.AddRange(Data.NetworkBytes); + } + + return pdu.ToArray(); + } + } + + public void Initialize(byte[] frame) + { + if (frame == null) + { + throw new ArgumentNullException(nameof(frame), "Argument frame cannot be null."); + } + + if (frame.Length < Modbus.MinimumFrameSize) + { + string msg = $"Message frame must contain at least {Modbus.MinimumFrameSize} bytes of data."; + throw new FormatException(msg); + } + + SlaveAddress = frame[0]; + FunctionCode = frame[1]; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsRequest.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsRequest.cs new file mode 100644 index 0000000..7ccc4de --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsRequest.cs @@ -0,0 +1,76 @@ +namespace Modbus.Message +{ + using System; + using System.IO; + using System.Net; + + public class ReadCoilsInputsRequest : AbstractModbusMessage, IModbusRequest + { + public ReadCoilsInputsRequest() + { + } + + public ReadCoilsInputsRequest(byte functionCode, byte slaveAddress, ushort startAddress, ushort numberOfPoints) + : base(slaveAddress, functionCode) + { + StartAddress = startAddress; + NumberOfPoints = numberOfPoints; + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + public ushort NumberOfPoints + { + get + { + return MessageImpl.NumberOfPoints.Value; + } + + set + { + if (value > Modbus.MaximumDiscreteRequestResponseSize) + { + string msg = $"Maximum amount of data {Modbus.MaximumDiscreteRequestResponseSize} coils."; + throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg); + } + + MessageImpl.NumberOfPoints = value; + } + } + + public override string ToString() + { + string msg = $"Read {NumberOfPoints} {(FunctionCode == Modbus.ReadCoils ? "coils" : "inputs")} starting at address {StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = (ReadCoilsInputsResponse)response; + + // best effort validation - the same response for a request for 1 vs 6 coils (same byte count) will pass validation. + var expectedByteCount = (NumberOfPoints + 7) / 8; + + if (expectedByteCount != typedResponse.ByteCount) + { + string msg = $"Unexpected byte count. Expected {expectedByteCount}, received {typedResponse.ByteCount}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsResponse.cs new file mode 100644 index 0000000..c6d83a5 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadCoilsInputsResponse.cs @@ -0,0 +1,50 @@ +namespace Modbus.Message +{ + using System; + using System.Linq; + using Data; + + using Unme.Common; + + public class ReadCoilsInputsResponse : AbstractModbusMessageWithData, IModbusMessage + { + public ReadCoilsInputsResponse() + { + } + + public ReadCoilsInputsResponse(byte functionCode, byte slaveAddress, byte byteCount, DiscreteCollection data) + : base(slaveAddress, functionCode) + { + ByteCount = byteCount; + Data = data; + } + + public byte ByteCount + { + get { return MessageImpl.ByteCount.Value; } + set { MessageImpl.ByteCount = value; } + } + + public override int MinimumFrameSize + { + get { return 3; } + } + + public override string ToString() + { + string msg = $"Read {Data.Count()} {(FunctionCode == Modbus.ReadInputs ? "inputs" : "coils")} - {Data}."; + return msg; + } + + protected override void InitializeUnique(byte[] frame) + { + if (frame.Length < 3 + frame[2]) + { + throw new FormatException("Message frame data segment does not contain enough bytes."); + } + + ByteCount = frame[2]; + Data = new DiscreteCollection(frame.Slice(3, ByteCount).ToArray()); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersRequest.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersRequest.cs new file mode 100644 index 0000000..ff2922d --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersRequest.cs @@ -0,0 +1,76 @@ +namespace Modbus.Message +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Net; + + public class ReadHoldingInputRegistersRequest : AbstractModbusMessage, IModbusRequest + { + public ReadHoldingInputRegistersRequest() + { + } + + public ReadHoldingInputRegistersRequest(byte functionCode, byte slaveAddress, ushort startAddress, ushort numberOfPoints) + : base(slaveAddress, functionCode) + { + StartAddress = startAddress; + NumberOfPoints = numberOfPoints; + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + public ushort NumberOfPoints + { + get + { + return MessageImpl.NumberOfPoints.Value; + } + + set + { + if (value > Modbus.MaximumRegisterRequestResponseSize) + { + string msg = $"Maximum amount of data {Modbus.MaximumRegisterRequestResponseSize} registers."; + throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg); + } + + MessageImpl.NumberOfPoints = value; + } + } + + public override string ToString() + { + string msg = $"Read {NumberOfPoints} {(FunctionCode == Modbus.ReadHoldingRegisters ? "holding" : "input")} registers starting at address {StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = response as ReadHoldingInputRegistersResponse; + Debug.Assert(typedResponse != null, "Argument response should be of type ReadHoldingInputRegistersResponse."); + var expectedByteCount = NumberOfPoints * 2; + + if (expectedByteCount != typedResponse.ByteCount) + { + string msg = $"Unexpected byte count. Expected {expectedByteCount}, received {typedResponse.ByteCount}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersResponse.cs new file mode 100644 index 0000000..b61eea6 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadHoldingInputRegistersResponse.cs @@ -0,0 +1,56 @@ +namespace Modbus.Message +{ + using System; + using System.Linq; + + using Data; + + using Unme.Common; + + public class ReadHoldingInputRegistersResponse : AbstractModbusMessageWithData, IModbusMessage + { + public ReadHoldingInputRegistersResponse() + { + } + + public ReadHoldingInputRegistersResponse(byte functionCode, byte slaveAddress, RegisterCollection data) + : base(slaveAddress, functionCode) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + ByteCount = data.ByteCount; + Data = data; + } + + public byte ByteCount + { + get { return MessageImpl.ByteCount.Value; } + set { MessageImpl.ByteCount = value; } + } + + public override int MinimumFrameSize + { + get { return 3; } + } + + public override string ToString() + { + string msg = $"Read {Data.Count} {(FunctionCode == Modbus.ReadHoldingRegisters ? "holding" : "input")} registers."; + return msg; + } + + protected override void InitializeUnique(byte[] frame) + { + if (frame.Length < MinimumFrameSize + frame[2]) + { + throw new FormatException("Message frame does not contain enough bytes."); + } + + ByteCount = frame[2]; + Data = new RegisterCollection(frame.Slice(3, ByteCount).ToArray()); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs new file mode 100644 index 0000000..c2a7d63 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/ReadWriteMultipleRegistersRequest.cs @@ -0,0 +1,108 @@ +namespace Modbus.Message +{ + using System; + using System.IO; + + using Data; + + public class ReadWriteMultipleRegistersRequest : AbstractModbusMessage, IModbusRequest + { + private ReadHoldingInputRegistersRequest _readRequest; + private WriteMultipleRegistersRequest _writeRequest; + + public ReadWriteMultipleRegistersRequest() + { + } + + public ReadWriteMultipleRegistersRequest( + byte slaveAddress, + ushort startReadAddress, + ushort numberOfPointsToRead, + ushort startWriteAddress, + RegisterCollection writeData) + : base(slaveAddress, Modbus.ReadWriteMultipleRegisters) + { + _readRequest = new ReadHoldingInputRegistersRequest( + Modbus.ReadHoldingRegisters, + slaveAddress, + startReadAddress, + numberOfPointsToRead); + + _writeRequest = new WriteMultipleRegistersRequest( + slaveAddress, + startWriteAddress, + writeData); + } + + public override byte[] ProtocolDataUnit + { + get + { + byte[] readPdu = _readRequest.ProtocolDataUnit; + byte[] writePdu = _writeRequest.ProtocolDataUnit; + var stream = new MemoryStream(readPdu.Length + writePdu.Length); + + stream.WriteByte(FunctionCode); + + // read and write PDUs without function codes + stream.Write(readPdu, 1, readPdu.Length - 1); + stream.Write(writePdu, 1, writePdu.Length - 1); + + return stream.ToArray(); + } + } + + public ReadHoldingInputRegistersRequest ReadRequest + { + get { return _readRequest; } + } + + public WriteMultipleRegistersRequest WriteRequest + { + get { return _writeRequest; } + } + + public override int MinimumFrameSize + { + get { return 11; } + } + + public override string ToString() + { + string msg = $"Write {_writeRequest.NumberOfPoints} holding registers starting at address {_writeRequest.StartAddress}, and read {_readRequest.NumberOfPoints} registers starting at address {_readRequest.StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = (ReadHoldingInputRegistersResponse)response; + var expectedByteCount = ReadRequest.NumberOfPoints * 2; + + if (expectedByteCount != typedResponse.ByteCount) + { + string msg = $"Unexpected byte count in response. Expected {expectedByteCount}, received {typedResponse.ByteCount}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + if (frame.Length < MinimumFrameSize + frame[10]) + { + throw new FormatException("Message frame does not contain enough bytes."); + } + + byte[] readFrame = new byte[2 + 4]; + byte[] writeFrame = new byte[frame.Length - 6 + 2]; + + readFrame[0] = writeFrame[0] = SlaveAddress; + readFrame[1] = writeFrame[1] = FunctionCode; + + Buffer.BlockCopy(frame, 2, readFrame, 2, 4); + Buffer.BlockCopy(frame, 6, writeFrame, 2, frame.Length - 6); + + _readRequest = ModbusMessageFactory.CreateModbusMessage(readFrame); + _writeRequest = ModbusMessageFactory.CreateModbusMessage(writeFrame); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/SlaveExceptionResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/SlaveExceptionResponse.cs new file mode 100644 index 0000000..a499861 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/SlaveExceptionResponse.cs @@ -0,0 +1,80 @@ +namespace Modbus.Message +{ + using System; + using System.Collections.Generic; + using System.Globalization; + + public class SlaveExceptionResponse : AbstractModbusMessage, IModbusMessage + { + private static readonly Dictionary _exceptionMessages = CreateExceptionMessages(); + + public SlaveExceptionResponse() + { + } + + public SlaveExceptionResponse(byte slaveAddress, byte functionCode, byte exceptionCode) + : base(slaveAddress, functionCode) + { + SlaveExceptionCode = exceptionCode; + } + + public override int MinimumFrameSize + { + get { return 3; } + } + + public byte SlaveExceptionCode + { + get { return MessageImpl.ExceptionCode.Value; } + set { MessageImpl.ExceptionCode = value; } + } + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + string msg = _exceptionMessages.ContainsKey(SlaveExceptionCode) + ? _exceptionMessages[SlaveExceptionCode] + : Resources.Unknown; + + return string.Format( + CultureInfo.InvariantCulture, + Resources.SlaveExceptionResponseFormat, + Environment.NewLine, + FunctionCode, + SlaveExceptionCode, + msg); + } + + internal static Dictionary CreateExceptionMessages() + { + Dictionary messages = new Dictionary(9); + + messages.Add(1, Resources.IllegalFunction); + messages.Add(2, Resources.IllegalDataAddress); + messages.Add(3, Resources.IllegalDataValue); + messages.Add(4, Resources.SlaveDeviceFailure); + messages.Add(5, Resources.Acknowlege); + messages.Add(6, Resources.SlaveDeviceBusy); + messages.Add(8, Resources.MemoryParityError); + messages.Add(10, Resources.GatewayPathUnavailable); + messages.Add(11, Resources.GatewayTargetDeviceFailedToRespond); + + return messages; + } + + protected override void InitializeUnique(byte[] frame) + { + if (FunctionCode <= Modbus.ExceptionOffset) + { + throw new FormatException(Resources.SlaveExceptionResponseInvalidFunctionCode); + } + + SlaveExceptionCode = frame[2]; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsRequest.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsRequest.cs new file mode 100644 index 0000000..76fb7a1 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsRequest.cs @@ -0,0 +1,108 @@ +namespace Modbus.Message +{ + using System; + using System.IO; + using System.Linq; + using System.Net; + + using Data; + + using Unme.Common; + + /// + /// Write Multiple Coils request. + /// + public class WriteMultipleCoilsRequest : AbstractModbusMessageWithData, IModbusRequest + { + /// + /// Write Multiple Coils request. + /// + public WriteMultipleCoilsRequest() + { + } + + /// + /// Write Multiple Coils request. + /// + public WriteMultipleCoilsRequest(byte slaveAddress, ushort startAddress, DiscreteCollection data) + : base(slaveAddress, Modbus.WriteMultipleCoils) + { + StartAddress = startAddress; + NumberOfPoints = (ushort)data.Count; + ByteCount = (byte)((data.Count + 7) / 8); + Data = data; + } + + public byte ByteCount + { + get { return MessageImpl.ByteCount.Value; } + set { MessageImpl.ByteCount = value; } + } + + public ushort NumberOfPoints + { + get + { + return MessageImpl.NumberOfPoints.Value; + } + + set + { + if (value > Modbus.MaximumDiscreteRequestResponseSize) + { + string msg = $"Maximum amount of data {Modbus.MaximumDiscreteRequestResponseSize} coils."; + throw new ArgumentOutOfRangeException("NumberOfPoints", msg); + } + + MessageImpl.NumberOfPoints = value; + } + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override int MinimumFrameSize + { + get { return 7; } + } + + public override string ToString() + { + string msg = $"Write {NumberOfPoints} coils starting at address {StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = (WriteMultipleCoilsResponse)response; + + if (StartAddress != typedResponse.StartAddress) + { + string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}."; + throw new IOException(msg); + } + + if (NumberOfPoints != typedResponse.NumberOfPoints) + { + string msg = $"Unexpected number of points in response. Expected {NumberOfPoints}, received {typedResponse.NumberOfPoints}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + if (frame.Length < MinimumFrameSize + frame[6]) + { + throw new FormatException("Message frame does not contain enough bytes."); + } + + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)); + ByteCount = frame[6]; + Data = new DiscreteCollection(frame.Slice(7, ByteCount).ToArray()); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsResponse.cs new file mode 100644 index 0000000..ef0412d --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleCoilsResponse.cs @@ -0,0 +1,61 @@ +namespace Modbus.Message +{ + using System; + using System.Net; + + public class WriteMultipleCoilsResponse : AbstractModbusMessage, IModbusMessage + { + public WriteMultipleCoilsResponse() + { + } + + public WriteMultipleCoilsResponse(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + : base(slaveAddress, Modbus.WriteMultipleCoils) + { + StartAddress = startAddress; + NumberOfPoints = numberOfPoints; + } + + public ushort NumberOfPoints + { + get + { + return MessageImpl.NumberOfPoints.Value; + } + + set + { + if (value > Modbus.MaximumDiscreteRequestResponseSize) + { + string msg = $"Maximum amount of data {Modbus.MaximumDiscreteRequestResponseSize} coils."; + throw new ArgumentOutOfRangeException("NumberOfPoints", msg); + } + + MessageImpl.NumberOfPoints = value; + } + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + public override string ToString() + { + string msg = $"Wrote {NumberOfPoints} coils starting at address {StartAddress}."; + return msg; + } + + protected override void InitializeUnique(byte[] frame) + { + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersRequest.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersRequest.cs new file mode 100644 index 0000000..1e99594 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersRequest.cs @@ -0,0 +1,99 @@ +namespace Modbus.Message +{ + using System; + using System.IO; + using System.Linq; + using System.Net; + + using Data; + + using Unme.Common; + + public class WriteMultipleRegistersRequest : AbstractModbusMessageWithData, IModbusRequest + { + public WriteMultipleRegistersRequest() + { + } + + public WriteMultipleRegistersRequest(byte slaveAddress, ushort startAddress, RegisterCollection data) + : base(slaveAddress, Modbus.WriteMultipleRegisters) + { + StartAddress = startAddress; + NumberOfPoints = (ushort)data.Count; + ByteCount = (byte)(data.Count * 2); + Data = data; + } + + public byte ByteCount + { + get { return MessageImpl.ByteCount.Value; } + set { MessageImpl.ByteCount = value; } + } + + public ushort NumberOfPoints + { + get + { + return MessageImpl.NumberOfPoints.Value; + } + + set + { + if (value > Modbus.MaximumRegisterRequestResponseSize) + { + string msg = $"Maximum amount of data {Modbus.MaximumRegisterRequestResponseSize} registers."; + throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg); + } + + MessageImpl.NumberOfPoints = value; + } + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override int MinimumFrameSize + { + get { return 7; } + } + + public override string ToString() + { + string msg = $"Write {NumberOfPoints} holding registers starting at address {StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = (WriteMultipleRegistersResponse)response; + + if (StartAddress != typedResponse.StartAddress) + { + string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}."; + throw new IOException(msg); + } + + if (NumberOfPoints != typedResponse.NumberOfPoints) + { + string msg = $"Unexpected number of points in response. Expected {NumberOfPoints}, received {typedResponse.NumberOfPoints}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + if (frame.Length < MinimumFrameSize + frame[6]) + { + throw new FormatException("Message frame does not contain enough bytes."); + } + + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)); + ByteCount = frame[6]; + Data = new RegisterCollection(frame.Slice(7, ByteCount).ToArray()); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersResponse.cs new file mode 100644 index 0000000..0ecdfba --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteMultipleRegistersResponse.cs @@ -0,0 +1,61 @@ +namespace Modbus.Message +{ + using System; + using System.Net; + + public class WriteMultipleRegistersResponse : AbstractModbusMessage, IModbusMessage + { + public WriteMultipleRegistersResponse() + { + } + + public WriteMultipleRegistersResponse(byte slaveAddress, ushort startAddress, ushort numberOfPoints) + : base(slaveAddress, Modbus.WriteMultipleRegisters) + { + StartAddress = startAddress; + NumberOfPoints = numberOfPoints; + } + + public ushort NumberOfPoints + { + get + { + return MessageImpl.NumberOfPoints.Value; + } + + set + { + if (value > Modbus.MaximumRegisterRequestResponseSize) + { + string msg = $"Maximum amount of data {Modbus.MaximumRegisterRequestResponseSize} registers."; + throw new ArgumentOutOfRangeException(nameof(NumberOfPoints), msg); + } + + MessageImpl.NumberOfPoints = value; + } + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + public override string ToString() + { + string msg = $"Wrote {NumberOfPoints} holding registers starting at address {StartAddress}."; + return msg; + } + + protected override void InitializeUnique(byte[] frame) + { + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + NumberOfPoints = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4)); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleCoilRequestResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleCoilRequestResponse.cs new file mode 100644 index 0000000..2e87766 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleCoilRequestResponse.cs @@ -0,0 +1,69 @@ +namespace Modbus.Message +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + + using Data; + + using Unme.Common; + + public class WriteSingleCoilRequestResponse : AbstractModbusMessageWithData, IModbusRequest + { + public WriteSingleCoilRequestResponse() + { + } + + public WriteSingleCoilRequestResponse(byte slaveAddress, ushort startAddress, bool coilState) + : base(slaveAddress, Modbus.WriteSingleCoil) + { + StartAddress = startAddress; + Data = new RegisterCollection(coilState ? Modbus.CoilOn : Modbus.CoilOff); + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override string ToString() + { + Debug.Assert(Data != null, "Argument Data cannot be null."); + Debug.Assert(Data.Count() == 1, "Data should have a count of 1."); + + string msg = $"Write single coil {(Data.First() == Modbus.CoilOn ? 1 : 0)} at address {StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = (WriteSingleCoilRequestResponse)response; + + if (StartAddress != typedResponse.StartAddress) + { + string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}."; + throw new IOException(msg); + } + + if (Data.First() != typedResponse.Data.First()) + { + string msg = $"Unexpected data in response. Expected {Data.First()}, received {typedResponse.Data.First()}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + Data = new RegisterCollection(frame.Slice(4, 2).ToArray()); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleRegisterRequestResponse.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleRegisterRequestResponse.cs new file mode 100644 index 0000000..8cd1d55 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Message/WriteSingleRegisterRequestResponse.cs @@ -0,0 +1,67 @@ +namespace Modbus.Message +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Net; + + using Data; + + public class WriteSingleRegisterRequestResponse : AbstractModbusMessageWithData, IModbusRequest + { + public WriteSingleRegisterRequestResponse() + { + } + + public WriteSingleRegisterRequestResponse(byte slaveAddress, ushort startAddress, ushort registerValue) + : base(slaveAddress, Modbus.WriteSingleRegister) + { + StartAddress = startAddress; + Data = new RegisterCollection(registerValue); + } + + public override int MinimumFrameSize + { + get { return 6; } + } + + public ushort StartAddress + { + get { return MessageImpl.StartAddress.Value; } + set { MessageImpl.StartAddress = value; } + } + + public override string ToString() + { + Debug.Assert(Data != null, "Argument Data cannot be null."); + Debug.Assert(Data.Count() == 1, "Data should have a count of 1."); + + string msg = $"Write single holding register {Data[0]} at address {StartAddress}."; + return msg; + } + + public void ValidateResponse(IModbusMessage response) + { + var typedResponse = (WriteSingleRegisterRequestResponse)response; + + if (StartAddress != typedResponse.StartAddress) + { + string msg = $"Unexpected start address in response. Expected {StartAddress}, received {typedResponse.StartAddress}."; + throw new IOException(msg); + } + + if (Data.First() != typedResponse.Data.First()) + { + string msg = $"Unexpected data in response. Expected {Data.First()}, received {typedResponse.Data.First()}."; + throw new IOException(msg); + } + } + + protected override void InitializeUnique(byte[] frame) + { + StartAddress = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 2)); + Data = new RegisterCollection((ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(frame, 4))); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Modbus.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Modbus.cs new file mode 100644 index 0000000..e3630e1 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Modbus.cs @@ -0,0 +1,60 @@ +namespace Modbus +{ + /// + /// Defines constants related to the Modbus protocol. + /// + internal static class Modbus + { + // supported function codes + public const byte ReadCoils = 1; + public const byte ReadInputs = 2; + public const byte ReadHoldingRegisters = 3; + public const byte ReadInputRegisters = 4; + public const byte WriteSingleCoil = 5; + public const byte WriteSingleRegister = 6; + public const byte Diagnostics = 8; + public const ushort DiagnosticsReturnQueryData = 0; + public const byte WriteMultipleCoils = 15; + public const byte WriteMultipleRegisters = 16; + public const byte ReadWriteMultipleRegisters = 23; + + public const int MaximumDiscreteRequestResponseSize = 2040; + public const int MaximumRegisterRequestResponseSize = 127; + + // modbus slave exception offset that is added to the function code, to flag an exception + public const byte ExceptionOffset = 128; + + // modbus slave exception codes + public const byte IllegalFunction = 1; + public const byte IllegalDataAddress = 2; + public const byte Acknowledge = 5; + public const byte SlaveDeviceBusy = 6; + + // default setting for number of retries for IO operations + public const int DefaultRetries = 3; + + // default number of milliseconds to wait after encountering an ACKNOWLEGE or SLAVE DEVIC BUSY slave exception response. + public const int DefaultWaitToRetryMilliseconds = 250; + + // default setting for IO timeouts in milliseconds + public const int DefaultTimeout = 1000; + + // smallest supported message frame size (sans checksum) + public const int MinimumFrameSize = 2; + + public const ushort CoilOn = 0xFF00; + public const ushort CoilOff = 0x0000; + + // IP slaves should be addressed by IP + public const byte DefaultIpSlaveUnitId = 0; + + // An existing connection was forcibly closed by the remote host + public const int ConnectionResetByPeer = 10054; + + // Existing socket connection is being closed + public const int WSACancelBlockingCall = 10004; + + // used by the ASCII tranport to indicate end of message + public const string NewLine = "\r\n"; + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Properties/AssemblyInfo.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..628e55b --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("NModbus4")] +[assembly: AssemblyProduct("NModbus4")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyCopyright("Copyright © 2006 Scott Alexander, 2015 Dmitry Turin")] +[assembly: AssemblyDescription("NModbus4 is a C# implementation of the Modbus protocol. " + + "Provides connectivity to Modbus slave compatible devices and applications. " + + "Supports ASCII, RTU, TCP, and UDP protocols. " + + "NModbus4 it's a fork of NModbus(https://code.google.com/p/nmodbus)")] + +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: CLSCompliant(false)] +[assembly: NeutralResourcesLanguage("en-US")] +[assembly: ComVisible(false)] +[assembly: AssemblyVersion("3.0.0.0")] +[assembly: AssemblyFileVersion("3.0.0.0")] +[assembly: AssemblyInformationalVersion("3.0.0-dev")] + +#if !SIGNED +[assembly: InternalsVisibleTo("NModbus4.UnitTests")] +[assembly: InternalsVisibleTo("NModbus4.IntegrationTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Resources.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Resources.cs new file mode 100644 index 0000000..823883e --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Resources.cs @@ -0,0 +1,41 @@ +namespace Modbus +{ + internal static class Resources + { + public const string Acknowlege = "Specialized use in conjunction with programming commands.The server (or slave) has accepted the request and is processing it, but a long duration of time will be required to do so.This response is returned to prevent a timeout error from occurring in the client(or master). The client(or master) can next issue a Poll Program Complete message to determine if processing is completed."; + + public const string EmptyEndPoint = "Argument endPoint cannot be empty."; + + public const string GatewayPathUnavailable = "Specialized use in conjunction with gateways, indicates that the gateway was unable to allocate an internal communication path from the input port to the output port for processing the request.Usually means that the gateway is misconfigured or overloaded."; + + public const string GatewayTargetDeviceFailedToRespond = "Specialized use in conjunction with gateways, indicates that no response was obtained from the target device.Usually means that the device is not present on the network."; + + public const string HexCharacterCountNotEven = "Hex string must have even number of characters."; + + public const string IllegalDataAddress = "The data address received in the query is not an allowable address for the server (or slave). More specifically, the combination of reference number and transfer length is invalid.For a controller with 100 registers, the PDU addresses the first register as 0, and the last one as 99. If a request is submitted with a starting register address of 96 and a quantity of registers of 4, then this request will successfully operate(address-wise at least) on registers 96, 97, 98, 99. If a request is submitted with a starting register address of 96 and a quantity of registers of 5, then this request will fail with Exception Code 0x02 “Illegal Data Address” since it attempts to operate on registers 96, 97, 98, 99 and 100, and there is no register with address 100."; + + public const string IllegalDataValue = "A value contained in the query data field is not an allowable value for server(or slave). This indicates a fault in the structure of the remainder of a complex request, such as that the implied length is incorrect.It specifically does NOT mean that a data item submitted for storage in a register has a value outside the expectation of the application program, since the MODBUS protocol is unaware of the significance of any particular value of any particular register."; + + public const string IllegalFunction = "The function code received in the query is not an allowable action for the server (or slave). This may be because the function code is only applicable to newer devices, and was not implemented in the unit selected.It could also indicate that the server(or slave) is in the wrong state to process a request of this type, for example because it is unconfigured and is being asked to return register values."; + + public const string MemoryParityError = "Specialized use in conjunction with function codes 20 and 21 and reference type 6, to indicate that the extended file area failed to pass a consistency check."; + + public const string NetworkBytesNotEven = "Array networkBytes must contain an even number of bytes."; + + public const string SlaveDeviceBusy = "Specialized use in conjunction with programming commands. The server (or slave) is engaged in processing a long–duration program command.The client(or master) should retransmit the message later when the server(or slave) is free."; + + public const string SlaveDeviceFailure = "An unrecoverable error occurred while the server(or slave) was attempting to perform the requested action."; + + public const string SlaveExceptionResponseFormat = "Function Code: {1}{0}Exception Code: {2} - {3}"; + + public const string SlaveExceptionResponseInvalidFunctionCode = "Invalid function code value for SlaveExceptionResponse."; + + public const string TimeoutNotSupported = "The compact framework UDP client does not support timeouts."; + + public const string UdpClientNotConnected = "UdpClient must be bound to a default remote host. Call the Connect method."; + + public const string Unknown = "Unknown slave exception code."; + + public const string WaitRetryGreaterThanZero = "WaitToRetryMilliseconds must be greater than 0."; + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/SerialPortAdapter.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/SerialPortAdapter.cs new file mode 100644 index 0000000..6592f18 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/SerialPortAdapter.cs @@ -0,0 +1,71 @@ +namespace Modbus.Serial +{ + using System; + using System.Diagnostics; + using System.IO.Ports; + using global::Modbus.IO; + + /// + /// Concrete Implementor - http://en.wikipedia.org/wiki/Bridge_Pattern + /// + public class SerialPortAdapter : IStreamResource + { + private const string NewLine = "\r\n"; + private SerialPort _serialPort; + + public SerialPortAdapter(SerialPort serialPort) + { + Debug.Assert(serialPort != null, "Argument serialPort cannot be null."); + + _serialPort = serialPort; + _serialPort.NewLine = NewLine; + } + + public int InfiniteTimeout + { + get { return SerialPort.InfiniteTimeout; } + } + + public int ReadTimeout + { + get { return _serialPort.ReadTimeout; } + set { _serialPort.ReadTimeout = value; } + } + + public int WriteTimeout + { + get { return _serialPort.WriteTimeout; } + set { _serialPort.WriteTimeout = value; } + } + + public void DiscardInBuffer() + { + _serialPort.DiscardInBuffer(); + } + + public int Read(byte[] buffer, int offset, int count) + { + return _serialPort.Read(buffer, offset, count); + } + + public void Write(byte[] buffer, int offset, int count) + { + _serialPort.Write(buffer, offset, count); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _serialPort?.Dispose(); + _serialPort = null; + } + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/SlaveException.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/SlaveException.cs new file mode 100644 index 0000000..a9a9a1f --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/SlaveException.cs @@ -0,0 +1,173 @@ +namespace Modbus +{ + using System; + using System.Diagnostics.CodeAnalysis; +#if NET46 + using System.Runtime.Serialization; + using System.Security.Permissions; +#endif + using Message; + + /// + /// Represents slave errors that occur during communication. + /// +#if NET46 + [Serializable] +#endif + public class SlaveException : Exception + { + private const string SlaveAddressPropertyName = "SlaveAdress"; + private const string FunctionCodePropertyName = "FunctionCode"; + private const string SlaveExceptionCodePropertyName = "SlaveExceptionCode"; + + private readonly SlaveExceptionResponse _slaveExceptionResponse; + + /// + /// Initializes a new instance of the class. + /// + public SlaveException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + public SlaveException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The inner exception. + public SlaveException(string message, Exception innerException) + : base(message, innerException) + { + } + + internal SlaveException(SlaveExceptionResponse slaveExceptionResponse) + { + _slaveExceptionResponse = slaveExceptionResponse; + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Used by test code.")] + internal SlaveException(string message, SlaveExceptionResponse slaveExceptionResponse) + : base(message) + { + _slaveExceptionResponse = slaveExceptionResponse; + } + +#if NET46 + /// + /// Initializes a new instance of the class. + /// + /// + /// The that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// + /// The class name is null or + /// is zero (0). + /// + /// The info parameter is null. + protected SlaveException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + if (info != null) + { + _slaveExceptionResponse = new SlaveExceptionResponse( + info.GetByte(SlaveAddressPropertyName), + info.GetByte(FunctionCodePropertyName), + info.GetByte(SlaveExceptionCodePropertyName)); + } + } +#endif + + /// + /// Gets a message that describes the current exception. + /// + /// + /// The error message that explains the reason for the exception, or an empty string. + /// + public override string Message + { + get + { + string responseString; + responseString = _slaveExceptionResponse != null ? string.Concat(Environment.NewLine, _slaveExceptionResponse) : string.Empty; + return string.Concat(base.Message, responseString); + } + } + + /// + /// Gets the response function code that caused the exception to occur, or 0. + /// + /// The function code. + public byte FunctionCode + { + get { return _slaveExceptionResponse != null ? _slaveExceptionResponse.FunctionCode : (byte)0; } + } + + /// + /// Gets the slave exception code, or 0. + /// + /// The slave exception code. + public byte SlaveExceptionCode + { + get { return _slaveExceptionResponse != null ? _slaveExceptionResponse.SlaveExceptionCode : (byte)0; } + } + + /// + /// Gets the slave address, or 0. + /// + /// The slave address. + public byte SlaveAddress + { + get { return _slaveExceptionResponse != null ? _slaveExceptionResponse.SlaveAddress : (byte)0; } + } + +#if NET46 + /// + /// When overridden in a derived class, sets the + /// with information about the exception. + /// + /// + /// The that holds the serialized + /// object data about the exception being thrown. + /// + /// + /// The that contains contextual + /// information about the source or destination. + /// + /// The info parameter is a null reference (Nothing in Visual Basic). + /// + /// + /// + /// + [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] + [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods", Justification = "Argument info is validated, rule does not understand AND condition.")] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + if (info != null && _slaveExceptionResponse != null) + { + info.AddValue(SlaveAddressPropertyName, _slaveExceptionResponse.SlaveAddress); + info.AddValue(FunctionCodePropertyName, _slaveExceptionResponse.FunctionCode); + info.AddValue(SlaveExceptionCodePropertyName, _slaveExceptionResponse.SlaveExceptionCode); + } + } +#endif + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/DisposableUtility.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/DisposableUtility.cs new file mode 100644 index 0000000..52075f9 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/DisposableUtility.cs @@ -0,0 +1,19 @@ +namespace Modbus.Unme.Common +{ + using System; + + internal static class DisposableUtility + { + public static void Dispose(ref T item) + where T : class, IDisposable + { + if (item == null) + { + return; + } + + item.Dispose(); + item = default(T); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/SequenceUtility.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/SequenceUtility.cs new file mode 100644 index 0000000..747c946 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Unme.Common/SequenceUtility.cs @@ -0,0 +1,32 @@ +namespace Modbus.Unme.Common +{ + using System; + using System.Collections.Generic; + using System.Linq; + + internal static class SequenceUtility + { + public static IEnumerable Slice(this IEnumerable source, int startIndex, int size) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var enumerable = source as T[] ?? source.ToArray(); + int num = enumerable.Count(); + + if (startIndex < 0 || num < startIndex) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + if (size < 0 || startIndex + size > num) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + + return enumerable.Skip(startIndex).Take(size); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/DiscriminatedUnion.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/DiscriminatedUnion.cs new file mode 100644 index 0000000..1d41440 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/DiscriminatedUnion.cs @@ -0,0 +1,122 @@ +namespace Modbus.Utility +{ + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// Possible options for DiscriminatedUnion type. + /// + public enum DiscriminatedUnionOption + { + /// + /// Option A. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "A")] + A, + + /// + /// Option B. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "B")] + B + } + + /// + /// A data type that can store one of two possible strongly typed options. + /// + /// The type of option A. + /// The type of option B. + public class DiscriminatedUnion + { + private TA optionA; + private TB optionB; + private DiscriminatedUnionOption option; + + /// + /// Gets the value of option A. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "A")] + public TA A + { + get + { + if (this.Option != DiscriminatedUnionOption.A) + { + string msg = $"{DiscriminatedUnionOption.A} is not a valid option for this discriminated union instance."; + throw new InvalidOperationException(msg); + } + + return this.optionA; + } + } + + /// + /// Gets the value of option B. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "B")] + public TB B + { + get + { + if (this.Option != DiscriminatedUnionOption.B) + { + string msg = $"{DiscriminatedUnionOption.B} is not a valid option for this discriminated union instance."; + throw new InvalidOperationException(msg); + } + + return this.optionB; + } + } + + /// + /// Gets the discriminated value option set for this instance. + /// + public DiscriminatedUnionOption Option + { + get { return this.option; } + } + + /// + /// Factory method for creating DiscriminatedUnion with option A set. + /// + [SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "Factory method.")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "0#a")] + public static DiscriminatedUnion CreateA(TA a) + { + return new DiscriminatedUnion() { option = DiscriminatedUnionOption.A, optionA = a }; + } + + /// + /// Factory method for creating DiscriminatedUnion with option B set. + /// + [SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "Factory method.")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "0#b")] + public static DiscriminatedUnion CreateB(TB b) + { + return new DiscriminatedUnion() { option = DiscriminatedUnionOption.B, optionB = b }; + } + + /// + /// Returns a that represents the current . + /// + /// + /// A that represents the current . + /// + public override string ToString() + { + string value = null; + + switch (Option) + { + case DiscriminatedUnionOption.A: + value = A.ToString(); + break; + case DiscriminatedUnionOption.B: + value = B.ToString(); + break; + } + + return value; + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/ModbusUtility.cs b/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/ModbusUtility.cs new file mode 100644 index 0000000..89188e5 --- /dev/null +++ b/Plugins/Drivers/DriverModbusTCP/NModbus4/Utility/ModbusUtility.cs @@ -0,0 +1,217 @@ +namespace Modbus.Utility +{ + using System; + using System.Linq; + using System.Net; + using System.Text; + + /// + /// Modbus utility methods. + /// + public static class ModbusUtility + { + private static readonly ushort[] CrcTable = + { + 0X0000, 0XC0C1, 0XC181, 0X0140, 0XC301, 0X03C0, 0X0280, 0XC241, + 0XC601, 0X06C0, 0X0780, 0XC741, 0X0500, 0XC5C1, 0XC481, 0X0440, + 0XCC01, 0X0CC0, 0X0D80, 0XCD41, 0X0F00, 0XCFC1, 0XCE81, 0X0E40, + 0X0A00, 0XCAC1, 0XCB81, 0X0B40, 0XC901, 0X09C0, 0X0880, 0XC841, + 0XD801, 0X18C0, 0X1980, 0XD941, 0X1B00, 0XDBC1, 0XDA81, 0X1A40, + 0X1E00, 0XDEC1, 0XDF81, 0X1F40, 0XDD01, 0X1DC0, 0X1C80, 0XDC41, + 0X1400, 0XD4C1, 0XD581, 0X1540, 0XD701, 0X17C0, 0X1680, 0XD641, + 0XD201, 0X12C0, 0X1380, 0XD341, 0X1100, 0XD1C1, 0XD081, 0X1040, + 0XF001, 0X30C0, 0X3180, 0XF141, 0X3300, 0XF3C1, 0XF281, 0X3240, + 0X3600, 0XF6C1, 0XF781, 0X3740, 0XF501, 0X35C0, 0X3480, 0XF441, + 0X3C00, 0XFCC1, 0XFD81, 0X3D40, 0XFF01, 0X3FC0, 0X3E80, 0XFE41, + 0XFA01, 0X3AC0, 0X3B80, 0XFB41, 0X3900, 0XF9C1, 0XF881, 0X3840, + 0X2800, 0XE8C1, 0XE981, 0X2940, 0XEB01, 0X2BC0, 0X2A80, 0XEA41, + 0XEE01, 0X2EC0, 0X2F80, 0XEF41, 0X2D00, 0XEDC1, 0XEC81, 0X2C40, + 0XE401, 0X24C0, 0X2580, 0XE541, 0X2700, 0XE7C1, 0XE681, 0X2640, + 0X2200, 0XE2C1, 0XE381, 0X2340, 0XE101, 0X21C0, 0X2080, 0XE041, + 0XA001, 0X60C0, 0X6180, 0XA141, 0X6300, 0XA3C1, 0XA281, 0X6240, + 0X6600, 0XA6C1, 0XA781, 0X6740, 0XA501, 0X65C0, 0X6480, 0XA441, + 0X6C00, 0XACC1, 0XAD81, 0X6D40, 0XAF01, 0X6FC0, 0X6E80, 0XAE41, + 0XAA01, 0X6AC0, 0X6B80, 0XAB41, 0X6900, 0XA9C1, 0XA881, 0X6840, + 0X7800, 0XB8C1, 0XB981, 0X7940, 0XBB01, 0X7BC0, 0X7A80, 0XBA41, + 0XBE01, 0X7EC0, 0X7F80, 0XBF41, 0X7D00, 0XBDC1, 0XBC81, 0X7C40, + 0XB401, 0X74C0, 0X7580, 0XB541, 0X7700, 0XB7C1, 0XB681, 0X7640, + 0X7200, 0XB2C1, 0XB381, 0X7340, 0XB101, 0X71C0, 0X7080, 0XB041, + 0X5000, 0X90C1, 0X9181, 0X5140, 0X9301, 0X53C0, 0X5280, 0X9241, + 0X9601, 0X56C0, 0X5780, 0X9741, 0X5500, 0X95C1, 0X9481, 0X5440, + 0X9C01, 0X5CC0, 0X5D80, 0X9D41, 0X5F00, 0X9FC1, 0X9E81, 0X5E40, + 0X5A00, 0X9AC1, 0X9B81, 0X5B40, 0X9901, 0X59C0, 0X5880, 0X9841, + 0X8801, 0X48C0, 0X4980, 0X8941, 0X4B00, 0X8BC1, 0X8A81, 0X4A40, + 0X4E00, 0X8EC1, 0X8F81, 0X4F40, 0X8D01, 0X4DC0, 0X4C80, 0X8C41, + 0X4400, 0X84C1, 0X8581, 0X4540, 0X8701, 0X47C0, 0X4680, 0X8641, + 0X8201, 0X42C0, 0X4380, 0X8341, 0X4100, 0X81C1, 0X8081, 0X4040 + }; + + /// + /// Converts four UInt16 values into a IEEE 64 floating point format. + /// + /// Highest-order ushort value. + /// Second-to-highest-order ushort value. + /// Second-to-lowest-order ushort value. + /// Lowest-order ushort value. + /// IEEE 64 floating point value. + public static double GetDouble(ushort b3, ushort b2, ushort b1, ushort b0) + { + byte[] value = BitConverter.GetBytes(b0) + .Concat(BitConverter.GetBytes(b1)) + .Concat(BitConverter.GetBytes(b2)) + .Concat(BitConverter.GetBytes(b3)) + .ToArray(); + + return BitConverter.ToDouble(value, 0); + } + + /// + /// Converts two UInt16 values into a IEEE 32 floating point format. + /// + /// High order ushort value. + /// Low order ushort value. + /// IEEE 32 floating point value. + public static float GetSingle(ushort highOrderValue, ushort lowOrderValue) + { + byte[] value = BitConverter.GetBytes(lowOrderValue) + .Concat(BitConverter.GetBytes(highOrderValue)) + .ToArray(); + + return BitConverter.ToSingle(value, 0); + } + + /// + /// Converts two UInt16 values into a UInt32. + /// + public static uint GetUInt32(ushort highOrderValue, ushort lowOrderValue) + { + byte[] value = BitConverter.GetBytes(lowOrderValue) + .Concat(BitConverter.GetBytes(highOrderValue)) + .ToArray(); + + return BitConverter.ToUInt32(value, 0); + } + + /// + /// Converts an array of bytes to an ASCII byte array. + /// + /// The byte array. + /// An array of ASCII byte values. + public static byte[] GetAsciiBytes(params byte[] numbers) + { + return Encoding.UTF8.GetBytes(numbers.SelectMany(n => n.ToString("X2")).ToArray()); + } + + /// + /// Converts an array of UInt16 to an ASCII byte array. + /// + /// The ushort array. + /// An array of ASCII byte values. + public static byte[] GetAsciiBytes(params ushort[] numbers) + { + return Encoding.UTF8.GetBytes(numbers.SelectMany(n => n.ToString("X4")).ToArray()); + } + + /// + /// Converts a network order byte array to an array of UInt16 values in host order. + /// + /// The network order byte array. + /// The host order ushort array. + public static ushort[] NetworkBytesToHostUInt16(byte[] networkBytes) + { + if (networkBytes == null) + { + throw new ArgumentNullException(nameof(networkBytes)); + } + + if (networkBytes.Length % 2 != 0) + { + throw new FormatException(Resources.NetworkBytesNotEven); + } + + ushort[] result = new ushort[networkBytes.Length / 2]; + + for (int i = 0; i < result.Length; i++) + { + result[i] = (ushort)IPAddress.NetworkToHostOrder(BitConverter.ToInt16(networkBytes, i * 2)); + } + + return result; + } + + /// + /// Converts a hex string to a byte array. + /// + /// The hex string. + /// Array of bytes. + public static byte[] HexToBytes(string hex) + { + if (hex == null) + { + throw new ArgumentNullException(nameof(hex)); + } + + if (hex.Length % 2 != 0) + { + throw new FormatException(Resources.HexCharacterCountNotEven); + } + + byte[] bytes = new byte[hex.Length / 2]; + + for (int i = 0; i < bytes.Length; i++) + { + bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); + } + + return bytes; + } + + /// + /// Calculate Longitudinal Redundancy Check. + /// + /// The data used in LRC. + /// LRC value. + public static byte CalculateLrc(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte lrc = 0; + + foreach (byte b in data) + { + lrc += b; + } + + lrc = (byte)((lrc ^ 0xFF) + 1); + + return lrc; + } + + /// + /// Calculate Cyclical Redundancy Check. + /// + /// The data used in CRC. + /// CRC value. + public static byte[] CalculateCrc(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + ushort crc = ushort.MaxValue; + + foreach (byte b in data) + { + byte tableIndex = (byte)(crc ^ b); + crc >>= 8; + crc ^= CrcTable[tableIndex]; + } + + return BitConverter.GetBytes(crc); + } + } +} diff --git a/Plugins/Drivers/DriverModbusTCP/System.IO.Ports.dll b/Plugins/Drivers/DriverModbusTCP/System.IO.Ports.dll new file mode 100644 index 0000000000000000000000000000000000000000..bd5d3fbf897eb146021623f17b912b232ec81260 GIT binary patch literal 39536 zcmeHw2VB(0)AwHCXo565=!p##j{_`VM?giT1h7Vf$8o?Z+~M7!U3&hE~(+1)d7 z&}^n)j4AQ==bw!2#5aAqa`@Mv1kv6NKJaGmx*u=2Q`YBr!}MIERb5~)=U8<4YQ4^6 zGTYRd2DPQoq&Aw=ami`wd~=pTt645(`#)%CwB-f#N1m zH%-U48h_>ZW6Xz#<$~TsApQC05+LaAb!8~m(8HK6g~hc%rr>ht zRK`}L4F&`v|EY|Xh_t`@sxJ?z$(TormxOP5tr;PV$(qzuEFC99E464Z(iN3a=<6H{ zn^d-_J02~fY*9}uXZ4XO1o(!a{Hjp7yhVUVh%bV2b1h`&aY51a0#iYE8EML+TOtrvi6F&RzE@?k06y94TBvpsPoVO^VQ_LaJJF zo{R$p*CHfHR0TBwm0GN=N^1qQc2=YT@qSR1+5%^j+7*;g6Z<)VJ-CA24)!K>yq$7z z@m7X3q*U9Kq0L0q6%N&v6;xM-1QV;G#rW|qXxApKYBoKI=9Gst0@;er=Fy~tB3fuH zXs80-S_p?bg^f<(xlZBDPT@07VT@>YIleMSILs-W>=d5p6yE3*KJOH+A$Q1$bqddN z3h#9a`zjp#iB92(PT>PiVTIDc-_j|Z;}l-*6h7q?e(V%ZaB;|4>=b_ADSXo@+)m}- z&v6Q`aSDIo6t3gy;7@Z3Z*&S@cM7|>Iruv`g$+*O$xh+-oWd^djZ}{n!{ogwI&(?{m#sB0Qb*xIz*_AEo%g6BVV9fk0(D7S5 zPe5AvJnP$$2Zj-z2mf}E(f2u-e>=#{MY8xUEUz5VSo82y{M)hW0ynL?R*q{)@eX#5 zqI~>&w0$ua3NEbGgjC9v<62RweifsY0u_mgn<|NOVNsdZtDB5gSoqNTb7X6cRuid> z438#?3t!qU!V0R)zuG2?^rTGoHa4w*Y;`A8PYu;Qd3)L|IE`S&h-m*5YT$&fYci$CH)TKUsba_6XLzaDht+x@ zHqD)28+WU>iq&_Wf^de4!Yw@rZsl|thb!HPvX{d}oRaNIlq~PfzAARjjo@w7_y84q zMMe1z@St3-`4UvQ65Q@buzz*RW=r)ny8 zwmQKnUZE(<+n&vlS2oWjg!7zrA zJVh*q5#~!6HgzGsJ^e~qfEc%+dQZQ}z=ah$U*VUW3)Dj^)YaOwo|xGnZ}h z>xupoYR8h$V+Pr=zSzIduwyaIi@k5hvRHL?)sE#cKjs%K$t=WB+S86rVD(s`fVrhr zx5(?W`6A}eMgdzPVt2d<+bCkUJqdeX#3H;2J0xOp96KdqwK?{!h)t0b=S>j{QW5r0 z#7g`LlVP8Tvb(*@bMO+eB^;|GV%eOtiHHs5dWMVGX|87{5xZ5LWG0B%4j$K6#18Ou zIuV=4qYDy&tbcW{o+TQ`$X&ujvW`V zA_c{r7qL|wyCz~8yv}|Uu^=UJ{w`u2TnKY-EY#U|Jg$a_W%Ia(B9_3*&`QJ_b1YiK z25?>CMeH*llKFy&F)n3@h;^?&9U7g_9Cx|BO(^f)14Ku5N;EfMeH-qc}K*)=hzdD&8gkWXAHDf)3Sk0EMjxzZ4{|YD`IQp1LUc! zqlh(x%rw?h#59nZ#?nRXLl2cAoedSS&pc#U?c|BrJN_y~Usfby2mEDNM^6$lSGh{j zkIfdbV9Zqg*isRzr|?kpXK#vFbJS9Q_O6K4@rzarU>}KCQ@_r@j)~Y6=rWLfDPliE zmx1g%5pxYtDF(57B33&<#s;w`A{HWxQ@qIBXt57_i3T>91&COpte0X4Q;S#zu$Ne< zh`q{E6d5c^#FhXX%Hjl!edVHJI+h}0EvpkYSj0MTESF;k*(H}6h#SVS1?;|0KSd@R zDRTN_U7yLyM65r15pgr^m|kIEuZvhF%AU*Cir8as!nTQ6bhW0y_K4V0v}P{*RK)Ii zw`IBPoQMrn4ObZ16+32CJsTHt66|l?-*f2X*1L=wd%x$=bhoXp; zI>i;S*-mlAY>_<<`xCZKj5~-Djb@weaVj>Ny=TX206Q#lzKk5kuoLz;A2x=4Z^ymLFDq03II9V~4UF@MN((z&suNviv^5i34kqJQaQ}vFptARzz%EzU@V&l*qbfqa0iE{IlRH)AAo5rz(vDeWMP1M zrUx9(jsjZQw}4}qFEp9RrS)dbRIzLcG>nD+DNUy5VZ9h$#RjV;a>_(bnFz1TMAa%z zS;Z-DuqTIs98Q-LrJ;)8B!7ZCIp=Z? zhjB_4mvfHG`Jp=Te89sWbNV74Uc@=Cb2yDl4v-P&P#*h$b3XPX%GVr@RZw^xhk8K= zB~iM#5WK;|!+1E6%M9UgFi(5Nhj^4cwpTR@zry)v@o+MirsR~Nf*ekNlfw+2V<%1z zDYrO0>9Q2kPP(k%a4le&*V}-{qnyeQub=<7|Cy)Fj<3j#g?TrWQkSi&i1JV?UF{sf!K3AW-ATKJtp_z{l{ z3?TYjN|IpqAZY^pgi~JOl(W9)>~spdvFRB7eb_5BcuT;hI5m)i8U`4GIY^GUNWd1bdpS7T0fw?@ zz*ejSpoVn<3}amYwV18sND~Vf!QufUSr5S0n7`ym-y5(k_K9*xNd}BU-;qOB8en_$ z8#yHQ2keO5jvR7d1nkVncix3%0CvT!Cr2K7z;4U{7|U`1<1i1(k<)O%M3xVjhV{K1 zX+{7J#YiecN-M&dtPo*6pd53>C_n?&#&VX8aaIml;{bEn1n?LEa5119tL?c6 zj{%fpcAAgyI6yfY&lVy)0Z`6L*kXiB0p+X=qqz)vEJ1h@TZZstKslSjmLvQ!pd2&X z3WTQt$}wuK0^G>f0B&aM0JmZ#AZPEgw*lWnjGTRd7&+U87-b2a=m;Mg|2E*1fz~UI z&nhV&h_Sb*3DIbXi#$DMpioS`7K-kp^c zkn}|sh4}`PExFK^oSjPIGLj62tR%B3$&iy&n4fAGQE0Rv4`NQv9A?nl;*8b;v(=E5 zffGH0E$h0m}f{b+q#5Y&OuR-9@m$kXW!T!o61v;v}w)+Ct>*PtgvT61;7 zg;WkK-joF!G=tvFkYhB7VJdf=%@S|YW#$Pf92l-7V&PkwWRwkJ55c7E?|?qZzFZi^`x15-LmR6Lwq6fR5C0Vs&|WhOETGJe#rL zFIfAdiVRipCi5)fi;PyAHG_;uYMPilSbA1*6*10@##1{db8bD(=p^!~(j}QIu=F+Q z3ensa<7h)xj9zcBT2Uu)29psTFUg#uv*lLEiEz<&Dn~na8V0+t_aI)n3Y~@(ICmkb z^psh;+1$r$%E{nGaw`RX^?>w-^ifoNpIjp@E*xHO`Q&!zdk- zXVh68u_84NZIVw`n`Y2i^tm*`I57#v&J|8IQ6+1PHJj+}zC#ipGGiy9;; zC=k*Mv$GABG)yt#VxPf<|a&<Urm*ikTZ!wP;9<_Ozei9H7w-A!keP@(BKwF+CK z4NZ#Kirvh!IO0u};-qaCOc_>MDLW~Wj4T6-P$53SY7df)Wn>yM$SOx+xXv(3OjJi1 zCM2s!(FyBIfl4b8)(f2#%XDG*jnPxxNL8I`$j-wyj3{L33D^u4Ic?t(l2ft7$DoyE z!zGUoynMu9f@Sk4u)h*{gy%#H`{bGOCM$CLF^$dET2z4 zETKD}LNMZvG-eqr4DZ2&fR#3?7FtS?4J4R^fkeWgvq;F~;}9>6qyQAca>2r^85XgF zNt8-^mn^<;6Lt<3r%koADXSv7!ltr9d;*R2I;T*nG*u36qZ&P7gq)EV3V=)(?j3bR$HdT%m zCO-_>mE)y#7VBOl)u2^lcRPq(Gc6U5#>52~6V_kU^ytP`UiVd5N`_e7WVaZWq*2gn zU#rm|4?Vdv!{LMAwTeoUV&a@U0F~pOZVsMM$5RWQ6Q>%}@CB$zmse~xT4BC=i`i<< zwpAo2nDlvtC>F0y@Y*X<>QNfUipI*wmW3J+cln}WTbX?_7FJ~f6P2Z#(PkCKGYQF( z0%R^}HgkcU#eqoFzi6aqH$*}*A7EKu>aS8G_cjzmkBq{nXfR~G&W5Fe(B481Wr72T znYkZ_nX|JoLa~Gx8uTF%D>9xvsvi?Sc6-hdkR-*6E-4z*`D_S&?n7essR6wQ2F->2 z&AV>a0bDH zYvFwPNRa@Zk%-BIXE+OZ5>naloecl4fgYkZPXWaOS`xVOL5l}00A36(L6#AGIgHhl zI5U6?Qp}Q+MvgejOUOTq%dqpbj71J6o_i+p$$-Dvz_C&AU6U>ukSc2Bn+V<_P^eUe z(3}NPNeCBY4@a1^)8Ws+ScCqc8+kYb(8x26?=+rgDri>t>?uVqPRK|v zc=!<3!D}v-x4BSPX^>EW|AbmH@ESM47jH&LFB6!%8Rt38j zc+>!uj&!4ZSt}uJAupX!lNEDJ#)&{S?!NJs?OmBWnb!sJr1BEyfRAdF>PRSm2GW}0 z?%O=xt<$m1~~*HnoHvJh8k6$fC%4 zsWwbjuHudh=u)i ze0S6_=_QS}ERouX#yA>@s8x+PU89oFc_CQ_76g1`Nk;onzc%t%VI+5GA&lLQ+{s#U zajtL1f7GXG1kbRKLiXG{3EHOdk&p5wzJB;NLraInJtH?*0nJW-pIXwB#r&(KwM&Rj z5jD$(j@YxHCTT8udX%QoIN4reJX9O_{3)v%xifH5OY@baiByM>94)o^vua(SS5g0% z)u;wjcwJX+FV?1FY-L-MT1Aj9=_9QfB$-dAl46)>%MP2Qnsl;dv3;Kvp8?+gtTk6E z$-iMOf+T4Sl}h|{e-Qbe+mfXcvwy1RNU7{?)T8qFSoI7crR&3cevVV0?uMGjnyiUn zArc?07-$Tq)eTvPI4ZOhxMI+AXf~xWvlwG&mAr{*h$9IG^ev4zGSldWJv3-m(E0+> zJmTOJ#%~8#Z^Tn92J_aZ4$tW|DDi`uKWGwry)=R;)iRlzLd|3@2>ALE>P(e#PE_ZXnZlpgVIB z0wD3IAOvK7CF^}lHmZ;}LdXSIO_70*-0j#H|38nn`u2sAa$GTC$`ZVAMM+e2g8+DH zT1+^uz2NQuULZ_4c(f>eN$U@G)a_84UFq=0_IGYaGn=wT+jUouo}ZAR{H*%q5q;l$ zZRT6e|M9!;XS+6bSm*u6y4|aS^M=i6=h5ek;+KG)!%ZIs51hOq$beuYyI_dT8GQ%UYv-MdKSy@?a!fW6a zo=@6vV*w=LPzu{zxWZ%#RgjBZh5zLWm0!MG<)%>is`1wteT-403WHIcY`T%Sm z*fJs4Wh7k9ZC;Hyl!*B@=4fM}E`%aA`V5DDS_=DhS&VU({Y5#uRnxJwlosP z1PR^&_LD#O91HSv#rzN%FO6W=B!y>JO~za^NibY>j1UO3n2lMSMPW2!6v9JurEMgI z@am*`B|jS8M9Y|q!PE+`*I7*&tLE%kRi;~T+QRC|SPjzDu8(vEgmToti4 z1yNHh2Aszvm@>^pPSgZzy2Y5E@5GCrx~gIKe5qs2mI5=*eaRurB4tdof4#cOSdAJd zm}-9ZWmUuf04Jg$OM}aUGL|ViZ>?&5o(@OPRyB?V)#L-UnDg@Ba8iRC*Ok=B2~K`5 z4=)cmd-BaV=Ti5@^&d5zoLk#_c~q(^j0=>E`*Ik5kEdE&^!bQzO`C{_aNId1ZHc07 z**YpB!VsyC)alx_Gh{|ZWJY9XMn>wRBO}7Iqaz~q+2P^n{D!u?`h4ug+rLRo)y!CH z8S}!e7qis}e`|4HOJ2iVo2|gwJ}fK;$2^6Z8ofC`EX!=eP!tAtwhb;J+i{79O<}ecjD8# zd#Hxy;4e?KD)=ac>jWApMD9C-B~+bn)tfDO#!ScbLO)6IT+>TtyLP%Z`ZjIDqa#}z zv{BKGp3Y3L7b(v)gN@ob5`XkbiHW2C14pM%e4=PKU4LxZYY~}SmqvXZw7u8C#2qaj z%-o^=%&=#Zcj?|XYlA*gmAM}Z2%dIynf2naI$ejHOEZl6a@EwL^RupPy*Oq3#H&|6 zEV|cYgW|_6IsNWu?*8V1w%bb&e>t}G*JHQ6AEnI>cr@+pGs;5~hPeBFFvPum{7T=$ zFO07-cKd|dWvA|hMhv+fCZFRMA@BcI>#f7xyDYzdwX1&Sh_n;($>|Gw{yy;2{0ABP zB1^JgmSqpiTGna!$EV`+SFD*>@NLSK5w`*t7w(Q+IH8&P+61Kw!##PrE6mtt7(qSE zgvI##6ZBUwb&P}&bw#`lcpJo@2kkS&2cf~d@bY6PaQfWB-woi&FyUsP6+zyINc$<$ zl_Gsz&{rY;CH#GdxK!}m2MyyGvq3K1hwg>HCBVx;zYSUpO(XM#|% zHJ(=}?Y1R9-V7vf{d_uAbj7`igq@!+p~DMQQN&je5n5S9Wrwe``?WfZy6wN_lV6Rb z5&yiy`dcJxAtlRzU+=Gd?9u3HEuMd4m<{$2f&K4KBR&9gba$0;bnK6%#$(U;Y_?`vwS1dR~E!!S=6Q?ms}JPv!H# zaI7&xgns(8>zC$3=L5P7Ft>je^WUG6B;b{F+kfq#WH^#WJ-^?XEL*Y}yv9M|0neYf zs`byaoz@2-JVeS%>K%^r8|m!Fh^owCK6s}bEHP)mf#~c=bR65#bLP4ZHWT;EJED6b zy3fCQ06@(r4UCey8UMaOX{-`D_jm~G`mbszX}9d(6C!A|A42Lo`bSlV+SkD%>z1Bt zQ0L$lsEP08)h1k;SBH$zj%%*Y!mVmGE~k-iz??VIpthOSIyLT(8nc|Y{~B3xRWk5H z;6~wHhDNPUu&MKP#q?9)k^piXgsO3ao#&vdC@|Ie4vJd1o}$h|wxkVq?gn)>o`VeTwK-c&7y02Up$AbKSyWnqK@V3W{2bZ1w~9!*xXRgW zX0tjESCyb4944e7=dcR*mSjP2XSFiXqX5*n8ad$+5vo^sQ>BsB!L>x((yJ;;OLa_c z3Q|ZaK@KiI;({3R(V5i94}zd2nyFQ|MiyCB3In_!NT5SwSIh~iDbZ^(Tyezx5?&!v zxeOMAR1cZ_mYcnBI=b~tIr9rXYE*Uwm8kq;AyJ2T4M9)K;7g*$Gtxap(v_HSF_3DR zg2I*_0`Yc--)hvz+NFc;xZX+Ga8HwNq|V462&m~+B~MLe#p7|qQX?B=e`w_NWAr-y zdIoK0%vMYHeGQa`iVb}#++U`$RUn8Lnl-YP&0sgpoa9pSQMqIfRE&y>q~Z!Fw_V;~ zY9VQ+L9Z*cQkUR1f}FDOw2p2D7OQdHH&>_*>W{orikCvw2L2vNBMZe+-o8S;*h=2o{bpOb~k{x`+FSW#VWTHGX_l8&L4XHC6;b4Vja9v+OrG*G<}ZMAAWZaC`n7&)vO zS>I;~cT)Oa5F1>DSo?&+O<1%^UK*hwmE*Z;xIIj--K35t^j>b|4)e2H7K{jLXBucO zK-WU2D27RyZ8YYIg2d_SZ^TNb`kxTUOZZO-{F{P1RoOo!Hp*@rs3iP*9H+Sz&$0&Ha@w6BnK_fdS$ALg5A8x3R!+2=s6|a=5v+&3k)|4dp9CG_ImQlAYAY^vl&&~bZ7tO2VqH)<3P(#As4&3L+Zn-7=t^cPcOW;z1Y=PX z&aZA-fTv+$f#W9=y}uL}7*;A83WNK^SQyc$h4m6fW9i5w&p6zm79J=FVt54$1R)>H zBKB1YtwJ1?Z^C6-(qS~Nf436mVU4V-_^y@sgc%SEDteRwm2kgTSaTS3yu6~i=0X%j z8j&@!7)ao{{Ock@$%yiGMaF!rljzMOtO2$oEqAgEbW@$$7zNVEuFD)JK8^*Y_{0#? zzVKd09G8W`%l`h*9!b~jSqnt345#kI^g!Jx_5e}5hJ0JJ# z)pR==6DO)S%Z&F~WPT_CrG<^=7|^b2T#$yv(4e0O<5uV439uSL9j0qMJ>fkk%dF=& zi}^{LMkcoO(}kp`D##GSowx`PhcJlePvu2>#xkeUVHs5-9(-7^X2P)=U-3BV28-MN zn5S`Usgd=fHLEo1VoG#6o8z+p>QCr$6_Qi$7saD!!ZTovEQp#n1@lJ|Ej%0sX`pvb zSfNrhAAh*v)7%Z!_+rc7IZ7xR#KrK{7Ih2CPns&T_`~I=qs7&m6y2zb)b=^=SxhA3 zDW(8Yjz%$%?ntMd^w3tFjUGr-Xs6&x%@VB6 z#;TicC`d8VNsv{8vrmCrBGH9>djh_z=oD z;77Im*ozDYhJ^tLhHW%K1{PA7^=P8UzquqHj;V(mFb4`WgY?b^Bbb$5=&D<_k`%G? z3x)oRHwhZ&ha76sZdOl)c#jo@?Px4FI% z%WfUW%ebU->=O=hE z^NH`-+ATe{hxd*8!q94eVZrY6zsq;z4_o<1>uU+J_WGVF8*aEIOn%?`=7|O0eSCGB zIr#ITFL&{s@%h(ZT-X`be`~+yA3yRHf@=h%?Cy?8x+NRrrDqv zxznr3x?fB56QQp3Sw>;CgjM{O{s}69nt}TZc#xQgXIG3L;~EV75h0KLvk8P$E^SwKXE|l*JgD%UX=Xv7!8-ep)T+(hOKYzzMhOc`{dw=RN@@@YxmC@3GT{lC^Y6GI)a3vdq0_>KE~`_U%u z2En~(fd(TXkG$|!n5v%VO*gOwT)1;gbUNy!yy>1ay{#cHshabVKI9-(^PBV*aM7Jf zFZ_}n{n0&t%9YaR;f|zQyiJ*fP&#mWZ(+r+N)fN~4NDELeT|cMn0|q;iuWZbM;}(x zUcLfehsBP&pomX|hI+2M6?$YtLp9e6Pd$NCtxGQr$ToZ+OR8JCfo|n#>3+R~i~WtR zaKEEO4mtg}=2qxI^+2Dts3R@@BJew08vN!~1P@Vbkq!Y!OBO=6QZ4wV`b`3)gXzD^ zTZ7i5HX`HKKn|UF=Wq$s76qttYOx&LfVY8XJ?i55>qi4_x>ZZ9?T|v2L-%aSYAmoP zy3LP!S!g>dN3zICHbdo*>Xxv7t~_0N+Xz;hg=2bp!{Mlvs@i!IG}C}59=3+xro#;j znP~M=`pPXMwTl+Nq1GC|d`9hJz-1Xd{&e`S9sV<*Ur|gZ{;~lhd3-b=wPrScDJ_Ci zvVn!8F6ci4TAV&nxRoU1FF~lb@{wBT70=d#?Bstx_8KS={vDhx_;+^x_w)Z>4e)>C zRF2;;ENeVe<=$dS&ndrn$z0{-WsP3|DN!zyg;&>lsoaN7=_xZPlyaF1(+*a7v{cEI zva)t^nX)`d+ecf=iBetbu8?6Zuu@*awBeP~D%F=K^y)Zw<{PtuzU(~gj|=IUFMmIx z-L4hFGcI#DM=5El>N#TxL_>z$JIKy|Etvk!`pm#I)j=>?oVPx zWo6Z%()e&stvi0JUa9hz_e~4;*ZLCT=I_y8XU)YH!e%yw`)Ki->V1{2{;sKpEZkMi z3a_sXB#MWB)z@dkL$pmPUg6)se!~>kYvCqH!-fS?)hV$t>IiLkxVC;RFWg{_2oI0Y zYNOkP4+0tiR4bxd{eOvmURqBL`8g{GBFmzT*(*^+?;-M-zR zyB64R^zzR28XP^haBjOg4?mf(ZNTkYVLjZm#qIN($d(-K?wWI>Y-6VK&Vz4$|LmpO zE7tznTQi{etEKL{yU#qq7tj0fT>% zU)=O#pZ#I;T=qA;^i|=?ZbMg>&tEasd(OewOTNo*zmq9nyU4SCe(tR&+q=9Rd~;Rf zq)r>oU2pYAX~z6ll_xuRe17XtgLlKqr!R2xT=+uK`Ka}WR@8p|YxB}qhK0ZL)7jTN zmPWGozFToOC2;?XmSg_Eq`C~9nfTJBX_xnu_8y^9+&nSu^|`O;cD(Bu?o)H&7Vmdw zp4@ZjtAIYUSG&jd?=&)DbElBGuPA>Db@91()x$UI{T|DD-8qk z*XXWWUTigHW3)DfqJ5PK+8(RpwXxyNwav&DJpG+6dnZetKPw(l7%X(S1s7R(O)dT@ zg}zD`f3MbUTSr7hwxqA|JPjm^6(>-pxj(fxvtrTFB&e)!lAgRF17 z_Uq>35u=jB-dT2G>ac@Hca`l+|8(qth1a*ae*Sx-Q~!8jnCq-{^{| ze6mBf`j3Y1coz&mS2X+mY2)sknA>7hCt1{y(KFt=_~>G3E6v&MNz+)$tK*Y&k3FV# z%Du6Cj4|nQzDKNg?}(6XGd{o7qyMR2Tb33d4_VUOrMN+e*`U2YD{4dJ`x}SNbTdzk zJU0HzheuwYydWamq8;(hkDn^`kKOidpXQ$gD~En@Jvwm9NYzevH*?Mx-M+7RG-lVm zrT2UNZaTTeW!7gW-)qogNnQhMe&++#UmY6PX~&)ToeO$>`NJRUcFsQ$T4&F!h5b8B zP1{vt#*t|a=B)eU>x|j&wmaS7*u!e`|LCXvU}sd_@x5R9sZP%eqX%SGpPrsIB;K@X z!Tf>W%V%73KX$n7^5K2814plQKfJiS?eN|!>y3R-cW(8Jo@AiUd| zXUEHKbn8P{ooZfA9VlVQdHIkQ&yXzwdMNitAt4)yu4!zg;kBBqxC^~K3w)wTF@u!!Sgg#8_H>Y8n zCI_NTSrgJeSXzB!oko+6KKMW}WyUWV_fDqneRbZ;AAZ!()+D2L$c|&pLZX^1itg3+ zv$0!W+Z5Pn-TLg?x(358ha@b^n0lqjkTVT?cTPRHy?>X+ihHL)nDj7 z>YZQNwVt0_x0!%)o(UR_^|fQxUuoy?K@x@eaUjY@x{H}7azL2 zKd}GT6KC8`Owg`-d*;R4FD~EwQ@S-WmrPRaMvGmA2| zg-!aR|I)#GM}>s^cqjir$gI#=UE3w^yF58=+Cld|hdy5u8)lpTi|N-Q^?*?SAw%cC z-Z?pP(pOui*1GUU(t`zCKI~qex9Iexb5mdSl%~Twm=14heN^t^qOyieiFF@yn*XYt zjOy4E1jv=u>XW;p5Zk|47QV+=OfSuJe<9zdEnQB^tCgh-wWag4rE~0+q)})~ zCu+M$*~w)!BmXKpn!ItsWeqF9V>6wlSYKe(Rb&&IrfB}`yzBxZlS+nR;dU0LNnJ;ZQ!Am?B_+Hk(wz%7Zai@!` z&E99o(1dpW^?>D6{=`4_bh+X2X@|W%)~~v6{93;^xXqe{LkyEruR7W@)liM3?Go7Z{Th;4O+C|yNM4znsvW>%L`9BoZQ{! zuBpkUYnz+hIr;5<@6C%t7A{Kk?&SHC+q82H4n)+xa_>;96ED8;ZbG!jVUPWXH*UJN z?flm@roIqAASz-+v$_+uK4|vqh0ykD)x{+=<1?cD{B$bxvYzSVz%c; z?<79FJZnOrxyHEobtA8JZDCk-U`T4_lmkKfwhO0R{ObO%Kh<3Edb7*N*DO5w!w`MU z)fZitzT8AZJQ|`GK^Raix4;Kt!w-^4rV(@}5*|`)zq-^D7WzY&5xeE@(<6FTgDEXpJ?wW`^>y}u`pjhr_-rr{gRUR zmh=x^QRExZaP-|6kF8xEzj)1)t1$`h~1{PbpC($#P!S#r_&y|ymXTm1CY1{bytK}voGSYC&sqY zu|xI@_s06uf!EN6IT9+vgO%!~r*D0~bC-Hzt;6^K`0`2lh$Bti=A9VV;cA_aMt41) zX{@R3=9Iizcx!D$c|eKZ({9EUbxUeLt4}+3JIut&GFi#1M>4~=-kq@F(cIRfytb8o zZeDSA`h%u@i^lwNDes$Aov*tu3H2zu;kjjcS^52}Axr!1c!M1ZI+)isYvhtU{yW<2 ze)!GVlYy&y^o~Asb)L(-TD5<1t0oU=pEWOHY?|rkY5UZf58F;NZ67pbSho8h)3?iS zZ(7%7($`&*7U|YCSv<%;d(o^WAN;&0IpSOo+0=czj;}WUaDU~fT`%og_~okP6L$uN z?@FsN$TOqnysNc61J0G5fAhyD_n$mY?(JG`Jkor?BkhgdaoO=f=l)SI?Yynv)Z(+b z$)oNTMNEF%{mbIfsh?jS`C0U;-8aH7*B|BU`mMY9$i%QY;|D)k9Xmqz(XY%t$=>;Cw+9rg zo_yqk-e<1AeYHWYe!FTGUEMJ5i}Ckv4Y+deY_(x^C(d>2^;?Yi8J?b0;$AN!^SJc_V=S37B*&&YYoE5Sc_#hprxPpPU)+X0*dQ z-8O9^G?5W)25FTga@m9O($80yp3;_{_%B%&{}Y?qrrO4|g7);s4F(;qyQ$OD($w*3 zN$uNeyT!JOkBo|J711`bO?VS+BjQu|2Rc^T^y_V{@QOUYK#l7vbb+P3%o}y6e5fr` zeCzN+$p(eKZ;ESu)Eq*a1KA+wPDPu8vHvBNAeKzwA5^8rq7OZRf5xFYg^S#*0Sj~I z9=?6CZo>U{kA3~=!J^_ivK5`1@9qA|*|4N}?M`g1c_eFnL-&Zof1aDr!)=mRcH>*y zcIErGD;{@q$))4d?{|1~a^B;wo11CU%eK1_;O(PTie^d^4><1&3{$vG2dK1R{NT?z^;cHspYhs z&ayV?@-T4LYvWIH2Tu>5eLr^0*zWZXMdl8goZna5PgoTv zYvKMWhx^Cwn|Z!fG5Q#K7>t$NKU(ho3Fj-~LDGs6D`N5`_mXg|h&w-PIwSv*V@3Q7 z*_Hi1^pBas<5o@UA!VU(tJH8jV2)R+3dcA@FsmxtC0`S|r81}@(DVB_wob*AlIlR5Zk^8G;ra$azW-2LF@ ztan1EPS~y)bLp1_TgH4=JUeo!9?eBM;>Y_c^ ztKo@XFZ^Se$C7XM`FCF3tK&Oeo9v(Eb$Ct7=<+A^Ke>V&bst@5AwbEHIz_Q(n)| zZF=Lm?*_SVTk*|nMO}`BYL2)4=*#YN5+9p~w5m7a)U|EduedMn(xUm6o^Zd9z~DDf za)!8UE{&ZCO^J2Hn(?Z#-Rh(bo)oX*Ezf8i}z}zHB*K+h^be1 ze$WYb|CwWT+JRfOan*kuI(1TdY_%^EcSgCr)9u^28_rp^{hOApiuXEs>tT=5<9izq zjTkVr`%eRUOo^CO-=@o%lTn(!^|QHsUfXwY?T}MCe^bcey1z{+*>zF(r2D>RL5*|# zEOi@B-`qQET1w{DA-|7m(CJ?P!VSBpcT4R0!l~*@Rh@GtJk0(`u^?{z-IKk4H(h)E z)2(m*G`fXH_`R=^RC9Jrb!&5@C~(Wo#m75d4~zR?-GgtkcCVK0Q@hVCI{xItkB-C* zd8y`7nM-nTqBK%{kCE!iU%CRS9Gd&B&xu-hKoBiS!Ctk08@ZTNu z>|1ql&}-W&BJw{q=siO=Tma=X&f&*;{#0T!Rr;E?bXEl~K&iHqH2%R05dO>|Hl=rh z-~xzENeh#%XaBtqfZp#pd;oOAhVFyV*}U*00MGCNxPCwN1+OcIFD>d)IzrKD*{T%{ z&F}nU?Xid9dp>iyvi^@p{qyT+-`RgC`^d1153k=V+WqmHZ#233+lP0Cr}uYj`9fKo z-{bewUOo0;sNeLF$GUcnS@O-`T7M4y?WJ067B%kI+jh5`&nKJrPB=W_r*`88bqV}w zadEFXscdsp^;!?Zic1aW*KZ&G-Nz?>{&X}m_twTW4(|Bb9mSdjvj#T*aL4b;%LU6L zdNupK@8+;Sc3hhoxhpf{^{@MPz4_yb#o4o8{c~96gl`Y^@|@H3O7!hIy+-JljcJ&F zVCkxr9Un~?w_w=pqP6*_FE9Nq=ttfLYD-K6X`ap(4uCBaeZ?M<#_ z-Tvjl-tqc@O{$O9ME4)La>QGz>D8yz{6M$P_vq_idiQv}rUPsH}T%(4kO$z z4zVo?cGG>eC8fT3_m7v^#p~g&tLL`=#n7YZ#;4qjTY2QOIx&ftru^38^;N#v+B3Us5xpZwWX1oR+lrNqJWxDWl}k%+ zg}kn!20K>bvS|0=z`~_$LGXo|Q}z!s)YX*3Pgh#9di;O$oaJ;%=7L|#b79vSM|RZ) z*RC$^1*5dsWkhHP*$L4iL2K1UJ2sw5S$IorbFGIcfE>Fj037UVUFloyb5n?HGU+3V zZ0nR%PABko=Lzj%<>ek)H%g{(U7=nfg=JI9otGw1S5aEZjx4w}@cu}*>aFV3X_6AS zV$E9lh^+&nmAziiUb^nu&PN|ltnr=ZyNGF5Z(UmIHt%P@wyD$iH97p{oxG8gU9%f) zyEEz9i=!XMyKXr7$H~hJOgViHPd?&bTag&kW%3()eqHu`l5xw8S+>Nmch|RUzwhXn z!q!0_dLL< z#E#0kD&wcV{Q`^C9-wn|oD66~qi>4D^3%xM6_2k}DgDt=1Z=JA3Te|3f zKsTtm;^4H~QFE+ewO91Z%_6%!eBG@?w;=q9Uo^`Lk8OL_uebZ3CC&y3-vFF4tZdj< zcYoaDI_R?Bss=OM*ZkRi=S`JYy>TBuh_lSz-%9iH?#^lB-*x%s+s5VjS9W>6u8fF{ zU!}QLcgXI`2LqE{c<{-o*Xy_Md^tmY+E8mrbL)iT4xvi56RPmnU z6ZTyFG9Ys7$Mb*A{qFOkj_SdA?K+RnEctn6z@+8cImaiNMz>b=ou-*{c+34aHmysj z6?A0N(EaV3ch`1W{z2SJ2UM$@O1RDIGS&lcZot9J3{Np%N(68he-IUBtu4p+t{?6yTd-rKdqS-NPCF?1O1q#yZ`_I literal 0 HcmV?d00001 diff --git a/Plugins/Plugin/DrvierService.cs b/Plugins/Plugin/DrvierService.cs index 7773bc6..0b0a937 100644 --- a/Plugins/Plugin/DrvierService.cs +++ b/Plugins/Plugin/DrvierService.cs @@ -126,9 +126,9 @@ namespace Plugin } } } - catch (Exception) + catch (Exception ex) { - + Console.WriteLine("驱动加载失败,一般是驱动项目引用的nuget或dll没有复制到驱动文件夹"); } } diff --git a/README.md b/README.md index faba6df..9eeb513 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # iotgateway -# github地址:[iotgateway](https://github.com/iioter/iotgateway/) https://github.com/iioter/iotgateway -# gitee地址:[iotgateway](https://gitee.com/wang_haidong/iotgateway/) https://gitee.com/wang_haidong/iotgateway +## github地址:[iotgateway](https://github.com/iioter/iotgateway/) https://github.com/iioter/iotgateway +## gitee地址:[iotgateway](https://gitee.com/wang_haidong/iotgateway/) https://gitee.com/wang_haidong/iotgateway 基于.net5的跨平台物联网网关。通过可视化配置,轻松的连接到你的任何设备和系统(如PLC、扫码枪、CNC、数据库、串口设备、上位机、OPC Server、OPC UA Server、Mqtt Server等),从而与 Thingsboard、IoTSharp或您自己的物联网平台进行双向数据通讯。提供简单的驱动开发接口;当然也可以进行边缘计算。 * 物联网网关mqtt输出,支持thingsboard * 抛砖引玉,共同进步 @@ -53,3 +53,12 @@ ![6 gateway 修改设备为自启动](https://user-images.githubusercontent.com/29589505/145705269-c816789c-cd67-4c01-973f-ae4f10eb41d9.png) ![7 thingsboard 查看到设备和数据](https://user-images.githubusercontent.com/29589505/145705270-31d8884f-7f6f-4ff5-a6bb-1d57a97012f4.png) ![8 gateway 查看到数据](https://user-images.githubusercontent.com/29589505/145705271-cb80b80e-006e-4312-8843-6d0ae9457cb1.png) + + +# 声明 +## 君子性非异也,善假于物也 +1. [WTM(MIT)](https://github.com/dotnetcore/WTM) +2. [NModbus4(MIT)](https://github.com/NModbus4/NModbus4) +3. [EFCore(MIT)](https://github.com/dotnet/efcore) +4. [LayUI(MIT)](https://github.com/sentsin/layui) +5. [SQLite](https://github.com/sqlite/sqlite)