From c9563e9bc9fa024f88205e10b29dfa31cc5d2216 Mon Sep 17 00:00:00 2001 From: aleks Date: Fri, 11 Mar 2022 17:12:37 +0100 Subject: [PATCH] esp-modbus: add build docs support --- .gitlab-ci.yml | 65 ++ docs/Doxyfile | 57 ++ docs/README.md | 11 + docs/_static/404-page__en.svg | 260 ++++++++ docs/_static/modbus-data-mapping.png | Bin 0 -> 49888 bytes docs/_static/modbus-segment.png | Bin 0 -> 23167 bytes docs/conf_common.py | 20 + docs/en/applications_and_references.rst | 72 +++ docs/en/conf.py | 27 + docs/en/esp-modbus.rst | 662 --------------------- docs/en/index.rst | 14 + docs/en/master_api_overview.rst | 300 ++++++++++ docs/en/overview_messaging_and_mapping.rst | 47 ++ docs/en/port_initialization.rst | 35 ++ docs/en/slave_api_overview.rst | 215 +++++++ docs/requirements.txt | 1 + docs/utils.sh | 18 + 17 files changed, 1142 insertions(+), 662 deletions(-) create mode 100644 docs/Doxyfile create mode 100644 docs/README.md create mode 100644 docs/_static/404-page__en.svg create mode 100644 docs/_static/modbus-data-mapping.png create mode 100644 docs/_static/modbus-segment.png create mode 100644 docs/conf_common.py create mode 100644 docs/en/applications_and_references.rst create mode 100644 docs/en/conf.py delete mode 100644 docs/en/esp-modbus.rst create mode 100644 docs/en/index.rst create mode 100644 docs/en/master_api_overview.rst create mode 100644 docs/en/overview_messaging_and_mapping.rst create mode 100644 docs/en/port_initialization.rst create mode 100644 docs/en/slave_api_overview.rst create mode 100644 docs/requirements.txt create mode 100644 docs/utils.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 70cf5f6..ad0e049 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -83,6 +83,71 @@ build_idf_latest: # GNU Make based build system is not supported starting from IDF v5.0 SKIP_GNU_MAKE_BUILD: 1 +build_docs: + stage: build + image: $ESP_DOCS_ENV_IMAGE + tags: + - build_docs + artifacts: + when: always + paths: + - docs/_build/*/*/*.txt + - docs/_build/*/*/html/* + expire_in: 4 days + # No cleaning when the artifacts + after_script: [] + script: + - cd docs + - pip install -r requirements.txt + - build-docs -l en -t esp32 + +.deploy_docs_template: + stage: deploy + image: $ESP_DOCS_ENV_IMAGE + tags: + - deploy_docs + needs: + - build_docs + only: + changes: + - "docs/**/*" + script: + - source ${CI_PROJECT_DIR}/docs/utils.sh + - add_doc_server_ssh_keys $DOCS_DEPLOY_PRIVATEKEY $DOCS_DEPLOY_SERVER $DOCS_DEPLOY_SERVER_USER + - export GIT_VER=$(git describe --always) + - pip install -r ${CI_PROJECT_DIR}/docs/requirements.txt + - deploy-docs + +deploy_docs_preview: + extends: + - .deploy_docs_template + except: + refs: + - master + variables: + TYPE: "preview" + DOCS_BUILD_DIR: "${CI_PROJECT_DIR}/docs/_build/" + DOCS_DEPLOY_PRIVATEKEY: "$DOCS_DEPLOY_KEY" + DOCS_DEPLOY_SERVER: "$DOCS_SERVER" + DOCS_DEPLOY_SERVER_USER: "$DOCS_SERVER_USER" + DOCS_DEPLOY_PATH: "$DOCS_PATH" + DOCS_DEPLOY_URL_BASE: "https://$DOCS_PREVIEW_SERVER_URL/docs/esp-modbus" + +deploy_docs_production: + extends: + - .deploy_docs_template + only: + refs: + - master + variables: + TYPE: "production" + DOCS_BUILD_DIR: "${CI_PROJECT_DIR}/docs/_build/" + DOCS_DEPLOY_PRIVATEKEY: "$DOCS_PROD_DEPLOY_KEY" + DOCS_DEPLOY_SERVER: "$DOCS_PROD_SERVER" + DOCS_DEPLOY_SERVER_USER: "$DOCS_PROD_SERVER_USER" + DOCS_DEPLOY_PATH: "$DOCS_PROD_PATH" + DOCS_DEPLOY_URL_BASE: "https://docs.espressif.com/projects/esp-modbus" + push_master_to_github: stage: deploy tags: diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..c3c933c --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,57 @@ +# This is Doxygen configuration file +# +# Doxygen provides over 260 configuration statements +# To make this file easier to follow, +# it contains only statements that are non-default +# +# NOTE: +# It is recommended not to change defaults unless specifically required +# Test any changes how they affect generated documentation +# Make sure that correct warnings are generated to flag issues with documented code +# +# For the complete list of configuration statements see: +# http://doxygen.nl/manual/config.html + + +PROJECT_NAME = "IDF Programming Guide" + +## The 'INPUT' statement below is used as input by script 'gen-df-input.py' +## to automatically generate API reference list files heder_file.inc +## These files are placed in '_inc' directory +## and used to include in API reference documentation + +INPUT = \ + $(PROJECT_PATH)/freemodbus/common/include/esp_modbus_common.h \ + $(PROJECT_PATH)/freemodbus/common/include/esp_modbus_slave.h \ + $(PROJECT_PATH)/freemodbus/common/include/esp_modbus_master.h \ + +## Get warnings for functions that have no documentation for their parameters or return value +## +WARN_NO_PARAMDOC = YES + +## Enable preprocessing and remove __attribute__(...) expressions from the INPUT files +## +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +PREDEFINED = \ + $(ENV_DOXYGEN_DEFINES) \ + +## Do not complain about not having dot +## +HAVE_DOT = NO + +## Generate XML that is required for Breathe +## +GENERATE_XML = YES +XML_OUTPUT = xml + +GENERATE_HTML = NO +HAVE_DOT = NO +GENERATE_LATEX = NO +GENERATE_MAN = YES +GENERATE_RTF = NO + +## Skip distracting progress messages +## +QUIET = YES diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..daec062 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,11 @@ +# ESP-Modbus Library + +This folder represents the official documentation for the ESP-Modbus library (**esp-modbus component documentation**). The Modbus is a data communications protocol originally published by Modicon (now Schneider Electric) in 1979 for use with its programmable logic controllers (PLCs). The Modbus has become a de facto standard communication protocol and is now a commonly available means of connecting industrial electronic devices. This library supports Modbus communication in the networks that are based on RS485 or Ethernet interfaces. + +# Hosted Documentation + +* English: https://docs.espressif.com/projects/esp-modbus/ + +# Building Documentation + +The documentation is built using the python package `esp-docs`, which can be installed by running `pip install esp-docs`. Running `build-docs --help` will give a summary of available options. For more information see the `esp-docs` documentation at https://github.com/espressif/esp-docs/blob/master/README.md \ No newline at end of file diff --git a/docs/_static/404-page__en.svg b/docs/_static/404-page__en.svg new file mode 100644 index 0000000..928ec63 --- /dev/null +++ b/docs/_static/404-page__en.svg @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/modbus-data-mapping.png b/docs/_static/modbus-data-mapping.png new file mode 100644 index 0000000000000000000000000000000000000000..18fd2cf4fe3132d018be509d37121d2c764f58fa GIT binary patch literal 49888 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

xuHe{x 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 kPt}jvk(0vx zKqK)MuXRc-U5-e{%|4@$$Npr4g^thsgKEYN9SZFV3umwhP2u16@9F=0JG|ce*<_2Z z)-GN=@98YFgpbC?&)i@6@z-CDJa=4Pw=K+X-}h%rMNcR$y24cSLSLc%Vlkw$_Dq(( z^(c=^gHfSfVWI=yY3bnqpZ=YZtEzr)z9#7Fw+r%5rhLA5)qCFgGmj* zXX9;e|2OH}-pUDk4mHRqdFd*&|6?kW&`sZ-5ALY+ueA91M7YqL^9T=gigodKefhX~ z3pTgQm!7)0bbI^>O?97@k5y-<^V@rY;`SJCghP!3pWsrl3C{J5mOXrbZ-OhYd()l)zkHk-8Bhd0v&zd+5CHYY4(1%biTUV(`EeL{nDx}`5NRG`RlQWLltO1;3Ut9 z#2wJiXKLre8*OGeZp=nttTx;pZD9p6gl$w`23!oPZjhO+BsZ|n2RLz)aTVG zdGn}O?^0}2;0Ddw7T)+ZUHZEHg73?=U9$dtJ%aV4`1HP`SF!}uL5n74GFtLXm+tP~ zvcJ^yZEmFJ=EJ`q?&ms`!Bbz?)SW1^{?UHz3;zgG9G(s-0YgAsUZq=8;3(-qYM*+ko&&B7ucV> zzqsT7FTU#a)BjH9>-NU?ENc>i8O$Ieeqtf>Qjx9OwjI0Mv**Q&7ak?Q4>z4}4(iIv z&OZHX&z5c5Ce6QJxiUDzzU1vG$*TWv-f7I{@Ztflu4<5B3W!l?m;2yQm6D=zrO4gQ zZN;{2W><`SpJ+QKZqc7_w8%m~I5=3+FvKgcwzhWB(+wAjO^l2t-AKN!xx9{}g&DMJ zBh7)Yxv=n!f!QoZRz}M&4tHl|atX`L5qIYMtl0DIX|P+!^P?eoK~D|V6o*7d&;D`q zaM7!F2Mf>uTngKh3--{&m-)R*EysfS6I)Tn!u9LVYY2S(`ZdxxuxP9?ltXyU9;$gS|ugnadTlBKsp#oAu zw19@Q*q>Yg*;%qBIeD5yUv9?X=C_XbAhSd~rz*rerYm30US{*lw#z0W{f`dsmDJXY zPa9us>v$pI-FBN?q^M z-*+d@`{&Po8y9(bP5LE`>2(|}?U3bBKYM&xm;xgja-bcU$RvxKmw63YSiu9}PmMH; zd9V3yw%)~a;`^5ePv<#42{CXYyNASim8g}yY@V}aQK(E3DAwzfT?Q(lT3;F$nzJt905yj+raR8e{B~~3;-z*G+jF%R@jrWd z>8OU;lEi}=E_{1$J-6atX7nXC{O_}m#dgj2>@!XYZ^`{LuMQMISI^b#x8BB^x#!i^ zvp(vb@U(HH)`Nke;WxMA3s_3X;pZ;5U3cL8Ca1kT*B9%2*|O~5o+(WG`!`K^yVZ6Q zXk~6rs>jT^vu2%I^3ZJdtJ1mW4W3-^zx}0b%eHM&@}-YT{rb0)zs%vi;>r8O||K;mf z*Shy}a!;mDc<*L->m~airUaQ78$X^2x}g$Oc5B{=h*@%SUf!+Sn}lYc zZCE5cQ$|QI6t+b9aE3MmLqLAMzUI;uyLR20DOSWG0n)_@(zRGt+PHt^jr+T--l;Vj zDF+cXEdg3#qfrXYHj;**blg{#bL3M{K!_6PMxSTT(t^A$URvs%SyHm)0%)!Fl#3Sw zL4$spOIPgNc{5xNe7OcgL(sENKR9pQ-ely>8-)6kGVdk&5J0!-)z|bKD zijJ)d_A~ito_J`@9(O{>>F4_;#veXBS8P9U>UD1Jm6aiWJlC$a#$7*iZtj(pB75!a z=2fl@)_nZ>eSd%O@}MtO(a~X&&84L#cP=IA+<(2dX2Nr~ZLJ*{i#A{WyyxTZn0wD= z&AM>wdHaU*oE`kgwjDS;f4Qx6c*@>w>5sdvaELw;s7MqRo6RHIH08j|xz@%;TX!yN zODlVKX35+aZd0V?tS`NDP%o?b?rSrzkH88WhEUtZI12! zSHY3q5vtSg$(^p6fBR7B$<(Dbmov2{zkYt9-?9Gh&(Qgw?$%x2ST)u3!-waaFUh>A zI#Dranbnh`_{%?;S`#NXJ)Ab}LOTCanSJ-JO`Ev(;k0QoACopd`eHMC#pxNnPASJt zCa>K8Zfe_$WiFd09Mx^F(|o$EaW<$;vXb?$Xu_ju&SD}vTc)&jr@s0bI{(xO?&pao z4G$WrTz>KN#mkiHFEzVVgi=o{F2AlCdQN0k8lSav=(U`e^Ln*6Ejf3s?sj37tTOkq zx<23Inzu7|Z7+K8@ci==>KZ)zi&p4wobs}gt=Cyx?XRunWwVIz=`(fbym)hFRerHA zFDND_x&+U9IBlBGOqoYu{>)4FPlxk8-MI7C`uXX8?o(`wPZXV9 zIRCoBbMI|u!}K^n7YQkVlg*u?jNSVKPAUHX-78;zefok8CNGncZ+kog`NZSW!FBCL zr@qayobonhNkAp1=Hly#S^GIBJG{HOLn-}Rklf@cfq9$%McV9t6>NFB_U|_CAIHyc z3bJfo-mR?vKcz(S;?a)#mrq~3pM3Da|Lae?P8*-qJM}5|wAbD~$3veUw}wmAwrv!! zG|~0nTFX{->#V7%o8^`1PhRDkY(2Z$?xxxLBF74NI8Rb)ce#05J$Clh87ybYClE;kHy{nd-Bh2 zT{21akoNq#{R`)P|NHTyj^Y*9!168`umrquio=syZUq1 z+mfs4S<=&%xbZ&`V3-hfA)Zk!&+^OeH6i}5(~HF?$9Ps6-CdqOOIKOR-_m62+pT9p z?tRYK_5S>NnOBF-98Z{KX`-9Gl~sQKp8Emo`HLjv)=4Tax@}x%bWdLXd}NSCMELZr zCwuHJFXuk?IycwyG=HV%ihWb2oW7VY_VtV7lM7`$>xvgGzOSD-yOJk6a;1$~^l$dp zuX>(G&J$jAdvjM&`c;MJr%hgGX^XeG2R<#x$mqCr`}WF>8z-(>wJNFpfEiPx#@iLE zRxLVq%q=TFfBC|N3lG%tJrOY2zArdYXb0~)-b{(u13A(c4Um^zZ2B_#9evf3w?@y^oiQu>QRtx7lB(#!9E{lkGHDP1)8nGF6GQLsJ~*-rQ;Y`oC&o zj>qF!mRCMn7i(Oc&c|*UzwT_;q8l&8L<0G)uZqMNmXZfK?-_h{oRNoyXl}2}qSKNQUV*Ams{Zq^;b(a19+p%fs#eSJv)1GXa zGpq0XYtu!yon8p}pGd5@(Q;J9;@29<)9rd(IzFG`ENYo&`7M3%=Gxk0R+VBt`;I4N z%*x!tIc1OdrPmTZH{?bQ^2n8IWv0t)iU19B z$vm~ZHhrcO-{He*IyTkI&g;yami+3pS0vXHH}xX%v&zzD5$Q{pG`uW5G1Erh@`=s1 zyXLvxUa3E~{Wg%SUG9H*l9y)tvZb@sCK{bRSGo267VR}}=gjC+Kk;)??q@CW7WXGa z&7Xmst{C|4Z|<&~kG54-I_BG+&WtNkegD(=t;wqW`tw(6r1SZnXS#Cw%KT=H*6Uju zdshEl_Ibhm%^K-extm))S>*1X{_6ej${r4er+sr+Szam-*DP2T@?^u5jk&X!85mr4 z8dom689t!~{|p%;v*j5#y|pTzwol|`Sg_|}Irq^uiB_FH$m=ph85lY`R-iO`W+a_{ z_)@o4Rlat0oqD!q>it#sJNNo_TsU!IlfN+{=ff4!be(P z8iV@6XZez!i7c6Y;;BHh5re}G$m(V#frsupIUYUzD7IQ4qAN z#^yluu@fgc40E%yvwhaGF8JbT&(b8cU1s0c=J%KN`{jB}9P$_#grhxO977xxge3F- zKKLDZ|M|h+`#f&H|DCjo_2aQeO1|Gp(q+H@yLNSkDa%Usq6n5fPXaVuWo2b8Gre8; zpdH|MX(=6>Y!j!<^_N^));E~}G}i&@FxSs`Qm|m9;nt=K=xBx2Gf-auGFri)1Trq? z&~op`SFVKktleyEWaQ+va+2V5joZR*H-AnE%zAF22@9Q{?Nbh4|Mw(ODkIBht%6)l zO769fa&K>R%=L1-$zZ9&6t}q%vZZO?KD+EFzr%{INm*H^e(j5s+kf78s*c(x=*&i? z;_1t+H%ji**48eWvgp^Zs_g17HCwlCo%#Js(I?&8EpIn}&~kqE@P4Go^Bpmu_25dN z(c(z?Cjt|fO(^>D?7P*AC-MIcwl-B%{&9I$;g@gN&(7eX4eB#BI7Sxic@nTs{r_6; zxpIH@c^X7iefji7LQ?YNiHSV3EjI1g@oDCSPgn7p0<)*|GPu$;+Fcy62Lv zp1W@4NzKVi15c&MfI9jW*VrvxmT2w)&(!Xk02-6zIn@;W!nWOIXVM`f!B&}~NXMf` zueEJCvG9zxJ-7Ya&&I_w&Rgo4A9~SVXIQMa>e*DAxFU>{KV0y70N7uJ+Z| z%VG1pwK~r{dZa#I<;12STi)wa--EX3d~5y}SmxIAy>}oL^hpz=#s9^yM0T<=b~Q+ckS}AQrhfsjP;2?;|cpeClV*IeEjr-GirvN zaJeyqhNnWi>w*c-WoDJ_GCA4$wsybqM5FmDbJm{?+OypFeqbEoN zZIDXo%j&&rb&L5I>GcIEJ$3^PPi(e3p{V35=)&h6De(0Dr;OvrB5pG~Xexmg8%}Tz z@cI$i#(k-Mb$z6bz4v6>yIVH@+_$Fjz&lajl3%T7UGs8ZCeE_F6BC?#XJyEI$&|L} zD5KNv=GU*J+)Y36NF!bFF79L#G`K8$qTGH|;7EV{Pj9K}lD+A(q`SL8V_0FokDF~t z^weY!Q)pMX=)iYc_~!mIy)p~tZ>c}7)hc7G|GsbM)1{5Rd_PP7+8sQ&%j)rB4Xdd} zTngtU_^w@@dggDN%LHeZMH5bh^DP!TvibJ*i`wy#j$c;COWu#4VdwliIy`-A?WX0@ zNgNIApjAQG z=;A4;e0H0rf!0M`?CX{M2wJ3c^I`3|qC6cvv8VdC;)+fb9skaE zYg*gBd)LxL9d7^aE@ZOwiI>U$&6gWrr*FGd#CEAj>HSJ|5O442aDS1IcMI9&7c8H0 zO6$4i(iQ*boj+Q4rqFd^kwmM)zTcB?|NTC*Q9k-|Z*B%?NdM)d8xQxqVq9?Ir{b$N!cO&0TySAsMPAi#rFX^6V`14mw zyGttnxTvj*+vO3iAFK0vX~=}bUyl5lH~nSn`I@$QL0$7!uUi(r|CgxMmj}#WPKisc z{C&>)rRLHX6}QVj>B$$(kWM_2c%#YUm%ZGnC;wj_v){gK_r@I^z3B`b44|%vMGyb> z`p*`+SiWT(xeOmHRs`-V=!yMH15#;w+7){&nB-|JQu(w@M5Q+adl9|6f%1f3?Y{?&Za02ZS@zp3-g{PB`}d3YrcGTr)wy=2uJ@0dZ*O-M z<*Pq2kS*Nv*80nope?mO_g*u3|L3akmnlJC4yD@(N9~<(_`?$e9!Hy-U$0yKy*1VN zl?GJ~vw27+0=L~+uhFNX=sBiQ(QPT8m+A_sM(+)ead|Vu_bK&&&=XQs`&7EHN zWJ=W^v{<%}X!=P3zedgPXeZ7&7UZ&rC zd-Lt>Ij2+At9L2uy6?a7Hs<1%V-w2jzE7XMth|0_X4UtuWy#jzCTj~%K0NeAa+gJ! zg>m#AgIYD^&&%i4^A$-Hoq1nrSv)_{?q}qah(~dMUxoL4*Z4K@aA$A*#LUy66zt4z zH$n6NRDF|Ub?XlGX*@BAceJ_uKl%0h|7Z9A@_v18DFZ{j17C0-*GtKUig^!wqoSiv zo`28c;(ej+#qRt6%=Ui#@jc^vmzq@E-}_xxybj!nh&dCPcKVX#&C9;(^J)%V&s!&W zdSTD;*rFH9Z6_T5&@{dK+2%cyTm8FkpE16BrS|z;5H{KW{OOu4aYZ-k&p+SvsbJ5C znde(}&Dk2Fq-A-=UEq8D=kIg(Z~uG$53^PMgR4!ybQw;Go=EIUoG2+Nskvj#j;94F zDJ@yq*~;f1FZb~%`t#&AxXS$H`FsAQ^WXLb^`td%JebQ~IdiSv+|7r7zxecnGkKqk zRR#i!g_^WWlPMCUSMAR;zsiS+4pQ~G>*w} zG(2Ij^wF%Xt+g~-&V6q!|2!)ztBx1jd`@2Xt(^Jm*RM4;fBtUll=QxO4lJKL0zw2zwXosXNf%Zqc%7?b<4JGQLg7#*G0?OAAfmO z?DkHEqwIeLIJ!WyBI&gg4rd&CpSN^<&BBSl-v5kIixnw4VYvT!>Z}_-#6e}8vGkpm zpFKx|I5gYYum35p`dlqL@kn<4Cj;Jl-(98lyzk!f`E>lLD?cZ1`=2@GU^D;vg_Hk3 z&(C;wpw8xvT!R3#1vrWEeBH~<(Ypd>UV8Oi?No`L;-t;3bLJiO{&)Lo*!oR>ml!_% zSUhLlHi5hIrkZx@Y4CwUNum8Ehy8iWIrl#oxn24p=WPF7X5YUH^67tXd-LV?pL+0Z z>D!57L41#U4%h$6wc7eJa+k$7;a?ewk^O2+3Y-pn!Cjs$IZel#Qn=>*-28Ov<`SLc z;NakS#+_EnByD5Q?Rjz`Y`Pz3}uihTC z&Nd`aa zUt+$pPj%k8c=cM{s+$uI{{Zz#Kt=ltrXMk-rh9)~U4LOAyZni$YnfHo5=72hzS;Nr zoX_9e)9=sLw0~DDQg%eQU8Zi{YAyy1o)d{)AEUywo=!;rx<{^3@$)8=lQUf>A1M5} zOm(T*mrr|L&cpoD%5mDun-RP+@^HyfQ0?`#{h!5W4^FYIZ%@D9`cBGf|MOH(4K`DC z*&dtFd-5;7&pTZH?^S1yz@`;)RUa51U%I;Fc(nbGI-SBF58D;m8AK#cBzhU%eVjho z`L>&`+Qr9vbfV%aD>7G3(eBK=9ys|$;g_#pEnk~MqVUE!T@5|tpi~lQZJ)ETMTSYh z7TVP^GO%59H#=Bzed?5Dd(=KAZ@i>BA-?$Rrk!%~3-{l@e*O9Cx|zq%7yioRexD&# z!Y0(h4(eZlT4JDU*8^&lrXT+L`g;4-Fad$te?){YE1OQ@J0I2b1G-Cvsi?z2MIYqw z`6r$rpK9XhP_a#aoupoE+4*ftOfo<8)#>NI(aLdRH+E5L;pt{RH zrO3kA*x5^S)tWUttJf;$h){g1T0%UOhJxauu1tNzf$xQFY=5T0m`GsE0hB zPUu*%bLY${L0d{*21&`v>TUt8a!xHNnKDf;*2_yXD=+WcRCe%`57R_=aNWEa8MP_J z^Vz@6si&ub)`HKqE?;+H8#6=Dxt+z&SM1+Ef0}Oew3p7)bRwNLZ{ED`lQSQ3NIWSz zu_k)^xgQ0Epjj2^*C?|p^S*-Gl7~QxP(Ffk1IaTfAJ_1M&-$3)%+S>jnv_G?XE6yJ z8St@OL|B0KxhsH%!+Ah6^N_FywUV83@o83x)>JW_Oq;7#J7|;I09!iGa3az^;eP zJMx0sk)R=M(EJwz!w!(DAnsq<2SCO$Fo1UEJ3#%6r)SDsFcjg__i^Q(OBNRvOOU_+OxP|1+V~U1RK$^2D}G4joiiwSoZLbE)JO9;9TWsQYM7>g zJix$kz&8P`X%c7z4H_s66F`x|00}XMhAW_ef(9qU5;@S30TAypD11eP1p~uMcF+bK zNI)@o*djua!QmAs@E92Kik=8Gf~=q7HseUG4hIWUql1gkfBSz1R)=HHy|QNj#iFa9 J%Q~loCIHv1NvQw; literal 0 HcmV?d00001 diff --git a/docs/_static/modbus-segment.png b/docs/_static/modbus-segment.png new file mode 100644 index 0000000000000000000000000000000000000000..80fb6bfe6ab5572423ce46e81bf56ceb5864d90a GIT binary patch literal 23167 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 z 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 O1B0ilpUXO@geCyH?)Jn0 literal 0 HcmV?d00001 diff --git a/docs/conf_common.py b/docs/conf_common.py new file mode 100644 index 0000000..bc44ed1 --- /dev/null +++ b/docs/conf_common.py @@ -0,0 +1,20 @@ +from esp_docs.conf_docs import * # noqa: F403,F401 + +extensions += ['sphinx_copybutton', + # Needed as a trigger for running doxygen + 'esp_docs.esp_extensions.dummy_build_system', + 'esp_docs.esp_extensions.run_doxygen' + ] + +# link roles config +github_repo = 'espressif/esp-modbus' + +# context used by sphinx_idf_theme +html_context['github_user'] = 'espressif' +html_context['github_repo'] = 'esp-modbus' + +# Extra options required by sphinx_idf_theme +project_slug = 'esp-modbus' + +idf_targets = ['esp32', 'esp32s2', 'esp32c3'] +languages = ['en'] diff --git a/docs/en/applications_and_references.rst b/docs/en/applications_and_references.rst new file mode 100644 index 0000000..ca5dbf1 --- /dev/null +++ b/docs/en/applications_and_references.rst @@ -0,0 +1,72 @@ +Possible Communication Issues And Solutions +------------------------------------------- + +If the examples do not work as expected and slave and master boards are not able to communicate correctly, it is possible to find the reason for errors. The most important errors are described in master example output and formatted as below: + +.. highlight:: none + +:: + + E (1692332) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x107) (ESP_ERR_TIMEOUT). + + +.. list-table:: Table 5 Modbus error codes and troubleshooting + :widths: 5 30 65 + :header-rows: 1 + + * - Error + - Description + - Possible solution + * - 0x106 + - ``ESP_ERR_NOT_SUPPORTED`` - Invalid register request - slave returned an exception because the requested register is not supported. + - Refer to slave register map. Check the master data dictionary for correctness. + * - 0x107 + - ``ESP_ERR_TIMEOUT`` - Slave response timeout - Modbus slave did not send response during configured slave response timeout. + - Measure and increase the maximum slave response timeout `idf.py menuconfig`, option ``CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND``. + Check physical connection or network configuration and make sure that the slave response can reach the master side. + If the application has some high performance tasks with higher priority than ``CONFIG_FMB_PORT_TASK_PRIO`` it is recommended to place Modbus tasks on the other core using an option ``CONFIG_FMB_PORT_TASK_AFFINITY``. + Configure the Modbus task's priority ``CONFIG_FMB_PORT_TASK_PRIO`` to ensure that the task gets sufficient processing time to handle Modbus stack events. + * - 0x108 + - ``ESP_ERR_INVALID_RESPONSE`` - Received unsupported response from slave or frame check failure. Master can not execute command handler because the command is either not supported or is incorrect. + - Check the physical connection then refer to register map of your slave to configure the master data dictionary properly. + * - 0x103 + - ``ESP_ERR_INVALID_STATE`` - Critical failure or FSM sequence failure or master FSM is busy processing previous request. + - Make sure your physical connection is working properly. Increase task stack size and check Modbus initialization sequence. + +Application Example +------------------- + +The examples below use the FreeModbus library port for serial TCP slave and master implementations accordingly. The selection of stack is performed through KConfig menu option "Enable Modbus stack support ..." for appropriate communication mode and related configuration keys. + +.. _example_mb_slave: + +- `Modbus serial slave example `__ + +.. _example_mb_master: + +- `Modbus serial master example `__ + +.. _example_mb_tcp_master: + +- `Modbus TCP master example `__ + +.. _example_mb_tcp_slave: + +- `Modbus TCP slave example `__ + +Please refer to the specific example README.md for details. + +.. _modbus_organization: + +Protocol References +------------------- + + - `Modbus Organization with protocol specifications `__ + +API Reference +------------- + +.. include-build-file:: inc/esp_modbus_common.inc +.. include-build-file:: inc/esp_modbus_master.inc +.. include-build-file:: inc/esp_modbus_slave.inc + diff --git a/docs/en/conf.py b/docs/en/conf.py new file mode 100644 index 0000000..90f2495 --- /dev/null +++ b/docs/en/conf.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# English Language RTD & Sphinx config file +# +# Uses ../conf_common.py for most non-language-specific settings. + +# Importing conf_common adds all the non-language-specific +# parts to this conf module +try: + from conf_common import * # noqa: F403,F401 +except ImportError: + import os + import sys + sys.path.insert(0, os.path.abspath('../')) + from conf_common import * # noqa: F403,F401 + +import datetime + +current_year = datetime.datetime.now().year + +# General information about the project. +project = u'ESP-Modbus Programming Guide' +copyright = u'2019 - {}, Espressif Systems (Shanghai) Co., Ltd'.format(current_year) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = 'en' diff --git a/docs/en/esp-modbus.rst b/docs/en/esp-modbus.rst deleted file mode 100644 index 6e17cf9..0000000 --- a/docs/en/esp-modbus.rst +++ /dev/null @@ -1,662 +0,0 @@ -ESP-Modbus -========== - -Overview --------- - -The Modbus serial communication protocol is de facto standard protocol widely used to connect industrial electronic devices. Modbus allows communication among many devices connected to the same network, for example, a system that measures temperature and humidity and communicates the results to a computer. The Modbus protocol uses several types of data: Holding Registers, Input Registers, Coils (single bit output), Discrete Inputs. Versions of the Modbus protocol exist for serial port and for Ethernet and other protocols that support the Internet protocol suite. -There are many variants of Modbus protocols, some of them are: - - - * ``Modbus RTU`` — This is used in serial communication and makes use of a compact, binary representation of the data for protocol communication. The RTU format follows the commands/data with a cyclic redundancy check checksum as an error check mechanism to ensure the reliability of data. Modbus RTU is the most common implementation available for Modbus. A Modbus RTU message must be transmitted continuously without inter-character hesitations. Modbus messages are framed (separated) by idle (silent) periods. The RS-485 interface communication is usually used for this type. - * ``Modbus ASCII`` — This is used in serial communication and makes use of ASCII characters for protocol communication. The ASCII format uses a longitudinal redundancy check checksum. Modbus ASCII messages are framed by leading colon (":") and trailing newline (CR/LF). - * ``Modbus TCP/IP or Modbus TCP`` — This is a Modbus variant used for communications over TCP/IP networks, connecting over port 502. It does not require a checksum calculation, as lower layers already provide checksum protection. - -The following document (and included code snippets) requires some familiarity with the Modbus protocol. Refer to the Modbus Organization's with protocol specifications for specifics. - -Messaging Model And Data Mapping --------------------------------- - -Modbus is an application protocol that defines rules for messaging structure and data organization that are independent of the data transmission medium. Traditional serial Modbus is a register-based protocol that defines message transactions that occur between master(s) and slave devices (multiple masters are allowed on using Modbus TCP/IP). The slave devices listen for communication from the master and simply respond as instructed. The master(s) always controls communication and may communicate directly to one slave, or all connected slaves, but the slaves cannot communicate directly with each other. - -.. figure:: ../../../_static/modbus-segment.png - :align: center - :scale: 80% - :alt: Modbus segment diagram - :figclass: align-center - - Modbus segment diagram - -.. note:: It is assumed that the number of slaves and their register maps are known by the Modbus master before the start of stack. - -The register map of each slave device is usually part of its device manual. A Slave device usually permits configuration of its short slave address and communication options that are used within the device's network segment. - -The Modbus protocol allows devices to map data to four types of registers (Holding, Input, Discrete, Coil). The figure below illustrates an example mapping of a device's data to the four types of registers. - -.. figure:: ../../../_static/modbus-data-mapping.png - :align: center - :scale: 80% - :alt: Modbus data mapping - :figclass: align-center - - Modbus data mapping - -The following sections give an overview of how to use the ESP_Modbus component found under `components/freemodbus`. The sections cover initialization of a Modbus port, and the setup a master or slave device accordingly: - -- :ref:`modbus_api_port_initialization` -- :ref:`modbus_api_slave_overview` -- :ref:`modbus_api_master_overview` - -.. _modbus_api_port_initialization: - -Modbus Port Initialization -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ESP_Modbus supports Modbus SERIAL and TCP ports and a port must be initialized before calling any other Modbus API. The functions below are used to create and then initialize Modbus controller interface (either master or slave) over a particular transmission medium (either Serial or TCP/IP): - -- :cpp:func:`mbc_slave_init` -- :cpp:func:`mbc_master_init` -- :cpp:func:`mbc_slave_init_tcp` -- :cpp:func:`mbc_master_init_tcp` - -The API call uses the first parameter to recognize the type of port being initialized. Supported enumeration for different ports: :cpp:enumerator:`MB_PORT_SERIAL_MASTER`, :cpp:enumerator:`MB_PORT_SERIAL_SLAVE` accordingly. -The parameters :cpp:enumerator:`MB_PORT_TCP_MASTER`, :cpp:enumerator:`MB_PORT_TCP_SLAVE` are reserved for internal usage. - -.. code:: c - - void* master_handler = NULL; // Pointer to allocate interface structure - // Initialization of Modbus master for serial port - esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &master_handler); - if (master_handler == NULL || err != ESP_OK) { - ESP_LOGE(TAG, "mb controller initialization fail."); - } - -This example code to initialize slave port: - -.. code:: c - - void* slave_handler = NULL; // Pointer to allocate interface structure - // Initialization of Modbus slave for TCP - esp_err_t err = mbc_slave_init_tcp(&slave_handler); - if (slave_handler == NULL || err != ESP_OK) { - // Error handling is performed here - ESP_LOGE(TAG, "mb controller initialization fail."); - } - -.. _modbus_api_master_overview: - -Modbus Master API Overview --------------------------- - -The following overview describes how to setup Modbus master communication. The overview reflects a typical programming workflow and is broken down into the sections provided below: - -1. :ref:`modbus_api_port_initialization` - Initialization of Modbus controller interface for the selected port. -2. :ref:`modbus_api_master_configure_descriptor` - Configure data descriptors to access slave parameters. -3. :ref:`modbus_api_master_setup_communication_options` - Allows to setup communication options for selected port. -4. :ref:`modbus_api_master_start_communication` - Start stack and sending / receiving data. -5. :ref:`modbus_api_master_destroy` - Destroy Modbus controller and its resources. - -.. _modbus_api_master_configure_descriptor: - -Configuring Master Data Access -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The architectural approach of ESP_Modbus includes one level above standard Modbus IO driver. -The additional layer is called Modbus controller and its goal is to add an abstraction such as CID - characteristic identifier. -The CID is linked to a corresponding Modbus registers through the table called Data Dictionary and represents device physical parameter (such as temperature, humidity, etc.) in specific Modbus slave device. -This approach allows the upper layer (e.g., MESH or MQTT) to be isolated from Modbus specifics thus simplify Modbus integration with other protocols/networks. - -The Data Dictionary is the list in the Modbus master which shall be defined by user to link each CID to its corresponding Modbus registers representation using Register Mapping table of the Modbus slave being used. -Each element in this data dictionary is of type :cpp:type:`mb_parameter_descriptor_t` and represents the description of one physical characteristic: - -.. list-table:: Table 1 Modbus master Data Dictionary description - :widths: 8 10 82 - :header-rows: 1 - - * - Field - - Description - - Detailed information - * - ``cid`` - - Characteristic ID - - The identifier of characteristic (must be unique). - * - ``param_key`` - - Characteristic Name - - String description of the characteristic. - * - ``param_units`` - - Characteristic Units - - Physical Units of the characteristic. - * - ``mb_slave_addr`` - - Modbus Slave Address - - The short address of the device with correspond parameter UID. - * - ``mb_param_type`` - - Modbus Register Type - - Type of Modbus register area. - :cpp:enumerator:`MB_PARAM_INPUT`, :cpp:enumerator:`MB_PARAM_HOLDING`, :cpp:enumerator:`MB_PARAM_COIL`, :cpp:enumerator:`MB_PARAM_DISCRETE` - represents Input , Holding, Coil and Discrete input register area accordingly; - * - ``mb_reg_start`` - - Modbus Register Start - - Relative register address of the characteristic in the register area. - * - ``mb_size`` - - Modbus Register Size - - Length of characteristic in registers. - * - ``param_offset`` - - Instance Offset - - Offset to instance of the characteristic in bytes. It is used to calculate the absolute address to the characteristic in the storage structure. - It is optional field and can be set to zero if the parameter is not used in the application. - * - ``param_type`` - - Data Type - - Specifies type of the characteristic. - :cpp:enumerator:`PARAM_TYPE_U8`, :cpp:enumerator:`PARAM_TYPE_U16`, :cpp:enumerator:`PARAM_TYPE_U32` - Unsigned integer 8/16/32 bit type; - :cpp:enumerator:`PARAM_TYPE_FLOAT` - IEEE754 floating point format; - :cpp:enumerator:`PARAM_TYPE_ASCII` - ASCII string or binary data; - * - ``param_size`` - - Data Size - - The storage size of the characteristic (bytes). - * - ``param_opts`` - - Parameter Options - - Limits, options of characteristic used during processing of alarm in user application (optional) - * - ``access`` - - Parameter access type - - Can be used in user application to define the behavior of the characteristic during processing of data in user application; - :cpp:enumerator:`PAR_PERMS_READ_WRITE_TRIGGER`, :cpp:enumerator:`PAR_PERMS_READ`, :cpp:enumerator:`PAR_PERMS_READ_WRITE_TRIGGER`; - -.. note:: The ``cid`` and ``param_key`` have to be unique. Please use the prefix to the parameter key if you have several similar parameters in your register map table. - -.. list-table:: Table 2 Example Register mapping table of Modbus slave - :widths: 5 5 2 10 5 5 68 - :header-rows: 1 - - * - CID - - Register - - Length - - Range - - Type - - Units - - Description - * - 0 - - 30000 - - 4 - - MAX_UINT - - U32 - - Not defined - - Serial number of device (4 bytes) read-only - * - 1 - - 30002 - - 2 - - MAX_UINT - - U16 - - Not defined - - Software version (4 bytes) read-only - * - 2 - - 40000 - - 4 - - -20..40 - - FLOAT - - DegC - - Room temperature in DegC. Writing a temperature value to this register for single point calibration. - -.. code:: c - - // Enumeration of modbus slave addresses accessed by master device - enum { - MB_DEVICE_ADDR1 = 1, - MB_DEVICE_ADDR2, - MB_SLAVE_COUNT - }; - - // Enumeration of all supported CIDs for device - enum { - CID_SER_NUM1 = 0, - CID_SW_VER1, - CID_TEMP_DATA_1, - CID_SER_NUM2, - CID_SW_VER2, - CID_TEMP_DATA_2 - }; - - // Example Data Dictionary for Modbus parameters in 2 slaves in the segment - mb_parameter_descriptor_t device_parameters[] = { - // CID, Name, Units, Modbus addr, register type, Modbus Reg Start Addr, Modbus Reg read length, - // Instance offset (NA), Instance type, Instance length (bytes), Options (NA), Permissions - { CID_SER_NUM1, STR("Serial_number_1"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 2, - 0, PARAM_TYPE_U32, 4, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, - { CID_SW_VER1, STR("Software_version_1"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, - 0, PARAM_TYPE_U16, 2, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, - { CID_TEMP_DATA_1, STR("Temperature_1"), STR("C"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 2, - 0, PARAM_TYPE_FLOAT, 4, OPTS( 16, 30, 1 ), PAR_PERMS_READ_WRITE_TRIGGER }, - { CID_SER_NUM2, STR("Serial_number_2"), STR("--"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, 0, 2, - 0, PARAM_TYPE_U32, 4, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, - { CID_SW_VER2, STR("Software_version_2"), STR("--"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, 2, 1, - 0, PARAM_TYPE_U16, 2, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, - { CID_TEMP_DATA_2, STR("Temperature_2"), STR("C"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, 0, 2, - 0, PARAM_TYPE_FLOAT, 4, OPTS( 20, 30, 1 ), PAR_PERMS_READ_WRITE_TRIGGER }, - }; - // Calculate number of parameters in the table - uint16_t num_device_parameters = (sizeof(device_parameters) / sizeof(device_parameters[0])); - -During initialization of the Modbus stack, a pointer to the Data Dictionary (called descriptor) must be provided as the parameter of the function below. - -:cpp:func:`mbc_master_set_descriptor`: Initialization of master descriptor. - -.. code:: c - - ESP_ERROR_CHECK(mbc_master_set_descriptor(&device_parameters[0], num_device_parameters)); - -The Data Dictionary can be initialized from SD card, MQTT or other source before start of stack. Once the initialization and setup is done, the Modbus controller allows the reading of complex parameters from any slave included in descriptor table using its CID. - -.. _modbus_api_master_setup_communication_options: - -Master Communication Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Calling the setup function allows for specific communication options to be defined for port. - -:cpp:func:`mbc_master_setup` - -The communication structure provided as a parameter is different for serial and TCP communication mode. - -Example setup for serial port: - -.. code:: c - - mb_communication_info_t comm_info = { - .port = MB_PORT_NUM, // Serial port number - .mode = MB_MODE_RTU, // Modbus mode of communication (MB_MODE_RTU or MB_MODE_ASCII) - .baudrate = 9600, // Modbus communication baud rate - .parity = MB_PARITY_NONE // parity option for serial port - }; - - ESP_ERROR_CHECK(mbc_master_setup((void*)&comm_info)); - -Modbus master TCP port requires additional definition of IP address table where number of addresses should be equal to number of unique slave addresses in master Modbus Data Dictionary: - -The order of IP address string corresponds to short slave address in the Data Dictionary. - -.. code:: c - - #define MB_SLAVE_COUNT 2 // Number of slaves in the segment being accessed (as defined in Data Dictionary) - - char* slave_ip_address_table[MB_SLAVE_COUNT] = { - "192.168.1.2", // Address corresponds to UID1 and set to predefined value by user - "192.168.1.3", // corresponds to UID2 in the segment - NULL // end of table - }; - - mb_communication_info_t comm_info = { - .ip_port = MB_TCP_PORT, // Modbus TCP port number (default = 502) - .ip_addr_type = MB_IPV4, // version of IP protocol - .ip_mode = MB_MODE_TCP, // Port communication mode - .ip_addr = (void*)slave_ip_address_table, // assign table of IP addresses - .ip_netif_ptr = esp_netif_ptr // esp_netif_ptr pointer to the corresponding network interface - }; - - ESP_ERROR_CHECK(mbc_master_setup((void*)&comm_info)); - -.. note:: Refer to :doc:`esp_netif component <../network/esp_netif>` for more information about network interface initialization. - -The slave IP addresses in the table can be assigned automatically using mDNS service as described in the example. -Refer to :example:`protocols/modbus/tcp/mb_tcp_master` for more information. - -.. note:: RS485 communication requires call to UART specific APIs to setup communication mode and pins. Refer to :ref:`uart-api-running-uart-communication` section of UART documentation. - - -.. _modbus_api_master_start_communication: - -Master Communication -^^^^^^^^^^^^^^^^^^^^ - -The starting of the Modbus controller is the final step in enabling communication. This is performed using function below: - -:cpp:func:`mbc_master_start` - -.. code:: c - - esp_err_t err = mbc_master_start(); - if (err != ESP_OK) { - ESP_LOGE(TAG, "mb controller start fail, err=%x.", err); - } - -The list of functions below are used by the Modbus master stack from a user's application: - -:cpp:func:`mbc_master_send_request`: This function executes a blocking Modbus request. The master sends a data request (as defined in parameter request structure :cpp:type:`mb_param_request_t`) and then blocks until a response from corresponding slave and returns the status of command execution. This function provides a standard way for read/write access to Modbus devices in the network. - -:cpp:func:`mbc_master_get_cid_info`: The function gets information about each characteristic supported in the data dictionary and returns the characteristic's description in the form of the :cpp:type:`mb_parameter_descriptor_t` structure. Each characteristic is accessed using its CID. - -:cpp:func:`mbc_master_get_parameter`: The function reads the data of a characteristic defined in the parameters of a Modbus slave device. The additional data for request is taken from parameter description table. - -Example: - -.. code:: c - - const mb_parameter_descriptor_t* param_descriptor = NULL; - uint8_t temp_data[4] = {0}; // temporary buffer to hold maximum CID size - uint8_t type = 0; - .... - - // Get the information for characteristic cid from data dictionary - esp_err_t err = mbc_master_get_cid_info(cid, ¶m_descriptor); - if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) { - err = mbc_master_get_parameter(param_descriptor->cid, (char*)param_descriptor->param_key, (uint8_t*)temp_data, &type); - if (err == ESP_OK) { - ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%08x) read successful.", - param_descriptor->cid, - (char*)param_descriptor->param_key, - (char*)param_descriptor->param_units, - *(uint32_t*)temp_data); - } else { - ESP_LOGE(TAG, "Characteristic #%d (%s) read fail, err = 0x%x (%s).", - param_descriptor->cid, - (char*)param_descriptor->param_key, - (int)err, - (char*)esp_err_to_name(err)); - } - } else { - ESP_LOGE(TAG, "Could not get information for characteristic %d.", cid); - } - - -:cpp:func:`mbc_master_set_parameter` - -The function writes characteristic's value defined as a name and cid parameter in corresponded slave device. The additional data for parameter request is taken from master parameter description table. - -.. code:: c - - uint8_t type = 0; // Type of parameter - uint8_t temp_data[4] = {0}; // temporary buffer - - esp_err_t err = mbc_master_set_parameter(CID_TEMP_DATA_2, "Temperature_2", (uint8_t*)temp_data, &type); - if (err == ESP_OK) { - ESP_LOGI(TAG, "Set parameter data successfully."); - } else { - ESP_LOGE(TAG, "Set data fail, err = 0x%x (%s).", (int)err, (char*)esp_err_to_name(err)); - } - - -.. _modbus_api_master_destroy: - -Modbus Master Teardown -^^^^^^^^^^^^^^^^^^^^^^ - -This function stops Modbus communication stack and destroys controller interface and free all used active objects. - -:cpp:func:`mbc_master_destroy` - -.. code:: c - - ESP_ERROR_CHECK(mbc_master_destroy()); - - -.. _modbus_api_slave_overview: - -Modbus Slave API Overview -------------------------- - -The sections below represent typical programming workflow for the slave API which should be called in following order: - -1. :ref:`modbus_api_port_initialization` - Initialization of Modbus controller interface for the selected port. -2. :ref:`modbus_api_slave_configure_descriptor` - Configure data descriptors to access slave parameters. -3. :ref:`modbus_api_slave_setup_communication_options` - Allows to setup communication options for selected port. -4. :ref:`modbus_api_slave_communication` - Start stack and sending / receiving data. Filter events when master accesses the register areas. -5. :ref:`modbus_api_slave_destroy` - Destroy Modbus controller and its resources. - -.. _modbus_api_slave_configure_descriptor: - -Configuring Slave Data Access -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The following functions must be called when the Modbus controller slave port is already initialized. Refer to :ref:`modbus_api_port_initialization`. - -The slave stack requires the user to define structures (memory storage areas) that store the Modbus parameters accessed by stack. These structures should be prepared by the user and be assigned to the Modbus controller interface using :cpp:func:`mbc_slave_set_descriptor` API call before the start of communication. The slave task can call the :cpp:func:`mbc_slave_check_event` function which will block until the Modbus master access the slave. The slave task can then get information about the data being accessed. - -.. note:: One slave can define several area descriptors per each type of Modbus register area with different start_offset. - -Register area is defined by using the :cpp:type:`mb_register_area_descriptor_t` structure. - -.. list-table:: Table 3 Modbus register area descriptor - :widths: 8 92 - :header-rows: 1 - - * - Field - - Description - * - ``start_offset`` - - Zero based register relative offset for defined register area. Example: register address = 40002 ( 4x register area - Function 3 - holding register ), start_offset = 2 - * - ``type`` - - Type of the Modbus register area. Refer to :cpp:type:`mb_param_type_t` for more information. - * - ``address`` - - A pointer to the memory area which is used to store the register data for this area descriptor. - * - ``size`` - - The size of the memory area in bytes which is used to store register data. - -:cpp:func:`mbc_slave_set_descriptor` - -The function initializes Modbus communication descriptors for each type of Modbus register area (Holding Registers, Input Registers, Coils (single bit output), Discrete Inputs). Once areas are initialized and the :cpp:func:`mbc_slave_start()` API is called the Modbus stack can access the data in user data structures by request from master. - -.. code:: c - - #define MB_REG_INPUT_START_AREA0 (0) - #define MB_REG_HOLDING_START_AREA0 (0) - #define MB_REG_HOLD_CNT (100) - #define MB_REG_INPUT_CNT (100) - - mb_register_area_descriptor_t reg_area; // Modbus register area descriptor structure - unit16_t holding_reg_area[MB_REG_HOLD_CNT] = {0}; // storage area for holding registers - unit16_t input_reg_area[MB_REG_INPUT_CNT] = {0}; // storage area for input registers - - reg_area.type = MB_PARAM_HOLDING; // Set type of register area - reg_area.start_offset = MB_REG_HOLDING_START_AREA0; // Offset of register area in Modbus protocol - reg_area.address = (void*)&holding_reg_area[0]; // Set pointer to storage instance - reg_area.size = sizeof(holding_reg_area) << 1; // Set the size of register storage area in bytes - ESP_ERROR_CHECK(mbc_slave_set_descriptor(reg_area)); - - reg_area.type = MB_PARAM_INPUT; - reg_area.start_offset = MB_REG_INPUT_START_AREA0; - reg_area.address = (void*)&input_reg_area[0]; - reg_area.size = sizeof(input_reg_area) << 1; - ESP_ERROR_CHECK(mbc_slave_set_descriptor(reg_area)); - - -At least one area descriptor per each Modbus register type must be set in order to provide register access to its area. If the master tries to access an undefined area, the stack will generate a Modbus exception. - -Direct access to register area from user application must be protected by critical section: - -.. code:: c - - portENTER_CRITICAL(¶m_lock); - holding_reg_area[2] += 10; - portEXIT_CRITICAL(¶m_lock); - - -.. _modbus_api_slave_setup_communication_options: - -Slave Communication Options -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The function initializes the Modbus controller interface and its active context (tasks, RTOS objects and other resources). - -:cpp:func:`mbc_slave_setup` - -The function is used to setup communication parameters of the Modbus stack. - -Example initialization of Modbus TCP communication: - -.. code:: c - - esp_netif_init(); - ... - - mb_communication_info_t comm_info = { - .ip_port = MB_TCP_PORT, // Modbus TCP port number (default = 502) - .ip_addr_type = MB_IPV4, // version of IP protocol - .ip_mode = MB_MODE_TCP, // Port communication mode - .ip_addr = NULL, // This field keeps the client IP address to bind, NULL - bind to any client - .ip_netif_ptr = esp_netif_ptr // esp_netif_ptr - pointer to the corresponding network interface - }; - - // Setup communication parameters and start stack - ESP_ERROR_CHECK(mbc_slave_setup((void*)&comm_info)); - -Example initialization of Modbus serial communication: - -.. code:: c - - #define MB_SLAVE_DEV_SPEED 9600 - #define MB_SLAVE_ADDR 1 - #define MB_SLAVE_PORT_NUM 2 - ... - - // Setup communication parameters and start stack - mb_communication_info_t comm_info = { - .mode = MB_MODE_RTU, // Communication type - .slave_addr = MB_SLAVE_ADDR, // Short address of the slave - .port = MB_SLAVE_PORT_NUM, // UART physical port number - .baudrate = MB_SLAVE_DEV_SPEED, // Baud rate for communication - .parity = MB_PARITY_NONE // Parity option - }; - - ESP_ERROR_CHECK(mbc_slave_setup((void*)&comm_info)); - -.. _modbus_api_slave_communication: - -Slave Communication -^^^^^^^^^^^^^^^^^^^ - -The function below is used to start Modbus controller interface and allows communication. - -:cpp:func:`mbc_slave_start` - -.. code:: c - - ESP_ERROR_CHECK(mbc_slave_start()); - -:cpp:func:`mbc_slave_check_event` - -The blocking call to function waits for a event specified (represented as an event mask parameter). Once the master accesses the parameter and the event mask matches the parameter type, the application task will be unblocked and function will return the corresponding event :cpp:type:`mb_event_group_t` which describes the type of register access being done. - -:cpp:func:`mbc_slave_get_param_info` - -The function gets information about accessed parameters from the Modbus controller event queue. The KConfig :ref:`CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE` key can be used to configure the notification queue size. The timeout parameter allows a timeout to be specified when waiting for a notification. The :cpp:type:`mb_param_info_t` structure contains information about accessed parameter. - -.. list-table:: Table 4 Description of the register info structure: :cpp:type:`mb_param_info_t` - :widths: 10 90 - :header-rows: 1 - - * - Field - - Description - * - ``time_stamp`` - - the time stamp of the event when defined parameter is accessed - * - ``mb_offset`` - - start Modbus register accessed by master - * - ``type`` - - type of the Modbus register area being accessed (See the :cpp:type:`mb_event_group_t` for more information) - * - ``address`` - - memory address that corresponds to accessed register in defined area descriptor - * - ``size`` - - number of registers being accessed by master - -Example to get event when holding or input registers accessed in the slave: - -.. code:: c - - #define MB_READ_MASK (MB_EVENT_INPUT_REG_RD | MB_EVENT_HOLDING_REG_RD) - #define MB_WRITE_MASK (MB_EVENT_HOLDING_REG_WR) - #define MB_READ_WRITE_MASK (MB_READ_MASK | MB_WRITE_MASK) - #define MB_PAR_INFO_GET_TOUT (10 / portTICK_PERIOD_MS) - .... - - // The function blocks while waiting for register access - mb_event_group_t event = mbc_slave_check_event(MB_READ_WRITE_MASK); - - // Get information about data accessed from master - ESP_ERROR_CHECK(mbc_slave_get_param_info(®_info, MB_PAR_INFO_GET_TOUT)); - const char* rw_str = (event & MB_READ_MASK) ? "READ" : "WRITE"; - - // Filter events and process them accordingly - if (event & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { - ESP_LOGI(TAG, "HOLDING %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", - rw_str, - (uint32_t)reg_info.time_stamp, - (uint32_t)reg_info.mb_offset, - (uint32_t)reg_info.type, - (uint32_t)reg_info.address, - (uint32_t)reg_info.size); - } else if (event & (MB_EVENT_INPUT_REG_RD)) { - ESP_LOGI(TAG, "INPUT %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", - rw_str, - (uint32_t)reg_info.time_stamp, - (uint32_t)reg_info.mb_offset, - (uint32_t)reg_info.type, - (uint32_t)reg_info.address, - (uint32_t)reg_info.size); - } - -.. _modbus_api_slave_destroy: - -Modbus Slave Teardown -^^^^^^^^^^^^^^^^^^^^^ - -This function stops the Modbus communication stack, destroys the controller interface, and frees all used active objects allocated for the slave. - -:cpp:func:`mbc_slave_destroy` - -.. code:: c - - ESP_ERROR_CHECK(mbc_slave_destroy()); - -Possible Communication Issues And Solutions -------------------------------------------- - -If the examples do not work as expected and slave and master boards are not able to communicate correctly, it is possible to find the reason for errors. The most important errors are described in master example output and formatted as below: - -.. highlight:: none - -:: - - E (1692332) MB_CONTROLLER_MASTER: mbc_master_get_parameter(111): SERIAL master get parameter failure error=(0x107) (ESP_ERR_TIMEOUT). - - -.. list-table:: Table 5 Modbus error codes and troubleshooting - :widths: 5 30 65 - :header-rows: 1 - - * - Error - - Description - - Possible solution - * - 0x106 - - ``ESP_ERR_NOT_SUPPORTED`` - Invalid register request - slave returned an exception because the requested register is not supported. - - Refer to slave register map. Check the master data dictionary for correctness. - * - 0x107 - - ``ESP_ERR_TIMEOUT`` - Slave response timeout - Modbus slave did not send response during configured slave response timeout. - - Measure and increase the maximum slave response timeout `idf.py menuconfig`, option :ref:`CONFIG_FMB_MASTER_TIMEOUT_MS_RESPOND`. - Check physical connection or network configuration and make sure that the slave response can reach the master side. - If the application has some high performance tasks with higher priority than :ref:`CONFIG_FMB_PORT_TASK_PRIO` it is recommended to place Modbus tasks on the other core using an option :ref:`CONFIG_FMB_PORT_TASK_AFFINITY`. - Configure the Modbus task's priority :ref:`CONFIG_FMB_PORT_TASK_PRIO` to ensure that the task gets sufficient processing time to handle Modbus stack events. - * - 0x108 - - ``ESP_ERR_INVALID_RESPONSE`` - Received unsupported response from slave or frame check failure. Master can not execute command handler because the command is either not supported or is incorrect. - - Check the physical connection then refer to register map of your slave to configure the master data dictionary properly. - * - 0x103 - - ``ESP_ERR_INVALID_STATE`` - Critical failure or FSM sequence failure or master FSM is busy processing previous request. - - Make sure your physical connection is working properly. Increase task stack size and check Modbus initialization sequence. - -Application Example -------------------- - -The examples below use the FreeModbus library port for serial TCP slave and master implementations accordingly. The selection of stack is performed through KConfig menu option "Enable Modbus stack support ..." for appropriate communication mode and related configuration keys. - -- :example:`protocols/modbus/serial/mb_slave` -- :example:`protocols/modbus/serial/mb_master` -- :example:`protocols/modbus/tcp/mb_tcp_slave` -- :example:`protocols/modbus/tcp/mb_tcp_master` - -Please refer to the specific example README.md for details. - -Protocol References -------------------- - - - ``https://modbus.org/specs.php``: Modbus Organization with protocol specifications. - -API Reference -------------- - -.. include-build-file:: inc/esp_modbus_common.inc -.. include-build-file:: inc/esp_modbus_master.inc -.. include-build-file:: inc/esp_modbus_slave.inc - diff --git a/docs/en/index.rst b/docs/en/index.rst new file mode 100644 index 0000000..a392044 --- /dev/null +++ b/docs/en/index.rst @@ -0,0 +1,14 @@ +ESP-Modbus Library +================== + +An Espressif ESP-Modbus Library (esp-modbus) is a library to support Modbus communication in the networks based on RS485 or Ethernet interfaces. +The Modbus is a data communications protocol originally published by Modicon (now Schneider Electric) in 1979 for use with its programmable logic controllers (PLCs). + +.. toctree:: + :maxdepth: 1 + + The Overview, Messaging Model And Data Mapping + Modbus Port Initialization + Modbus Master API + Modbus Slave API + Applications and References \ No newline at end of file diff --git a/docs/en/master_api_overview.rst b/docs/en/master_api_overview.rst new file mode 100644 index 0000000..ba49481 --- /dev/null +++ b/docs/en/master_api_overview.rst @@ -0,0 +1,300 @@ +.. _modbus_api_master_overview: + +Modbus Master API Overview +-------------------------- + +The following overview describes how to setup Modbus master communication. The overview reflects a typical programming workflow and is broken down into the sections provided below: + +1. :ref:`modbus_api_port_initialization` - Initialization of Modbus controller interface for the selected port. +2. :ref:`modbus_api_master_configure_descriptor` - Configure data descriptors to access slave parameters. +3. :ref:`modbus_api_master_setup_communication_options` - Allows to setup communication options for selected port. +4. :ref:`modbus_api_master_start_communication` - Start stack and sending / receiving data. +5. :ref:`modbus_api_master_destroy` - Destroy Modbus controller and its resources. + +.. _modbus_api_master_configure_descriptor: + +Configuring Master Data Access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The architectural approach of ESP_Modbus includes one level above standard Modbus IO driver. +The additional layer is called Modbus controller and its goal is to add an abstraction such as CID - characteristic identifier. +The CID is linked to a corresponding Modbus registers through the table called Data Dictionary and represents device physical parameter (such as temperature, humidity, etc.) in specific Modbus slave device. +This approach allows the upper layer (e.g., MESH or MQTT) to be isolated from Modbus specifics thus simplify Modbus integration with other protocols/networks. + +The Data Dictionary is the list in the Modbus master which shall be defined by user to link each CID to its corresponding Modbus registers representation using Register Mapping table of the Modbus slave being used. +Each element in this data dictionary is of type :cpp:type:`mb_parameter_descriptor_t` and represents the description of one physical characteristic: + +.. list-table:: Table 1 Modbus master Data Dictionary description + :widths: 8 10 82 + :header-rows: 1 + + * - Field + - Description + - Detailed information + * - ``cid`` + - Characteristic ID + - The identifier of characteristic (must be unique). + * - ``param_key`` + - Characteristic Name + - String description of the characteristic. + * - ``param_units`` + - Characteristic Units + - Physical Units of the characteristic. + * - ``mb_slave_addr`` + - Modbus Slave Address + - The short address of the device with correspond parameter UID. + * - ``mb_param_type`` + - Modbus Register Type + - Type of Modbus register area. + :cpp:enumerator:`MB_PARAM_INPUT`, :cpp:enumerator:`MB_PARAM_HOLDING`, :cpp:enumerator:`MB_PARAM_COIL`, :cpp:enumerator:`MB_PARAM_DISCRETE` - represents Input , Holding, Coil and Discrete input register area accordingly; + * - ``mb_reg_start`` + - Modbus Register Start + - Relative register address of the characteristic in the register area. + * - ``mb_size`` + - Modbus Register Size + - Length of characteristic in registers. + * - ``param_offset`` + - Instance Offset + - Offset to instance of the characteristic in bytes. It is used to calculate the absolute address to the characteristic in the storage structure. + It is optional field and can be set to zero if the parameter is not used in the application. + * - ``param_type`` + - Data Type + - Specifies type of the characteristic. + :cpp:enumerator:`PARAM_TYPE_U8`, :cpp:enumerator:`PARAM_TYPE_U16`, :cpp:enumerator:`PARAM_TYPE_U32` - Unsigned integer 8/16/32 bit type; + :cpp:enumerator:`PARAM_TYPE_FLOAT` - IEEE754 floating point format; + :cpp:enumerator:`PARAM_TYPE_ASCII` - ASCII string or binary data; + * - ``param_size`` + - Data Size + - The storage size of the characteristic (bytes). + * - ``param_opts`` + - Parameter Options + - Limits, options of characteristic used during processing of alarm in user application (optional) + * - ``access`` + - Parameter access type + - Can be used in user application to define the behavior of the characteristic during processing of data in user application; + :cpp:enumerator:`PAR_PERMS_READ_WRITE_TRIGGER`, :cpp:enumerator:`PAR_PERMS_READ`, :cpp:enumerator:`PAR_PERMS_READ_WRITE_TRIGGER`; + +.. note:: The ``cid`` and ``param_key`` have to be unique. Please use the prefix to the parameter key if you have several similar parameters in your register map table. + +.. list-table:: Table 2 Example Register mapping table of Modbus slave + :widths: 5 5 2 10 5 5 68 + :header-rows: 1 + + * - CID + - Register + - Length + - Range + - Type + - Units + - Description + * - 0 + - 30000 + - 4 + - MAX_UINT + - U32 + - Not defined + - Serial number of device (4 bytes) read-only + * - 1 + - 30002 + - 2 + - MAX_UINT + - U16 + - Not defined + - Software version (4 bytes) read-only + * - 2 + - 40000 + - 4 + - -20..40 + - FLOAT + - DegC + - Room temperature in DegC. Writing a temperature value to this register for single point calibration. + +.. code:: c + + // Enumeration of modbus slave addresses accessed by master device + enum { + MB_DEVICE_ADDR1 = 1, + MB_DEVICE_ADDR2, + MB_SLAVE_COUNT + }; + + // Enumeration of all supported CIDs for device + enum { + CID_SER_NUM1 = 0, + CID_SW_VER1, + CID_TEMP_DATA_1, + CID_SER_NUM2, + CID_SW_VER2, + CID_TEMP_DATA_2 + }; + + // Example Data Dictionary for Modbus parameters in 2 slaves in the segment + mb_parameter_descriptor_t device_parameters[] = { + // CID, Name, Units, Modbus addr, register type, Modbus Reg Start Addr, Modbus Reg read length, + // Instance offset (NA), Instance type, Instance length (bytes), Options (NA), Permissions + { CID_SER_NUM1, STR("Serial_number_1"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 0, 2, + 0, PARAM_TYPE_U32, 4, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_SW_VER1, STR("Software_version_1"), STR("--"), MB_DEVICE_ADDR1, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_TEMP_DATA_1, STR("Temperature_1"), STR("C"), MB_DEVICE_ADDR1, MB_PARAM_HOLDING, 0, 2, + 0, PARAM_TYPE_FLOAT, 4, OPTS( 16, 30, 1 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_SER_NUM2, STR("Serial_number_2"), STR("--"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, 0, 2, + 0, PARAM_TYPE_U32, 4, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_SW_VER2, STR("Software_version_2"), STR("--"), MB_DEVICE_ADDR2, MB_PARAM_INPUT, 2, 1, + 0, PARAM_TYPE_U16, 2, OPTS( 0,0,0 ), PAR_PERMS_READ_WRITE_TRIGGER }, + { CID_TEMP_DATA_2, STR("Temperature_2"), STR("C"), MB_DEVICE_ADDR2, MB_PARAM_HOLDING, 0, 2, + 0, PARAM_TYPE_FLOAT, 4, OPTS( 20, 30, 1 ), PAR_PERMS_READ_WRITE_TRIGGER }, + }; + // Calculate number of parameters in the table + uint16_t num_device_parameters = (sizeof(device_parameters) / sizeof(device_parameters[0])); + +During initialization of the Modbus stack, a pointer to the Data Dictionary (called descriptor) must be provided as the parameter of the function below. + +:cpp:func:`mbc_master_set_descriptor`: Initialization of master descriptor. + +.. code:: c + + ESP_ERROR_CHECK(mbc_master_set_descriptor(&device_parameters[0], num_device_parameters)); + +The Data Dictionary can be initialized from SD card, MQTT or other source before start of stack. Once the initialization and setup is done, the Modbus controller allows the reading of complex parameters from any slave included in descriptor table using its CID. + +.. _modbus_api_master_setup_communication_options: + +Master Communication Options +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Calling the setup function allows for specific communication options to be defined for port. + +:cpp:func:`mbc_master_setup` + +The communication structure provided as a parameter is different for serial and TCP communication mode. + +Example setup for serial port: + +.. code:: c + + mb_communication_info_t comm_info = { + .port = MB_PORT_NUM, // Serial port number + .mode = MB_MODE_RTU, // Modbus mode of communication (MB_MODE_RTU or MB_MODE_ASCII) + .baudrate = 9600, // Modbus communication baud rate + .parity = MB_PARITY_NONE // parity option for serial port + }; + + ESP_ERROR_CHECK(mbc_master_setup((void*)&comm_info)); + +Modbus master TCP port requires additional definition of IP address table where number of addresses should be equal to number of unique slave addresses in master Modbus Data Dictionary: + +The order of IP address string corresponds to short slave address in the Data Dictionary. + +.. code:: c + + #define MB_SLAVE_COUNT 2 // Number of slaves in the segment being accessed (as defined in Data Dictionary) + + char* slave_ip_address_table[MB_SLAVE_COUNT] = { + "192.168.1.2", // Address corresponds to UID1 and set to predefined value by user + "192.168.1.3", // corresponds to UID2 in the segment + NULL // end of table + }; + + mb_communication_info_t comm_info = { + .ip_port = MB_TCP_PORT, // Modbus TCP port number (default = 502) + .ip_addr_type = MB_IPV4, // version of IP protocol + .ip_mode = MB_MODE_TCP, // Port communication mode + .ip_addr = (void*)slave_ip_address_table, // assign table of IP addresses + .ip_netif_ptr = esp_netif_ptr // esp_netif_ptr pointer to the corresponding network interface + }; + + ESP_ERROR_CHECK(mbc_master_setup((void*)&comm_info)); + +.. note:: Refer to `esp_netif component `__ for more information about network interface initialization. + +The slave IP addresses in the table can be assigned automatically using mDNS service as described in the example. +Refer to :ref:`example TCP master ` for more information. + +.. note:: RS485 communication requires call to UART specific APIs to setup communication mode and pins. Refer to the `UART communication section `__ in documentation. + + +.. _modbus_api_master_start_communication: + +Master Communication +^^^^^^^^^^^^^^^^^^^^ + +The starting of the Modbus controller is the final step in enabling communication. This is performed using function below: + +:cpp:func:`mbc_master_start` + +.. code:: c + + esp_err_t err = mbc_master_start(); + if (err != ESP_OK) { + ESP_LOGE(TAG, "mb controller start fail, err=%x.", err); + } + +The list of functions below are used by the Modbus master stack from a user's application: + +:cpp:func:`mbc_master_send_request`: This function executes a blocking Modbus request. The master sends a data request (as defined in parameter request structure :cpp:type:`mb_param_request_t`) and then blocks until a response from corresponding slave and returns the status of command execution. This function provides a standard way for read/write access to Modbus devices in the network. + +:cpp:func:`mbc_master_get_cid_info`: The function gets information about each characteristic supported in the data dictionary and returns the characteristic's description in the form of the :cpp:type:`mb_parameter_descriptor_t` structure. Each characteristic is accessed using its CID. + +:cpp:func:`mbc_master_get_parameter`: The function reads the data of a characteristic defined in the parameters of a Modbus slave device. The additional data for request is taken from parameter description table. + +Example: + +.. code:: c + + const mb_parameter_descriptor_t* param_descriptor = NULL; + uint8_t temp_data[4] = {0}; // temporary buffer to hold maximum CID size + uint8_t type = 0; + .... + + // Get the information for characteristic cid from data dictionary + esp_err_t err = mbc_master_get_cid_info(cid, ¶m_descriptor); + if ((err != ESP_ERR_NOT_FOUND) && (param_descriptor != NULL)) { + err = mbc_master_get_parameter(param_descriptor->cid, (char*)param_descriptor->param_key, (uint8_t*)temp_data, &type); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Characteristic #%d %s (%s) value = (0x%08x) read successful.", + param_descriptor->cid, + (char*)param_descriptor->param_key, + (char*)param_descriptor->param_units, + *(uint32_t*)temp_data); + } else { + ESP_LOGE(TAG, "Characteristic #%d (%s) read fail, err = 0x%x (%s).", + param_descriptor->cid, + (char*)param_descriptor->param_key, + (int)err, + (char*)esp_err_to_name(err)); + } + } else { + ESP_LOGE(TAG, "Could not get information for characteristic %d.", cid); + } + + +:cpp:func:`mbc_master_set_parameter` + +The function writes characteristic's value defined as a name and cid parameter in corresponded slave device. The additional data for parameter request is taken from master parameter description table. + +.. code:: c + + uint8_t type = 0; // Type of parameter + uint8_t temp_data[4] = {0}; // temporary buffer + + esp_err_t err = mbc_master_set_parameter(CID_TEMP_DATA_2, "Temperature_2", (uint8_t*)temp_data, &type); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Set parameter data successfully."); + } else { + ESP_LOGE(TAG, "Set data fail, err = 0x%x (%s).", (int)err, (char*)esp_err_to_name(err)); + } + + +.. _modbus_api_master_destroy: + +Modbus Master Teardown +^^^^^^^^^^^^^^^^^^^^^^ + +This function stops Modbus communication stack and destroys controller interface and free all used active objects. + +:cpp:func:`mbc_master_destroy` + +.. code:: c + + ESP_ERROR_CHECK(mbc_master_destroy()); diff --git a/docs/en/overview_messaging_and_mapping.rst b/docs/en/overview_messaging_and_mapping.rst new file mode 100644 index 0000000..f62ca32 --- /dev/null +++ b/docs/en/overview_messaging_and_mapping.rst @@ -0,0 +1,47 @@ +ESP-Modbus +========== + +Overview +-------- + +The Modbus serial communication protocol is de facto standard protocol widely used to connect industrial electronic devices. Modbus allows communication among many devices connected to the same network, for example, a system that measures temperature and humidity and communicates the results to a computer. The Modbus protocol uses several types of data: Holding Registers, Input Registers, Coils (single bit output), Discrete Inputs. Versions of the Modbus protocol exist for serial port and for Ethernet and other protocols that support the Internet protocol suite. There are many variants of Modbus protocols, some of them are: + + * ``Modbus RTU`` — This is used in serial communication and makes use of a compact, binary representation of the data for protocol communication. The RTU format follows the commands/data with a cyclic redundancy check checksum as an error check mechanism to ensure the reliability of data. Modbus RTU is the most common implementation available for Modbus. A Modbus RTU message must be transmitted continuously without inter-character hesitations. Modbus messages are framed (separated) by idle (silent) periods. The RS-485 interface communication is usually used for this type. + * ``Modbus ASCII`` — This is used in serial communication and makes use of ASCII characters for protocol communication. The ASCII format uses a longitudinal redundancy check checksum. Modbus ASCII messages are framed by leading colon (":") and trailing newline (CR/LF). + * ``Modbus TCP/IP or Modbus TCP`` — This is a Modbus variant used for communications over TCP/IP networks, connecting over port 502. It does not require a checksum calculation, as lower layers already provide checksum protection. + +.. note:: This documentation (and included code snippets) requires some familiarity with the Modbus protocol. Refer to the Modbus Organization's with protocol specifications for specifics :ref:`modbus_organization`. + +Messaging Model And Data Mapping +-------------------------------- + +Modbus is an application protocol that defines rules for messaging structure and data organization that are independent of the data transmission medium. Traditional serial Modbus is a register-based protocol that defines message transactions that occur between master(s) and slave devices (multiple masters are allowed on using Modbus TCP/IP). The slave devices listen for communication from the master and simply respond as instructed. The master(s) always controls communication and may communicate directly to one slave, or all connected slaves, but the slaves cannot communicate directly with each other. + +.. figure:: ../_static/modbus-segment.png + :align: center + :scale: 80% + :alt: Modbus segment diagram + :figclass: align-center + + Modbus segment diagram + +.. note:: It is assumed that the number of slaves and their register maps are known by the Modbus master before the start of stack. + +The register map of each slave device is usually part of its device manual. A Slave device usually permits configuration of its short slave address and communication options that are used within the device's network segment. + +The Modbus protocol allows devices to map data to four types of registers (Holding, Input, Discrete, Coil). The figure below illustrates an example mapping of a device's data to the four types of registers. + +.. figure:: ../_static/modbus-data-mapping.png + :align: center + :scale: 80% + :alt: Modbus data mapping + :figclass: align-center + + Modbus data mapping + +The following sections give an overview of how to use the ESP_Modbus component found under `components/freemodbus`. The sections cover initialization of a Modbus port, and the setup a master or slave device accordingly: + +- :ref:`modbus_api_port_initialization` +- :ref:`modbus_api_slave_overview` +- :ref:`modbus_api_master_overview` + diff --git a/docs/en/port_initialization.rst b/docs/en/port_initialization.rst new file mode 100644 index 0000000..668c5f4 --- /dev/null +++ b/docs/en/port_initialization.rst @@ -0,0 +1,35 @@ +.. _modbus_api_port_initialization: + +Modbus Port Initialization +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ESP_Modbus supports Modbus SERIAL and TCP ports and a port must be initialized before calling any other Modbus API. The functions below are used to create and then initialize Modbus controller interface (either master or slave) over a particular transmission medium (either Serial or TCP/IP): + +- :cpp:func:`mbc_slave_init` +- :cpp:func:`mbc_master_init` +- :cpp:func:`mbc_slave_init_tcp` +- :cpp:func:`mbc_master_init_tcp` + +The API call uses the first parameter to recognize the type of port being initialized. Supported enumeration for different ports: :cpp:enumerator:`MB_PORT_SERIAL_MASTER`, :cpp:enumerator:`MB_PORT_SERIAL_SLAVE` accordingly. +The parameters :cpp:enumerator:`MB_PORT_TCP_MASTER`, :cpp:enumerator:`MB_PORT_TCP_SLAVE` are reserved for internal usage. + +.. code:: c + + void* master_handler = NULL; // Pointer to allocate interface structure + // Initialization of Modbus master for serial port + esp_err_t err = mbc_master_init(MB_PORT_SERIAL_MASTER, &master_handler); + if (master_handler == NULL || err != ESP_OK) { + ESP_LOGE(TAG, "mb controller initialization fail."); + } + +This example code to initialize slave port: + +.. code:: c + + void* slave_handler = NULL; // Pointer to allocate interface structure + // Initialization of Modbus slave for TCP + esp_err_t err = mbc_slave_init_tcp(&slave_handler); + if (slave_handler == NULL || err != ESP_OK) { + // Error handling is performed here + ESP_LOGE(TAG, "mb controller initialization fail."); + } \ No newline at end of file diff --git a/docs/en/slave_api_overview.rst b/docs/en/slave_api_overview.rst new file mode 100644 index 0000000..bc46d0d --- /dev/null +++ b/docs/en/slave_api_overview.rst @@ -0,0 +1,215 @@ +.. _modbus_api_slave_overview: + +Modbus Slave API Overview +------------------------- + +The sections below represent typical programming workflow for the slave API which should be called in following order: + +1. :ref:`modbus_api_port_initialization` - Initialization of Modbus controller interface for the selected port. +2. :ref:`modbus_api_slave_configure_descriptor` - Configure data descriptors to access slave parameters. +3. :ref:`modbus_api_slave_setup_communication_options` - Allows to setup communication options for selected port. +4. :ref:`modbus_api_slave_communication` - Start stack and sending / receiving data. Filter events when master accesses the register areas. +5. :ref:`modbus_api_slave_destroy` - Destroy Modbus controller and its resources. + +.. _modbus_api_slave_configure_descriptor: + +Configuring Slave Data Access +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following functions must be called when the Modbus controller slave port is already initialized. Refer to :ref:`modbus_api_port_initialization`. + +The slave stack requires the user to define structures (memory storage areas) that store the Modbus parameters accessed by stack. These structures should be prepared by the user and be assigned to the Modbus controller interface using :cpp:func:`mbc_slave_set_descriptor` API call before the start of communication. The slave task can call the :cpp:func:`mbc_slave_check_event` function which will block until the Modbus master access the slave. The slave task can then get information about the data being accessed. + +.. note:: One slave can define several area descriptors per each type of Modbus register area with different start_offset. + +Register area is defined by using the :cpp:type:`mb_register_area_descriptor_t` structure. + +.. list-table:: Table 3 Modbus register area descriptor + :widths: 8 92 + :header-rows: 1 + + * - Field + - Description + * - ``start_offset`` + - Zero based register relative offset for defined register area. Example: register address = 40002 ( 4x register area - Function 3 - holding register ), start_offset = 2 + * - ``type`` + - Type of the Modbus register area. Refer to :cpp:type:`mb_param_type_t` for more information. + * - ``address`` + - A pointer to the memory area which is used to store the register data for this area descriptor. + * - ``size`` + - The size of the memory area in bytes which is used to store register data. + +:cpp:func:`mbc_slave_set_descriptor` + +The function initializes Modbus communication descriptors for each type of Modbus register area (Holding Registers, Input Registers, Coils (single bit output), Discrete Inputs). Once areas are initialized and the :cpp:func:`mbc_slave_start()` API is called the Modbus stack can access the data in user data structures by request from master. + +.. code:: c + + #define MB_REG_INPUT_START_AREA0 (0) + #define MB_REG_HOLDING_START_AREA0 (0) + #define MB_REG_HOLD_CNT (100) + #define MB_REG_INPUT_CNT (100) + + mb_register_area_descriptor_t reg_area; // Modbus register area descriptor structure + unit16_t holding_reg_area[MB_REG_HOLD_CNT] = {0}; // storage area for holding registers + unit16_t input_reg_area[MB_REG_INPUT_CNT] = {0}; // storage area for input registers + + reg_area.type = MB_PARAM_HOLDING; // Set type of register area + reg_area.start_offset = MB_REG_HOLDING_START_AREA0; // Offset of register area in Modbus protocol + reg_area.address = (void*)&holding_reg_area[0]; // Set pointer to storage instance + reg_area.size = sizeof(holding_reg_area) << 1; // Set the size of register storage area in bytes + ESP_ERROR_CHECK(mbc_slave_set_descriptor(reg_area)); + + reg_area.type = MB_PARAM_INPUT; + reg_area.start_offset = MB_REG_INPUT_START_AREA0; + reg_area.address = (void*)&input_reg_area[0]; + reg_area.size = sizeof(input_reg_area) << 1; + ESP_ERROR_CHECK(mbc_slave_set_descriptor(reg_area)); + + +At least one area descriptor per each Modbus register type must be set in order to provide register access to its area. If the master tries to access an undefined area, the stack will generate a Modbus exception. + +Direct access to register area from user application must be protected by critical section: + +.. code:: c + + portENTER_CRITICAL(¶m_lock); + holding_reg_area[2] += 10; + portEXIT_CRITICAL(¶m_lock); + + +.. _modbus_api_slave_setup_communication_options: + +Slave Communication Options +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The function initializes the Modbus controller interface and its active context (tasks, RTOS objects and other resources). + +:cpp:func:`mbc_slave_setup` + +The function is used to setup communication parameters of the Modbus stack. + +Example initialization of Modbus TCP communication: + +.. code:: c + + esp_netif_init(); + ... + + mb_communication_info_t comm_info = { + .ip_port = MB_TCP_PORT, // Modbus TCP port number (default = 502) + .ip_addr_type = MB_IPV4, // version of IP protocol + .ip_mode = MB_MODE_TCP, // Port communication mode + .ip_addr = NULL, // This field keeps the client IP address to bind, NULL - bind to any client + .ip_netif_ptr = esp_netif_ptr // esp_netif_ptr - pointer to the corresponding network interface + }; + + // Setup communication parameters and start stack + ESP_ERROR_CHECK(mbc_slave_setup((void*)&comm_info)); + +Example initialization of Modbus serial communication: + +.. code:: c + + #define MB_SLAVE_DEV_SPEED 9600 + #define MB_SLAVE_ADDR 1 + #define MB_SLAVE_PORT_NUM 2 + ... + + // Setup communication parameters and start stack + mb_communication_info_t comm_info = { + .mode = MB_MODE_RTU, // Communication type + .slave_addr = MB_SLAVE_ADDR, // Short address of the slave + .port = MB_SLAVE_PORT_NUM, // UART physical port number + .baudrate = MB_SLAVE_DEV_SPEED, // Baud rate for communication + .parity = MB_PARITY_NONE // Parity option + }; + + ESP_ERROR_CHECK(mbc_slave_setup((void*)&comm_info)); + +.. _modbus_api_slave_communication: + +Slave Communication +^^^^^^^^^^^^^^^^^^^ + +The function below is used to start Modbus controller interface and allows communication. + +:cpp:func:`mbc_slave_start` + +.. code:: c + + ESP_ERROR_CHECK(mbc_slave_start()); + +:cpp:func:`mbc_slave_check_event` + +The blocking call to function waits for a event specified (represented as an event mask parameter). Once the master accesses the parameter and the event mask matches the parameter type, the application task will be unblocked and function will return the corresponding event :cpp:type:`mb_event_group_t` which describes the type of register access being done. + +:cpp:func:`mbc_slave_get_param_info` + +The function gets information about accessed parameters from the Modbus controller event queue. The KConfig ``CONFIG_FMB_CONTROLLER_NOTIFY_QUEUE_SIZE`` key can be used to configure the notification queue size. The timeout parameter allows a timeout to be specified when waiting for a notification. The :cpp:type:`mb_param_info_t` structure contains information about accessed parameter. + +.. list-table:: Table 4 Description of the register info structure: :cpp:type:`mb_param_info_t` + :widths: 10 90 + :header-rows: 1 + + * - Field + - Description + * - ``time_stamp`` + - the time stamp of the event when defined parameter is accessed + * - ``mb_offset`` + - start Modbus register accessed by master + * - ``type`` + - type of the Modbus register area being accessed (See the :cpp:type:`mb_event_group_t` for more information) + * - ``address`` + - memory address that corresponds to accessed register in defined area descriptor + * - ``size`` + - number of registers being accessed by master + +Example to get event when holding or input registers accessed in the slave: + +.. code:: c + + #define MB_READ_MASK (MB_EVENT_INPUT_REG_RD | MB_EVENT_HOLDING_REG_RD) + #define MB_WRITE_MASK (MB_EVENT_HOLDING_REG_WR) + #define MB_READ_WRITE_MASK (MB_READ_MASK | MB_WRITE_MASK) + #define MB_PAR_INFO_GET_TOUT (10 / portTICK_RATE_MS) + .... + + // The function blocks while waiting for register access + mb_event_group_t event = mbc_slave_check_event(MB_READ_WRITE_MASK); + + // Get information about data accessed from master + ESP_ERROR_CHECK(mbc_slave_get_param_info(®_info, MB_PAR_INFO_GET_TOUT)); + const char* rw_str = (event & MB_READ_MASK) ? "READ" : "WRITE"; + + // Filter events and process them accordingly + if (event & (MB_EVENT_HOLDING_REG_WR | MB_EVENT_HOLDING_REG_RD)) { + ESP_LOGI(TAG, "HOLDING %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", + rw_str, + (uint32_t)reg_info.time_stamp, + (uint32_t)reg_info.mb_offset, + (uint32_t)reg_info.type, + (uint32_t)reg_info.address, + (uint32_t)reg_info.size); + } else if (event & (MB_EVENT_INPUT_REG_RD)) { + ESP_LOGI(TAG, "INPUT %s (%u us), ADDR:%u, TYPE:%u, INST_ADDR:0x%.4x, SIZE:%u", + rw_str, + (uint32_t)reg_info.time_stamp, + (uint32_t)reg_info.mb_offset, + (uint32_t)reg_info.type, + (uint32_t)reg_info.address, + (uint32_t)reg_info.size); + } + +.. _modbus_api_slave_destroy: + +Modbus Slave Teardown +^^^^^^^^^^^^^^^^^^^^^ + +This function stops the Modbus communication stack, destroys the controller interface, and frees all used active objects allocated for the slave. + +:cpp:func:`mbc_slave_destroy` + +.. code:: c + + ESP_ERROR_CHECK(mbc_slave_destroy()); \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..3b07ded --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +esp-docs==0.2.0 \ No newline at end of file diff --git a/docs/utils.sh b/docs/utils.sh new file mode 100644 index 0000000..84f3748 --- /dev/null +++ b/docs/utils.sh @@ -0,0 +1,18 @@ +# Bash helper functions for adding SSH keys + +function add_ssh_keys() { + local key_string="${1}" + mkdir -p ~/.ssh + chmod 700 ~/.ssh + echo -n "${key_string}" >~/.ssh/id_rsa_base64 + base64 --decode --ignore-garbage ~/.ssh/id_rsa_base64 >~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa +} + +function add_doc_server_ssh_keys() { + local key_string="${1}" + local server_url="${2}" + local server_user="${3}" + add_ssh_keys "${key_string}" + echo -e "Host ${server_url}\n\tStrictHostKeyChecking no\n\tUser ${server_user}\n" >>~/.ssh/config +}