From 981462afc835924d3334efbccf75ef864c3eab79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Sun, 18 Oct 2026 15:55:35 +0200 Subject: [PATCH 1/3] Add sample implement DDOPs for tests Real-world Device Descriptor Object Pools collected from issues and bench captures, sorted by implement type under tools/testdata/implements, with a manifest.csv index and a README. Serial numbers and NAME identity bits are zeroed. Taken unchanged from the integration build, so tests can run section control detection against real pools without the machines. Co-Authored-By: Claude Opus 5.5 --- tools/testdata/implements/README.md | 104 ++++++++++++++++++ .../AMAZONE_PRECEA_4500_6sec_2booms.ddop | Bin 0 -> 2705 bytes tools/testdata/implements/manifest.csv | 25 +++++ .../AGISOSTACK_EXAMPLE_SEEDER_6sec.ddop | Bin 0 -> 1427 bytes .../seeder/KUHN_ESPRO_2sec_DDI141_en.ddop | Bin 0 -> 2285 bytes .../seeder/KUHN_ESPRO_2sec_DDI141_hu.ddop | Bin 0 -> 2455 bytes .../seeder/KUHN_ESPRO_2sec_DDI290_hu.ddop | Bin 0 -> 2677 bytes .../seeder/SKY_EASYDRILL_E2_1sec.ddop | Bin 0 -> 1255 bytes .../implements/sprayer/AGL_SPRAYER_7sec.ddop | Bin 0 -> 1660 bytes .../sprayer/JD_740_SPRAYER_9sec.ddop | Bin 0 -> 1797 bytes .../sprayer/ME_ECO_SPRITZE_SC_BOX_9sec.ddop | Bin 0 -> 1231 bytes .../sprayer/ME_SPRITZE_SPR_MIDI_3_11sec.ddop | Bin 0 -> 5246 bytes .../KVERNELAND_EDW2_ISO_2015_2sec.ddop | Bin 0 -> 576 bytes .../SULKY_ISOLINK_SPREADER_12sec.ddop | Bin 0 -> 1147 bytes .../spreader/SULKY_ISOLINK_SPREADER_6sec.ddop | Bin 0 -> 973 bytes .../implements/tractor/JD_6000M_TRACTOR.ddop | Bin 0 -> 1829 bytes .../implements/tractor/JD_6R150_TRACTOR.ddop | Bin 0 -> 1207 bytes .../tractor/JD_6R150_TRACTOR_minimal.ddop | Bin 0 -> 970 bytes .../tractor/JD_6R175_6000RW_TRACTOR.ddop | Bin 0 -> 1829 bytes .../MFG89_EDW2_ISO_DEMO_64sec_5booms.ddop | Bin 0 -> 2140 bytes .../MFG89_EDW3_MAIN_DEMO_24sec_3booms.ddop | Bin 0 -> 1238 bytes .../unsorted/MFG89_ESA_FC_DEMO_2sec.ddop | Bin 0 -> 603 bytes .../MFG89_E_COM_UD_DEMO_1sec_2booms.ddop | Bin 0 -> 818 bytes .../unsorted/MFG89_IXSPRAY_DEMO_7sec.ddop | Bin 0 -> 756 bytes .../unsorted/MFG89_PSD_MB_DEMO_12sec.ddop | Bin 0 -> 760 bytes .../unsorted/MFG89_PWRW_II_ISO_SIM_16sec.ddop | Bin 0 -> 762 bytes 26 files changed, 129 insertions(+) create mode 100644 tools/testdata/implements/README.md create mode 100644 tools/testdata/implements/combined/AMAZONE_PRECEA_4500_6sec_2booms.ddop create mode 100644 tools/testdata/implements/manifest.csv create mode 100644 tools/testdata/implements/seeder/AGISOSTACK_EXAMPLE_SEEDER_6sec.ddop create mode 100644 tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI141_en.ddop create mode 100644 tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI141_hu.ddop create mode 100644 tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI290_hu.ddop create mode 100644 tools/testdata/implements/seeder/SKY_EASYDRILL_E2_1sec.ddop create mode 100644 tools/testdata/implements/sprayer/AGL_SPRAYER_7sec.ddop create mode 100644 tools/testdata/implements/sprayer/JD_740_SPRAYER_9sec.ddop create mode 100644 tools/testdata/implements/sprayer/ME_ECO_SPRITZE_SC_BOX_9sec.ddop create mode 100644 tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec.ddop create mode 100644 tools/testdata/implements/spreader/KVERNELAND_EDW2_ISO_2015_2sec.ddop create mode 100644 tools/testdata/implements/spreader/SULKY_ISOLINK_SPREADER_12sec.ddop create mode 100644 tools/testdata/implements/spreader/SULKY_ISOLINK_SPREADER_6sec.ddop create mode 100644 tools/testdata/implements/tractor/JD_6000M_TRACTOR.ddop create mode 100644 tools/testdata/implements/tractor/JD_6R150_TRACTOR.ddop create mode 100644 tools/testdata/implements/tractor/JD_6R150_TRACTOR_minimal.ddop create mode 100644 tools/testdata/implements/tractor/JD_6R175_6000RW_TRACTOR.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_EDW2_ISO_DEMO_64sec_5booms.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_EDW3_MAIN_DEMO_24sec_3booms.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_ESA_FC_DEMO_2sec.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_E_COM_UD_DEMO_1sec_2booms.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_IXSPRAY_DEMO_7sec.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_PSD_MB_DEMO_12sec.ddop create mode 100644 tools/testdata/implements/unsorted/MFG89_PWRW_II_ISO_SIM_16sec.ddop diff --git a/tools/testdata/implements/README.md b/tools/testdata/implements/README.md new file mode 100644 index 0000000..4dcd606 --- /dev/null +++ b/tools/testdata/implements/README.md @@ -0,0 +1,104 @@ +# Sample implements + +Real-world Device Descriptor Object Pools (DDOPs), captured from implements that were connected to the +AOG-TaskController and shared in issues. They are the input for tests that exercise DDOP parsing, section +geometry and section-control method detection without needing the machine. + +`manifest.csv` is the machine-readable index. The table below is generated from it. + +## Layout + +One folder per implement type, using the same categories as the "Submit a machine" issue template: + +| Folder | Contents | +|---|---| +| `sprayer/` | Sprayers | +| `spreader/` | Fertilizer spreaders | +| `seeder/` | Seed drills and single-product planters | +| `combined/` | Multi-product machines (e.g. planter + fertilizer + microgranulate) | +| `tractor/` | Tractor pools. **No sections**, so they must never register as an implement | +| `unsorted/` | Real pools whose machine could not be identified from the DDOP or the issue. Move them once known | + +File names are `_[_].ddop`. The variant carries whatever distinguishes two pools of +the same machine: the section count (the Sulky reports a different pool depending on its configuration), +the section-control method, or the language. Put a `.iop` next to its `.ddop` under the same base name if a +machine's terminal pool is ever available. + +## Columns + +- **geometry**: where section offsets and width come from. `static` means the DDOP carries the values + (DeviceProperty), so they are available at activation. `process-data` means the DDOP only declares the + DDI, and the value has to be requested from the implement over the bus after activation. `none` is a pool + without sections. +- **section_control**: the method the TC picks, in priority order: `DDI290` (condensed setpoint), `DDI161` + (settable condensed actual), `DDI141` (per-element work state). `none` means the TC cannot control it. +- **connector_types**: DDI 157 values found on the connector elements (0 unknown, 1 drawbar, + 2 semi-mounted three-point, 3 mounted three-point, 4 hitch hook, ...). + +## Privacy + +The serial number string and the identity-number bits of the DVC NAME were zeroed in every file, with the +length and layout unchanged. Nothing else was altered. Do the same to anything you add: some pools carry a +VIN-style serial. + +## Adding a machine + +Attach the DDOP to a "Submit a machine" issue, then add it here with its issue number in the `source` +column. Only add pools from a machine you can name, and keep `unsorted/` for the ones you cannot. + +## Files + + +### sprayer + +| File | Sections | Geometry | Section control | Connector | Source | Note | +|---|---|---|---|---|---|---| +| `AGL_SPRAYER_7sec.ddop` | 7 | static | DDI290 | 1 | ddops.zip (no issue) | Y + width as static properties, no X offset. | +| `JD_740_SPRAYER_9sec.ddop` | 9 | process-data | DDI161 | 1 | #34 | Settable DDI 161. Geometry only as process data (Y + width). | +| `ME_ECO_SPRITZE_SC_BOX_9sec.ddop` | 9 | static | DDI141 | - | #64 | Mueller SC-Box from 2006. Per-section DDI 141 elements, no condensed DDIs. Static geometry. | +| `ME_SPRITZE_SPR_MIDI_3_11sec.ddop` | 11 | static | DDI290 | 0 | bench capture 2026-09-20 (no issue) | Full static geometry (DPT). Best reference pool. | + +### spreader + +| File | Sections | Geometry | Section control | Connector | Source | Note | +|---|---|---|---|---|---|---| +| `KVERNELAND_EDW2_ISO_2015_2sec.ddop` | 2 | process-data | DDI290 | - | #60 | Kept as kverneland_en.ddop in the DDOP editor folder. Issue #60 pairs it with the Exacta (a fertilizer spreader); model not confirmed. | +| `SULKY_ISOLINK_SPREADER_12sec.ddop` | 12 | process-data | DDI290 | - | #31, #93 | Same client NAME as the 6 section variant: the DDOP changes with machine configuration. Geometry only as process data. | +| `SULKY_ISOLINK_SPREADER_6sec.ddop` | 6 | process-data | DDI290 | - | #51 | Geometry only as process data. Localization label carries a control character (see #61). | + +### seeder + +| File | Sections | Geometry | Section control | Connector | Source | Note | +|---|---|---|---|---|---|---| +| `AGISOSTACK_EXAMPLE_SEEDER_6sec.ddop` | 6 | static | DDI290 | 9 | synthetic (AgIsoStack example) | Not a real machine. Full static geometry. | +| `KUHN_ESPRO_2sec_DDI141_en.ddop` | 2 | process-data | DDI141 | 2 | #10 | Same machine as the hu file, English strings. | +| `KUHN_ESPRO_2sec_DDI141_hu.ddop` | 2 | process-data | DDI141 | 2 | #10 | Non-ASCII strings (hu). Geometry only as process data. | +| `KUHN_ESPRO_2sec_DDI290_hu.ddop` | 2 | process-data | DDI290 | 2 | ddops.zip 2026-09-04 (#10 machine) | Newer capture of the same machine, condensed setpoint DDI 290 instead of 141. | +| `SKY_EASYDRILL_E2_1sec.ddop` | 1 | process-data | DDI290 | - | ddops.zip (no issue) | Single section, geometry only as process data. | + +### combined + +| File | Sections | Geometry | Section control | Connector | Source | Note | +|---|---|---|---|---|---|---| +| `AMAZONE_PRECEA_4500_6sec_2booms.ddop` | 6 | process-data | DDI290 | - | #35 | Planter + fertilizer + microgranulate. Only element 4 allows section control. | + +### tractor + +| File | Sections | Geometry | Section control | Connector | Source | Note | +|---|---|---|---|---|---|---| +| `JD_6000M_TRACTOR.ddop` | 0 | none | DDI141 | 1 3 4 5 6 7 8 | DDOP editor collection (woifi_tractor_en.ddop) | No sections. | +| `JD_6R150_TRACTOR.ddop` | 0 | none | none | 1 3 | #31, #93 | No sections. Must never register as an implement. | +| `JD_6R150_TRACTOR_minimal.ddop` | 0 | none | none | - | #93 | Reduced pool (no connectors, no booms). | +| `JD_6R175_6000RW_TRACTOR.ddop` | 0 | none | DDI141 | 1 3 4 5 6 7 8 | #34 | No sections, 8 connectors of mixed types. | + +### unsorted + +| File | Sections | Geometry | Section control | Connector | Source | Note | +|---|---|---|---|---|---|---| +| `MFG89_EDW2_ISO_DEMO_64sec_5booms.ddop` | 64 | process-data | DDI290 | - | ddops.zip 2026-04-06 | Stress case: 64 sections over 5 booms. | +| `MFG89_EDW3_MAIN_DEMO_24sec_3booms.ddop` | 24 | process-data | DDI290 | - | ddops.zip 2026-04-06 | | +| `MFG89_ESA_FC_DEMO_2sec.ddop` | 2 | process-data | DDI141 | - | ddops.zip 2026-04-06 | | +| `MFG89_E_COM_UD_DEMO_1sec_2booms.ddop` | 1 | process-data | DDI290 | - | ddops.zip 2026-04-06 | | +| `MFG89_IXSPRAY_DEMO_7sec.ddop` | 7 | process-data | DDI290 | - | ddops.zip 2026-04-06 | Name suggests a sprayer. | +| `MFG89_PSD_MB_DEMO_12sec.ddop` | 12 | process-data | DDI290 | - | ddops.zip 2026-04-06 | | +| `MFG89_PWRW_II_ISO_SIM_16sec.ddop` | 16 | process-data | DDI290 | - | ddops.zip 2026-04-06 | 16 sections in one pool. | diff --git a/tools/testdata/implements/combined/AMAZONE_PRECEA_4500_6sec_2booms.ddop b/tools/testdata/implements/combined/AMAZONE_PRECEA_4500_6sec_2booms.ddop new file mode 100644 index 0000000000000000000000000000000000000000..9d399f009fd55018f1aa3d67c01da69f33688ac3 GIT binary patch literal 2705 zcmaJ@OKcNI7@qa~j^j8FaNY#Uv{g?PflylYB~>(`Y88S~Y^C%#aRz7E^=`Tzr*ck6 zt>D6=xm2pu2Zo|52M%o{3L!jKIkw`>u~!Ihgb)Ns`p?)gc0$;b_y4}{pZ_)gx~s2~ zkcwVUBm|Fa`KqI1-I|{Ebsg&o+4(m4zVblTy5h6#=M7gY2^q`~eCE0~#SLNfd?lz;&g0D@2l%b)^6uo9Xf0@Z-B19cFLxgP5v29Qc}o8F-{upFXL4-L?$ z7`Xyty|EPIV1e;YU@69-0%HSRit&4a@h%4Y@;D9`81Ds_Vo*}i)Xj$yY1ihGaV9-( zN!RnZO^HD$IUl+ZYEmfy3AFSHIW?N%NoET^;OEjN8*&}Fg9B2l$uzxBAJB*N5q(Ub z&|6g1Rck9oFirp=(R2t4JV% zhYkySYSf>QRhSo$7HEYP&<0(-UDs)yVbs_7N7LyMDLjAD7V=mGfg3c6Kv)Cjz|`n* zWt^sai4m^u$v74t6n1t*$hkSR-=uT@k=1owVmr^+l5KXANz+Mc=oa0DlBT$y8+Pq9 zrp~slF|h{c6671 zrf8c*(&5Rh+u#3k+CBO!q2U#H!O{uO9FnH%uukl{=h`eLxfD9?`}6>^8e<}{1mOXF z!w7^MH>tuN@nrri90Tl7T6=g%bA*JO7e-x5c|=`3*cH(h0P)R}i1kZwak$_yWhItPI#;JvPs&M7%OyI$yK&OSDz>2*=5qs)Z+14s&!X zT5G#atj4%1Do?tWt|oyDBGK3gSB+=GI&TLDK)2q2VMLqDQKn!EM_N+?(2dzN#0!^| zP5tSqTg2@YKc$YQu*WdM(PClJqCY=drxgmY7tw~2W|reY_yKVomx5@Au-SIpI@l_% z#c#MerR`tqt7D`!?hh!}Md9aBox zv3*iG2tx&-b+w(CyteAN{Tz-CEhQ>>HmjC5JT z%{aIpSi&|j1D#PdygnNsix_F)4GT{*YM^>SfM=EKT8JzTP$PA)4|A`_8*G_a-+oqJ zoGsQi zz+r@=xFVbZ?hRn&uz?=lK3^r(2!9~j(#Jj1&m~NM97~(b`jb))a_Nuj)0^NI9Kja< zM_jhT@{h{=?ZrVjGL$LINi)pgT3Hz9u$>JVQ*`_0kD{8aAWvpjc$WEB&l-MUG z#4m-$NdUo`xERLi7EV?z77blSm7$@+{ePjlf~q{UNOcued54Qs*HERNMQ8Q(9*ZcT z`R@zw2LgkFLfZGB{bm0GJ#I-^Z06cX%Mn=&1=oh>+G8wqsheV=G9CW)|VzPPT zN6^eq;DsMZz)KjwKAgNm?czZ0d za%PmyoqD6jBs5WA5>hY)37CK+sB~HYCecnj%5JsVV!Z2Sr_e2|cFm^X?H%=5P+0ZtWgNTItpsh`9uL+R=m$KM+B+XA#WI&@0%Rp&JIxae za1Z|&o5E+ACec6fKbM|tFVfH0NPvmm25UOO?fSh_fyR4R&QZLU4639ORmMiCm5cBz0ubqSp=4_zbUqTGHAy3yKx-q>8lu(TgJ^9St%uNs zXn@uwo#+Csk3{Q;j{Y4)>%-`D2t6AM&}k`6(gK|xiB2Q>59Zs8sT)Xo7@Y~B=ac}Q zkuoGB(3z3w45EK1gXqjKIvYaI#{+a$%95-=XGfy5i2gP)h|Uh8EmNf*ao94UVe8I> zk$;$92);PebQ;I)-iUGQ?K!a4XfVfcxd~FeZXQ_cf#9oeR)C1#E4Lfu`tt%R8@EcN ch2PW`YO8&X#Okis=1i!YeHD~o=c7~SA9!zaH2?qr literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI141_en.ddop b/tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI141_en.ddop new file mode 100644 index 0000000000000000000000000000000000000000..f9f3fee053b64f1e6ff2e606d4bba0396a17b67b GIT binary patch literal 2285 zcmb7GOH&(15dL-rSR#o52?y~W8^3Kp0$c{1RDzN0N)-vjQaR?hmPXPV?JnLOA#+Pr zQpP^ykob`V46&VmkW?!FA>|x$$}ztqPtr3huY{3ZW!O9bz&MaM7 z3{8zq0`Cw1@_G14SK`HA<&rz+r4rmm2-r=fvl=wt5A+*C4Ba?{4s@amQS=~=Ui6_K z0~jQ97z~GT1S2?#V>pfzIEhm@jWallQKXhqF$jcWu@8-0$+Y<_6OJ&-%y7B!K*HUq z5IhjTyigdcaq^}o3_Hi~OWq+=i5G9i6Q;! zU>3M!@5`bKorK3<#P?(nMYNel5WNR~YKXj@bq%XbU&`hkTewb{mH8Skt1RO9OVcC$ z`xq6E=No9f2B-D}NH!GY9#O#W7ox2L^$3GI98@77fq<{v{|j zmlGB0fN+i6N~7l>{v)`TvJ33g2qsFLxn{8>l>88Oi4<=}D)WNR83uF`x?C~yvVDFY zUeypKRyFgYr25IA5vgY{DbLqTLzs@OD2GW#kGy&LqfCa~q?Apuu5vhn7P5CNu5mZ- zc&1%s?(@QoV3VM(PaJne+ViH%b5c+F9mST8NN=ikt=M%gs+`*_afKOno+VY#V|YZQ zzL${_3yO6d+Y~3drwm@ntQFt{b_jhdv+5e_+->;illV@$JqN_P%YAf5Uf)xAOla%_ zTX1)@TH?ISJYPb=r}3n@>KQzJh3Z*+|0>l{?Eaw-zmew!qf!>E>=db1>8y^nNY1fs zDxc%hLO=_~FpdP0GSe~K2Ex&Rz??$CQMw0sWI4EZTAvA!zremWb1eqhOU@?$KiQ(oxEyCxH@gD#8e3HDRPCBo z&8&mVbugWcprtc#^ZZc@(=(!z;vLz9+wz;k^&}fA6MPNx{0)CcMcT)$U9DxJsZpk^ z(%JSVfg>vL2JXth8?O=g?rQ{YL+D`e#TQMRC0i+M3pk1}aYKgYH~>xC*JsP{Jhn>f zKt6q2l32vY@?{L<{FhPkHYIH02O6WH0OntoQQavtZcPwt ZyYAL{D@p%f$hMKd-|&G{kC*R46-qaT;= zSi1jE0QFcPKq~-zGn2J+LqY+|^NWt<@dtu!z(>TkO4dJwn$vuHMPOC<3`*=JgET## z=lumQx4TRG)u>XGx;DN{+CFoAW|t1B2GzuMl;jHGLI6{!r9d{1CycG_!1$>n@_rh^UcL&V1(U(pd}sBxh9< zIX%6oXQoW%<#^tfrdIq#rk97i-q1Y9r2xplq@@jilcjz*?Jw}WWqMjB>(~Jx?d|wS zIKAOaP98LMV3$laH+bH)Dc$5!n`D+7_>BXq8f$sW<@rojYEl-3-%U&RJiZy^QiDQN zU6Il)s_jtD{;@O|J}tcyTo68KJ# zg+jqHP~&X;U|{ddA{Uso4*8pL zT!U|gZiUZO66;WDzSWO}0aDZW{OSCHCz@s~6@hwe+;yhAwWLasSE3L7xPo3>#Q=tJ z4TBgG(e=pz*Hl1D50w63#ub@Evx{0}(UA>sMb5bR|FGel>y>k^h_dkF+M?BGbt{|G zgW+-&BQz4V(CYz|dPm+x8NmSF+{fDNj%f!2LHaGgk{w2cTH5bCoyan6M&4rC{nzjq zk!VX8!bClyPv`?^;OSC$ZtN#P|d3*n^~5MF)(;R^aXhHsprFPx}>mMkaj zGUza1VovDmI0i*2^)f!+wsoWMgs==u0HRk3M(KUmTlsx3Ai z$PK6Vlc4~3DA$cY{4$`bvy6Zhx$*sz&j(dymeybH_2ALQS9?5&_1EDV$~&}P-E$yq w(8ikFsQk6_6{S3F^hlLQ#Y2%EyO*f-;<55ON&2TndmD1^=V#AeRn#f_U+vN}k^lez literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI290_hu.ddop b/tools/testdata/implements/seeder/KUHN_ESPRO_2sec_DDI290_hu.ddop new file mode 100644 index 0000000000000000000000000000000000000000..89ae5f5f169312a4b6ab4c718029cf63c0ad7ed1 GIT binary patch literal 2677 zcmb7`$!`=#6vn^qv0(R(IPlbBdIR z<&ZaX^{xD!w=N9$eSK52NB~PCqZtiaG!OLKj25(_4Gm~S6PnSE zjo5?^bfOF0G};4(E!c`}*p408iCx%@J=lwV*pCAk85?Oqh(t@J!WTD|q8v*_K z;<;mlcO}pYa&BQ|K$LQM$1oKXv7$KVIf1w%**07ywo|d;D%6@3i}xhffX|_&-ZIHE z2t_fR4GK$3)Zd5(O>5~Ax5zu>e#qV0AvK|qxXm@WayUg(1=Nh@{~~2Hk%@|_9lvWx zZOHiCb*!AhhP_8h@)O21HeV4YIBw3a_ex%z2=z6N-U0)P)dF4sFD0NGY5d@+X?InXo zR7-_296DoPlmfNYTS$Y#KKK6kICHI>Sd)OnjuQ?-CqAmQlC9Fa z;`ydnh-IZ_rXP=>Q+;b4jsv8m3B|LS*+3@EXeI%D?U?UPrdhvEkvCufhj17NaRf&( zgyT4k6F7-8IE7;vlyMHIQn#@@sdi~O6ji$lv#N?wvdnoZ6%MOv*YgTCu6PGk#jBt^ zzQV3d`YFT77mTRP9KkRZM;5EDKq-sVtyB>l#XHMbmtV8os5B_uGOXAktkG-h&d`iJ zXIIpHR`=jIo*8xR=ZBlg7_vIO##Lu-k+giq*fv0K7$}W*3Jz3%zRga*&5^1lzPhqQ;sA97QEm$RU zF|gPS6?!#8vBy--Z;InwnXD?|2UWsXpp}_V&}b%wy2e;0C~%ufe3IU9og8k;rCyf@ zT@}`=4*I5YtSq;}F1wq`cwG(Slrp}7ThjQ(8yMeu1LHOH_QxyE(EBa(iuKo%cO49v zurMwC4XlNx)mls_7G1+Ee}BwN*2IUT+8WD>dqGf$um^J7!go}Ay5n)fG2Ez1uo=22 zlk!DV2Rn@g+U*K>Q9PfcoGDQTn0YFIzQg7HyFB)#{H&tiC#QAIrS|QpNp0w^?6{*k zyyi~_BjB!TxBu|VQC*+n1l&^{-#`80n66Dx`+l`TKpkIIDaiKMaU0ru)P7Jk!8WO5 uUUjVdb=wK8-Zp!c%frf~M6Y|0s7IA)_1F68JCgbytG<^nUYykQ3HM)|Q-DSQ literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/seeder/SKY_EASYDRILL_E2_1sec.ddop b/tools/testdata/implements/seeder/SKY_EASYDRILL_E2_1sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..b46a478e8f9d875d022bd53c5cb3d29653a9d542 GIT binary patch literal 1255 zcmZvb%Wl(95Qb-tbFp)ClBPGPgn>1pVSxZ4RVXQ|0)<2stdNzSBv$P>*vEnHDk0Go zARYizUI7+tc>$1Eu|>Q_FJaDcTv9b&d1k&d|NQ6D+-d+A>o=?x=i59;kLu4?R{{R4 z!Jd9lSe^PxSOHA^+&a2q3zP^JPQd~HV z8F~~K|Ho`cCQ2%?`0dv%#`-oTOqIy}zIvCXcY4<;x7j>|k0e@b?+&?jPYCYXJw6^& z;RxPPL4VZkhFn-r3ygBdus@Z1rgA6n7_>^YZFlMmHZu9uIuW z8N_XsEe*fY2`z8RZ=dK%jPhkd_QEd8Trr{4lr#IlHY@Eg{ppjRF zm{;ixege&yPf%+Z@URm&Lph;R&EkDhUD&XL9*+WRn~Vka$aY-Xu~%vvb!<@Q#6kwvavAlOB>#GC6!lzE?JE!R0m%`@5ksNo;3gBQI^_@jY>i=eSr(Aq6}j zOG{))1imZPLU||tn%MITf3V9Y=&Wvk2 zhbBQ6&pN(9OIY{m|UE1|F0%ls=^PPa+7^J|!F#sY` zi9tqB9w%X-!4Vw8Ay7#JmVIJ-^m^-zDrH)xV-^c!poEetV`b844r@3s+oeF$+X45; kd0nGs2Ba{J8QPdUGSu({suD{>DmOV5t6rx?6I~tu0c03iN&o-= literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/sprayer/AGL_SPRAYER_7sec.ddop b/tools/testdata/implements/sprayer/AGL_SPRAYER_7sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..56b37368f9b1fcc94d05b4fa4ba0623273f83468 GIT binary patch literal 1660 zcmZ{k&u<$=6vw~gAIaJY&W}1jnzqR_p>0B&ICkRLP1B~v8?{J*uE;8dOQppbyDP7E zt@XOdf5VC|gY@;D+y&M5DoVE3n#aiihzbSSv&HSBXTD8Jo>!xZ-2A=LS=FrlZ z`j&rSA;pg?!JB@6m$4cpjH&N0zrU7a;kPWGlyRken|qdJp=yO1egPw(rm4{cCNN3u znhAVL0+1+rt}9A`=X0}FmrWVw1fF4M642gQe^4$rL=cxNCh>vdYH#+`19J+Y$jhEr z3lVLa-g8J|2D6yMJkDbQ7jO}mu!tpaT*fk1?XDc)X()d|}1pswA4E&iAj-q0gd7XE$Cb1x@UQE^h)x~ZK}!|F@uLhK6}j2;UH-uBoyBGRM4`#k zP`P{>GndIIpZwKspEb(LK%=~f2up`Se!&%7m4>=Pr}7QC_kZVNUM0e>zY+J0)vggC z6H$a!tVzP^P{JA!KK`Sh@DdTWqKa@GHzeWuP{Iu&-2bzmuug>S5k=TQN)k4P5>f;A zVUq~iQAJ22BMIrDgbWen4xQbHEh6k_im;8WBy0~QWQkCp>nH3GAs17G9P*No8%oF% zVRfaSa1-HC)ADI*TXEuO*u^dE;WqAIA9qn8roKCH7H`RWC^k!rDUV$+yWjXXU8Oz5|19g+!rVcWfHaW6x>UgeD{V_=+y9sUltH rO`qo8@GYA2QfCmgZLN{heM)B3Ngr*k+z+LlTaNcIC1TcQ3i6y zOlRm-FZuwTNuQ!ueTY6md(9JY=kC4aIF``eUf=%K(%L7jr2^?Enp3sSji&9`4NKY8 ztqb|`Vxb7gQ}}rKG@Xxrg<>WDL9grebpS(6XVZ}8DvqtItZLSs4b!Y~fHYjl!ADrbx`01S z!qQcO3O1k$czY6-uMyOsV^hFaxR_G=y`IKw{Qr3gi1)Q*5SNEiSMuKH#wT9qH5x~emgA%pi{=V#TL-W2pX{uF=5yWR78@A&_1 z_XkI^?em7{@V&q(=U42}(;!x5(C-ElUh)L&HU8jl12D&tctH9CdOyUHvp4*0FL=O;Nl!5K}+j3-RK7p=xK(y#sX&kClD+?JxX8Z8O|AJ=fv ztmpVM!-G2%>}P&im82RUZ%4+vk+F52L^AmNqcO*Yy`M_CE|1w16SbunRb~|Ud->Cn f6niSFG%3(n{k;;Cw)prZjp^4CHKm?Yn#}wk#6+A? literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/sprayer/ME_ECO_SPRITZE_SC_BOX_9sec.ddop b/tools/testdata/implements/sprayer/ME_ECO_SPRITZE_SC_BOX_9sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..e0119f902897a8104cfe7c829c1ee276fe9e5bb4 GIT binary patch literal 1231 zcmY+>OKTHR6bJDCNn#Q6Y9h97#FZe}*Ty{Dm^Ks9HlYEh>8@#|X_NGkG)+*nkD!pG zpisYn1l+iADd@(n^xX#BxDs5slzxGD&SWOxUI_fpy?1`+%tT8?0Z$^9jzy=_Pm85o z`DMnD3cd&c5!b(}tJw!*-5tC+uzv&D%CrORww6jB7~6wiTOk{`JnURU^`TK7si((A z`8`;>Z=9ZVE`NLHvJXgyLqkR48Ri|&OdkSbtZ4m?;;XrZ*HjtT@ zkc8cbL)l+OS@MzOEGm+ZCFk&5@`>a;+P~?xylGNf#T{vKA^U51EV(6l9dEe<&LkJ` z4)&{^`$aUnc?0};fm3KPop?4`ZY&~$B`hP09P(Jj8rD(328M~R+0e%q33iOoD2>UM zo%`^+@UMWPP!2w|8@Gl?OX@@P1pB`tz&X#N57Q(3t#Tvon$$Z0% ztzx)mkU|s|Jiy#%xAjPnIb#`kh`DD!+U-uOu)?{RbDCU)#%Y40!bKEr zT;V2|^Zc~9sKU9KbD7*EO;MZ@!c8*Q^jgzRDO{YnFMlj9p>Q7N+$N{f45esRI9=gp z6fVV_era*D3g>0cV{&tprg>TrZjQNcPHVcf!p$?+`;rR^*Uy~SkJ9oiLci@Dwe+X}bC k+~qxs+f}&h%=t~OMs;e?o^UmVt1DcCxesBB+v{=v0cy7Rg#Z8m literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec.ddop b/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..128f5286820887999b7a5a8e9d361973353be964 GIT binary patch literal 5246 zcmai%+jCpR8Nk0!c0(MSBsK|2TxNQpNm~+ptRu;~q%$RnB?c#$@!%7a@_;!eC$^#^ z$>b5g^=nMld=p|fodg*~7-B!GTAH`}>l8!(nYG7}JfQ9_-P>Vch`O z(FW6PZ>$LNU*G1<9p`?rW2`vb1hAMK8n^=Nx8d(Mr(;`M>|p{2ZGz*={z?=T!uovfVDVe8F2-WY{!f6{zJ)-oj)c zkad#-$^C#3z%?l+Us@=5T4}!EmF6>7fVyl50Ahz(n$Jx-_NeXF^d5i#Xlh^8Z|8E8 zX}2^#`Vo6)6g+BOY1)O-`;P5>%-UXo77So}zg4jI)7Y9PcfJajAkfk|P_W%63+Zt$ zozH2#`J7kCJKCUUdG_KW4KW5q8uqrm-b6lUIiIjm)9^H#slS-ZSn2)wPg&bRSo|mc zs5NOHu!@eSC6j&p?F>W^@vcW~$MQ-G6B-#VGqSI-*B#hG!fSSBJY6fzc?a^&WNFU5 z!Md{WAX^LG919z%4@7ONeiK{H8 z0>D>@Era%mX#9*dx^SIdT|LNHx6<=D>+#@2>S-M+EllMm^I4kf3)WnOIe?Z;yKTqL zQ4XZ5;cB|kr5jNm&;Cp(MCgG1N zD%#Vf$Ag@jflJVFuS@hkY7l8cZ>s7H7lgO`ZwxOJWRmd?OE^bu{SenjPuN6yCM@MA(Y z))CX?;e@!586Y#4Mc+RV_IKUxIrH>Z~Is z=ihH&tAdiV?`7Dgpyb>;4fiQ1IrGjymx7Y>?kwmEO3u1-&|ROZ$+`0oR8Vrp{T2)b zCFk2~a22qn-L%JS&-Un;V<)J#EA=Hi{w~0^I-Ps^c;_r!rc7PWE6@bJM2D(NwPj(v z=(58+Wg@-Hdq+$2g#$9#v$x6ZV4*2VfM&z=0fOsOBD{0yz$^!3B52@!hx$V57W7w}jbU4zre89&NB3 zZ7sDtXrsI$8$;5r+%jXRGGh(3e8zBP#u{q*j1j!ZS8-j9<5C(8m^~GuW}GD=Wwt_d z%b>U>XvQkq;TDKu4C9!Y)*zD92B&XY3NNNzo=x!dD7j8CA2 zBeL7$zWdh0-G<4nQ{^U*C&=v||5LaL#PV|!$Vel%9`3eQa*M9vZey6ngP1`_b{q3` zi*mO#xouXt3FIKTEz+-gP{?PAKr()A0&yC-vAbo;Y?s_(Yq?tiUG%Vs6S7;u*Dc1~ zTyoP@ZUXVh?QS}f>beP}=;tPoiAHWQ?$#l>#oM{tAv}yn@F-5pZijrow>Wn@Om1!5j6rPLb1JGk6wJcDQP9G;iuPM762ROs5` z9prXr%68QXFWVV{IZJxSR!X-3S+C5TEBjT)sj1&$obzSDPGtdS|NK(sdfCue#|{gIq9U62YfZE~hGfyqV-O883L8d)P!JW$F2f+OQG*_!u%01k z)TDVqqlP_-t##1d>{}oIT6=Al*=_)cHqE2z@y@|s$yhPgO4W){D;w2vr4D2~xX~y0 zs8^~+%{U!U8dI}*C?FUSh7EQ&p}`Foba>!}4-4=kfgl11Q6hpUVu*vp%G|XuZyA_k>*5a*?<1I!$Gfv&8%QxF%()PpHlFsGe3ocWsDe(te CA6_*8 literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/spreader/SULKY_ISOLINK_SPREADER_12sec.ddop b/tools/testdata/implements/spreader/SULKY_ISOLINK_SPREADER_12sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..001079c9a5520a871cb2b5c57c7fb64e224d2d29 GIT binary patch literal 1147 zcmZXTyKmD_6o=2bNfRf10s~^gfYhmlG;L^$kWfjAs6^|~M171_V#g+lQzZ_R4GF1C zNPUi=GO+c%u#~+okeC=j{0)6h;oO_jUW;t^`+mRUqZ>Oh76)h>s+0=0lUGwyWkb&z zCL-IUg|pV@(@ogs%y_2m5dU9oUJpIEVANfQNWQqfYP?&v1fG@>&-B z;S|jq(ni!sR|8F~&)fr&hAg82uQX-Z9rVhaEPE8Mv}73zd8IAO`(dwqANUFp7v|X$gh3LQlFi=Ky*HBZ2 zyTTvUi9x_nqhR$J0?*=e@ZAWE8+Oie*LZLaT_XMgdSlo5c^<2DZcNw}S9daosyQ1R zFW^hUO_$g-1z*Hh#Di(O$fKJ)x`fk&lsYm^!4B~wFhxnm)s#zX)@qbH!e!1X*BwJHP#Xy!nl4KJ_yqe#*BsOw zl`p!*7oTL`&;OoFw#_~e3qQ5pjHRYuPt{jfeYIb3Gng<;y;|VU8B4CT^Dj31^UgzP z$mi}^I|@LU|6K{+T}UmmK?{Z5L_4V`j;PljEUBU0AG7&VvOZ^ZBu~t&vX5h MC==Man1fjP3t8Yct^fc4 literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/spreader/SULKY_ISOLINK_SPREADER_6sec.ddop b/tools/testdata/implements/spreader/SULKY_ISOLINK_SPREADER_6sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..9b25972cef7e1e108269d84e05da6aa36544cbf3 GIT binary patch literal 973 zcmZXTyKmD_6o=2bc{ypzb3jZOkUF&xQo>6}P~svg(Ks|wlZ;ks$5s=kN^YQRNJwQu zYWf&KWnk-jVJUlGATcq5_#671!nrr4bwsxNeZSxF(Y2$EBmmA0)+!~(9aGceRnr(X ztHD?_b|c;g@cR+GiOh$h$G=#-FB*;6^~V6v)CMvL_+OV4_+b|3a3574g%||G2|bw{ zNM!V3YJd+mU=!9EIUg|p6I~x-gfB|G1>3L#J=lv=*uZI=!Cl;=Q3rU4N0?)IQ5yw+ zBu(=M^`siT6`+Z&nOi{8^vYPUU0Pn*7iyP9ue`0aOWP~s;dbeG<(-IErnC*11W**+ zvT7z*bBoh16gO3+Y)TU9}0CnH{Bw))&2sn8zm`r;3*}$WKtb6 zCY*A;EJY6nfq^n?a8pg2d_g>_7ej#I4#`?GB%Z<-;JX~mnoiN?OCs1nPQ>3&-@vjs zPh*|V9hy_)hFdUI-CYrQ2452HJjIqJ`7FL79?Cdn5nUD0Ih-J*w2);KuDM~7# zra7(IY*KCy=LJi9**?Cfwa;fORr)SP^7sN|;Q&98JexKP+^M)VuR6r5$l#2pM;9SW zM|h1)=Sk@w&#`ZG%^}@Y#iDCs@jUx}{CZB=I{QE@^334{Tg|*4Z>_HSn!n&mNHZ;? zUJ}n4NiB4X|3KvD^@k7`E8esXVvCYhF1&rfuuSKr({qqlT-U4-T$b?Cmml{SldTmA bzTTaTGd3n_s}d}KZrrCtmB8A`9K_mRSG^Ej literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/tractor/JD_6000M_TRACTOR.ddop b/tools/testdata/implements/tractor/JD_6000M_TRACTOR.ddop new file mode 100644 index 0000000000000000000000000000000000000000..f81cf7c9974e4ff9ce786a7de897d95ca611320a GIT binary patch literal 1829 zcmZvc+foxj5QhJ($tL6+P9k_1Ie93-0E&WIma?A8Lbo6V>W#I=iEO#-TDt?vomc9O zk6@J#;RE?3Voz^^nZmt+^#9L%-90_yY^?$LN9G8`-60 zYUOHgXD9YPzVYHB?_5J24P4WPA=ssD?_Cs%$G&Rslvp3e2u3le^^>rx_q`wx?I;Xo zTSal1En_%^aZG5-8Q4W$EFMSE`wGkFAaEX&TAqTPbA1(sRaT}ki;K9Vl^e8>{=8lb zfAHI-NwS8STj9>v6}+en$b{jyqoUIa*8jL9GN% zo1C>?xuSLOK^`6wZGiI`=|3nlog$}W8jNg8oQ-KPl3DB~GhsmflS`R%$?2JR!L6t; zn*%R7P-PYeUUA?oeo-qL|K>z&cyIl^gvARO`fU}8JU`;Tmq?D*m@>vox=HvnDaIMp z8GOuR)ZO4*j(LpIuTkmxDvfq0Rs5&w>lmRdORc>)>`#6fCn-Gsj8aRKl(8Sa6W*sl ziXduxiZbToTg4c;>b!VUhKaBIeP12i(u0)6V#X>A-iwvr-V1n*a3eoV$84C}Fq~eK z9eqvA7L0HU4BaSyl*>1Cqx3;4Jx`_A`w6Nyc4JJh@Fg@le~PiGVDQG7mkI`GfVf8CnyQdgD#SBiV|*FzLux zkXJ?RDB$%A_zn~r>mH46EViTgz1UO)VDTpRr literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/tractor/JD_6R150_TRACTOR.ddop b/tools/testdata/implements/tractor/JD_6R150_TRACTOR.ddop new file mode 100644 index 0000000000000000000000000000000000000000..0575ff9811d499928bf1be8730c0c3fc99b39e68 GIT binary patch literal 1207 zcmZWnOHUI~7(FvBr7ub;AeIPT7C;CBm1ls3QHn}JpeYkxyP>_%3GED-hkz>+6CVp* z=|*GXZ!rD|{{?@*Ki~$x`?Zv&OlEV>Ip5=+bMw1vK&RoFCEsyFxy5Nfgz*kPGr9H; zb91@J7wcPm@7e%;WZ-INl7IQr0!ECw--A>^=tMXZ2yAw=RuX}aR2<(DctG=vc& zc>=SF?m%bqMPWx2Wl37Eq#Gtd2uXAyrRWSovFCxT3d6KdJBZWJtMCWV6Wn#LB4yQ! zFf*)}8AOuIPdN8j9Vdzm76pZaoB14!UdRbAAphwp26U&w{CZ;fr zCFBeF7`dWH2X5~n_QjgxxMj;WeL1QmalCJn9JEUk_|PUfY?BzX_(2N+*_Kgjj%`a7 zkuWaLfvluvJpcfy3XsT(Z*s)$qp*HWs7wALTr$?Ss z4SXqBLPNcvcII1-&7yvM1-b?{Y|l4s`lSk0MbEZPb7$bi^ Xe_GZwb)LMf&`$ge!@(0AB#6bKBAnY>6Lt zLx04qp##fmP98$gZ)7V+N2pf3+r=Nxx~mKo!T_Tg8`IoA48K>`wl#QGB;4i&RCnh2-Z!tkIZk z1S?-#u~83klsZ#Uy|0$gxFz_88`7pT^hML|vW92{4tZCEuUMHmt5mscN2Ld_aH1zu zR_MvN+Z&L>r!4NEmwv#xCuE!1gDz)Xw`r@tfi`ZzHEhmMbDc|HVBR7amcZCG3#1a@ z^&`-GJp>p5ZuZTVrd9F2O!vQkIwdVpz2hLVLdfq=A2&5k-B-^$^iBhT5&<+*EBpiD CxCqJs literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/tractor/JD_6R175_6000RW_TRACTOR.ddop b/tools/testdata/implements/tractor/JD_6R175_6000RW_TRACTOR.ddop new file mode 100644 index 0000000000000000000000000000000000000000..3abef74935b9513b154f688e5606bc73b414dc34 GIT binary patch literal 1829 zcmZvc+foxj5QhIO$tL6+P9k_15mYR}a1hUB3G1mWbqi9U-cW0t$d=2lwL75Pd8OX? z2v+$JK9El$_Vgy0Dcl=K|NqR_-P1G9<{FS+X*QedTgWvR=9|la$l?or4pGqjkbW5% z(m#I=od>N9G8`-6BCcTtw~)nUT)`aX(ZmACV!^IFjH6JArET}cljpo93mbV9u%sOW zu*>Vxi+cu(HqBxXMU=3p4LRh?kK7HhF5A-ImvNR111O_{s+Lc|D)IWQ9VvsvCN+aI zTDjWW*@?Z6Z@jq3JJ(Q01IyYl47;@Dy^BKe*jMeH66+%v#TbUPegbxN)e8dAj>1s3 zRTP)mGLDm&z@)aEhF#>v;&BwcudsX;0_QNL<@2y}uCJo7%E~k@;1XuFa-9~^pY63k z?)yoN%voQ@uGK<}0CJo4caw&5zIXx3ExfK;= zbKn&Rs?6fRYYv>jFKR{O-<*hb@2$U=uy_%}zpX-%=SQr1iR5UFDdW7Pn}ko3Vw^#p z!N)vC-3`v=n8zr6nM&7JX{nE1-y_tn8|JxEzBW~{={{aE?!y@1yUH}b=D%tp8kBk48S z(bvRm!6>)D(2erPxO_u5N*|)q^Hh4hpP+hUH_r45UqYkvCmEXx25*9SsbGL62~;4^ zAa8a=ILn0Le|Oj{PcWxnHfZOV)-M~(DPm@cSxdLyp(jEdk*Aql<>$eKUSMcfF?g4l z*HsMEETL>d71RBWD3|dOR%4_UDK8KYIvsk(=n)kye>c6@S8$#tgT^zV=S_;Yvg7Rq zN+eP$8l{A%XAX0}EmR!mg)1CSN%2%3urQB#66$Uob$l8*Lu+D6Z@i&+BwMi;CLI|I z@~WsE1-yO%-+@BonMb1=i>)YrFE$hbSiHggsihC3*`S-;pq^xO=ni+NCmAieOD)P# fi%Q)syFJ=;XDFz@&-Y;M?mC<9?ew<}WM=*X)P~;W literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/unsorted/MFG89_EDW2_ISO_DEMO_64sec_5booms.ddop b/tools/testdata/implements/unsorted/MFG89_EDW2_ISO_DEMO_64sec_5booms.ddop new file mode 100644 index 0000000000000000000000000000000000000000..9fe0950f8e4e035e9036f980c61c177c857c2fc9 GIT binary patch literal 2140 zcmZwG$x|F<9LDkI?IA(qMRT#d1Ok#&5@v<4Mr9cyi;)51usiHCFkuNHi+f^}xRJ$u zOC%)j9-4|u`n*Wg>v!NXL2=hMG_yWg(vbhaHpc}IFMmFpVl&80g| z^;RcishU_KR#%-$q*BpjBAVpLL%6(e6Z_HPTySM}iDzLuok<_SUPK#&F&`Zym=4-w9l_E|@2A z$#*sStmn-8uoNIEM$D4oKF?Q7efPYs*<3cFT%1lMiNdAo;k+_ny9 z>Tt)NXV~@F^xc5Fe$7T~`EJ5J-_5x1y9E_4*j=X;mA>2XxbJpU`R>HB`|m!xu;RNL zXMOiz)%PfVL$G`QWBA?oBsLuPe#Z7Sg`3Vju(Ov8w*NxePUo<0Gew)3kS>?^_J5D>e z5KE9i5-DUr4ER0Rfd~dLgmI$?A6h^v!vX$|p$bP3MGSG+3z|g@Mo@=(G@ubpXhsWK z(Z)}s6J6-$XEcg2Ok&EGD&ayAE)^h$a)Uk0k`N|v&|oj&Va}AI0+l#~qv*#VYVA-d z4?Xh#hZea=wX<^%W7uYw2vLP)LQG+W5LY-$NGPllk_zVtDTOscjly|Ct-=MuafOS7 zI)zJwdW9DV4GJ$38Wmn5G%37HXjXWIa6;i#!V?Ow5uQ}IOn6G+3ZX^ebwaDcRYIG> z8-#X+HwkHlbwY>2HNr`Sw+K%wyiMp-c!$uX@GhZS;XOi+!Uo|PXD`t1`*EFcO5q0K zS%sT~UWHqPjKcebK7|hm{R$rv1{6LboL2al@SMUYgsj4+gh7SR2tx{=6NVMOAdDz{ zNf=eQO&C+SLl{^1itxO`CLyPAmoTBQMVM5$N0?H$PncHtnlPjA4PjQ{Tf&^ecZ9sc V_k@DN4}^Jz9|;QzKM{%^{spY5T_OMg literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/unsorted/MFG89_EDW3_MAIN_DEMO_24sec_3booms.ddop b/tools/testdata/implements/unsorted/MFG89_EDW3_MAIN_DEMO_24sec_3booms.ddop new file mode 100644 index 0000000000000000000000000000000000000000..3883f7bce7029e448ed2b1e05879039c70d59bad GIT binary patch literal 1238 zcmYk)%T7~49LMqBOp6y>=|Y_VLX++Q?N4QcfBEh z)F1SR;@)sDJm?DreKh0(4%&}#QC(3=e_g54EHtg0)rm83oyQWWyKok&)dRTAz0Tf? zH1{%hA2t1)?#C^l{6gyBwIGqwBj)7N7;9+Q^bUUzsZiWh7dKE9zhiQf?floq>z%OBwARyrZK~TaLK}f>MA>lE>u!JWBBNCnxj7oS$a81H< dg6k3v2yRGtL6DU2k{~7Fkl?0-R|IJTe*r~y)bjuU literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/unsorted/MFG89_ESA_FC_DEMO_2sec.ddop b/tools/testdata/implements/unsorted/MFG89_ESA_FC_DEMO_2sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..27faa5919db7cbbc1c604a794c74fd12d991f780 GIT binary patch literal 603 zcmZXSOD_Xq6o#J}incCQw5Us|OPjdNNE(+&L^`z-gxEF~Qb~ivec439io_2f;wK=n z@)Jm`Ed7)^=Tj4j`7+6Ko_Wvt&SJdH9FWMbFBX<^1uwt6GUiS=Q)5$(n;m!DaW@Oe zFy)4O9M^T^-QJT1D&DFWq>h>(LkNec$EtFeobs^xMqzmjH?ib%Gi%rpMl{pHk@Bc= zgk$Bjaw{i3M>$o_sBh!U=PoX=tMj{sfA30g>2uE?Zs$sOtLXk6!m_{NUamEt()mel zb*Axg?y#$wPVRjk1Anw#1DZ7k~MfktP*sBVMO3vbJ5? z_3|4QmQhiHglMLP2(3hEBTgsn#AqT+7rpe!s?t!49*Y1SG}1%2d=ecH8ThXoAE& literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/unsorted/MFG89_E_COM_UD_DEMO_1sec_2booms.ddop b/tools/testdata/implements/unsorted/MFG89_E_COM_UD_DEMO_1sec_2booms.ddop new file mode 100644 index 0000000000000000000000000000000000000000..c88fa5da197b1bcc00caf2c822bc24509cc34366 GIT binary patch literal 818 zcmZvaO=}ZT6o#Lpu5oz?)ib=>u(g@vRf-x z%U-?cRXwj%bxRVt%vth+Lfsh0bRIsE7GrEclBag4XYB|oS%=>nRvs;+qv4{u%gfoYL7apLL!Pz@#h7HSF-wMd zxwbAV$hrnCDtOf8k6L4$Z4NooN?a;&sa#?1zm?jwAki7wt&c-rn@Pc>?e_T#c{-1G literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/unsorted/MFG89_IXSPRAY_DEMO_7sec.ddop b/tools/testdata/implements/unsorted/MFG89_IXSPRAY_DEMO_7sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..9ba584ccd0c243bdf7e4dc1c20482f5517b3d361 GIT binary patch literal 756 zcmYk3%}!HM5P-jP3Y4ZLK%q1R0;L-_aA|A&>&6%^pb5sCwp`j3q7Gjk@4H-Pe;PAh8O>h6b|ckiv%f?7SO2DPiJ z^=kd{TCKWPmy|50xpOk4jVWB}J(Ir}gi$!k2TX>6@)(1^EDt%2A(f$t_;IY}IL>lg zc~^ao4ds^dgfQbno@5KT$@wW>VoSuDGtDdIJ>@*FJ;-ikz)tnOVeGd!D7x~2mFDB+)=CkIw*~@&d~D6J zoHt!u{PE{HBRlK;15vS8a)*jNCw^W#yJGFr<8ZTWWRv=hFv=J+%p%eX>OqQm7G&q5 zQqs5VS z10Uo87sq7U?)uLAcvTID&a7ATK^j<}&PJ(Q+Io z&}sBaCV^Aoe&Hm}EH~m@ctCs;E-c5eD%>YNjce%N`5D~U`OUbM^VH@1S=>v;a0(Cd zErwh1Xt@nfHs6k%`pb9W!)ChiX}J^iZ*W}yL8Kw4E_n4X&K8w)t*zOip?mwcp_bKvrhE`Jdn!4llYl@plN52tolh+ek@9p~w z>q;)2s~rwP2%`ZJxO}<lIT6Ud4h;si~l5oHg> zFohEoBNU;aD6k9%MUg@V+Zm%2k)Rl(hz3Q8!VL7ig-{|DELeM?=nu2wBiS^ CGjRU^ literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/unsorted/MFG89_PWRW_II_ISO_SIM_16sec.ddop b/tools/testdata/implements/unsorted/MFG89_PWRW_II_ISO_SIM_16sec.ddop new file mode 100644 index 0000000000000000000000000000000000000000..0839659e9296f0c2fbb820a42ab5dc88d5439edd GIT binary patch literal 762 zcmYk(NlpSm7>41mt3iftriL(2;;evxF^F3l6Bfpf5LglyCME_=oc9`Bx$poc=ow60 zc?Ug#3lD;SwNR!n($0pvA2 zIL7#@)@u#4j?d@_CT6RvK;el9R5;+o99(e23k^Da@FR#23`7w_90??mf@zs5uD}w= z@OI#ucb2ykH=^fo%R9&2g*(w+yh8C{G9lMcP?+7VE~8KLVN@E$K$stM<4J7QVe(FM z_hFBZY_}gx;Q{KI2QkL~)s`8;h_}5zLOt7K;8AoKFTCxsqL}>9;C9EL{j#aYp^Hwz zCprPY=p+Ku?pZm0Bi`Sh8;qS?m^~|R!?|(4t037Z3Ph#{+3;}v2f&zgm!vcXrhDBOBWmqEMZ!PXvCUD8HLg1ERmB1sz8i7}a gbplO>4FX+;B7ska5`kZaGC@FwO@g2d6@rj}KVU;|?f?J) literal 0 HcmV?d00001 From 14984a55b5af5ba0ee96f1d03fd5b9cc6c9527af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gunics=20Bal=C3=A1zs?= Date: Sun, 18 Oct 2026 15:55:45 +0200 Subject: [PATCH 2/3] Add synthetic MIDI 3 section control variants to the sample DDOPs The Mueller MIDI 3 sprayer (SPR_MIDI_3 V07.10.05.33) presents section control in four ways, chosen by two configuration bits: old or new SC DDIs, condensed work state on or off. Only one of them has been captured. The other three were derived from that capture with the firmware's own DDOP rules (from reverse engineering): it sends every template object and only filters the element child lists, and old SC mode relabels DDI 289 to 141 and 290+ to 161+ as settable. The same rules rebuild the real capture byte for byte. The files are named *_synthetic.ddop and marked as never seen on the wire in the manifest and README. Replace them once real captures exist. Co-Authored-By: Claude Opus 5.5 --- tools/testdata/implements/README.md | 19 +++++++++++++++++- tools/testdata/implements/manifest.csv | 3 +++ ...DI_3_11sec_newSC_perElement_synthetic.ddop | Bin 0 -> 5286 bytes ..._SPR_MIDI_3_11sec_oldSC_CWS_synthetic.ddop | Bin 0 -> 5244 bytes ...DI_3_11sec_oldSC_perElement_synthetic.ddop | Bin 0 -> 5264 bytes 5 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_newSC_perElement_synthetic.ddop create mode 100644 tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_CWS_synthetic.ddop create mode 100644 tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_perElement_synthetic.ddop diff --git a/tools/testdata/implements/README.md b/tools/testdata/implements/README.md index 4dcd606..7746333 100644 --- a/tools/testdata/implements/README.md +++ b/tools/testdata/implements/README.md @@ -31,7 +31,10 @@ machine's terminal pool is ever available. DDI, and the value has to be requested from the implement over the bus after activation. `none` is a pool without sections. - **section_control**: the method the TC picks, in priority order: `DDI290` (condensed setpoint), `DDI161` - (settable condensed actual), `DDI141` (per-element work state). `none` means the TC cannot control it. + (settable condensed actual), `DDI289` (per-element setpoint work state, actual on DDI 141 of the same element), + `DDI141` (settable per-element work state). `none` means the TC cannot control it. Only process data that a + device element links counts, because some implements also send objects no element references. The + `section_control_method` test checks every file against this column. - **connector_types**: DDI 157 values found on the connector elements (0 unknown, 1 drawbar, 2 semi-mounted three-point, 3 mounted three-point, 4 hitch hook, ...). @@ -46,6 +49,17 @@ VIN-style serial. Attach the DDOP to a "Submit a machine" issue, then add it here with its issue number in the `source` column. Only add pools from a machine you can name, and keep `unsorted/` for the ones you cannot. +## Synthetic pools + +Files ending in `_synthetic.ddop` were not captured from a machine. They are derived from a real capture by the +implement firmware's own rules, to cover configurations nobody has captured yet. Replace one once a real capture of +that configuration exists. + +The three MIDI 3 variants come from `ME_SPRITZE_SPR_MIDI_3_11sec.ddop` (firmware SPR_MIDI_3 V07.10.05.33), one per +section-control setting: old or new SC DDIs, condensed work state on or off. The firmware sends every template +object and only filters the element child lists, so these pools hold process data that no element links. The same +rules rebuild the real capture byte for byte. + ## Files @@ -57,6 +71,9 @@ column. Only add pools from a machine you can name, and keep `unsorted/` for the | `JD_740_SPRAYER_9sec.ddop` | 9 | process-data | DDI161 | 1 | #34 | Settable DDI 161. Geometry only as process data (Y + width). | | `ME_ECO_SPRITZE_SC_BOX_9sec.ddop` | 9 | static | DDI141 | - | #64 | Mueller SC-Box from 2006. Per-section DDI 141 elements, no condensed DDIs. Static geometry. | | `ME_SPRITZE_SPR_MIDI_3_11sec.ddop` | 11 | static | DDI290 | 0 | bench capture 2026-09-20 (no issue) | Full static geometry (DPT). Best reference pool. | +| `ME_SPRITZE_SPR_MIDI_3_11sec_newSC_perElement_synthetic.ddop` | 11 | static | DDI289 | 0 | synthetic, derived from the bench capture by firmware rules | SYNTHETIC, never seen on the wire. ISO options: new SC DDIs, condensed work state off. Each section links settable DDI 289 + DDI 141. | +| `ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_CWS_synthetic.ddop` | 11 | static | DDI161 | 0 | synthetic, derived from the bench capture by firmware rules | SYNTHETIC, never seen on the wire. ISO options: old SC DDIs, condensed work state on. The boom links a settable DDI 161; the original DDI 161 objects stay in the pool, unlinked and not settable. | +| `ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_perElement_synthetic.ddop` | 11 | static | DDI141 | 0 | synthetic, derived from the bench capture by firmware rules | SYNTHETIC, never seen on the wire. ISO options: old SC DDIs, condensed work state off. Each section links a settable DDI 141 (translated from 289). | ### spreader diff --git a/tools/testdata/implements/manifest.csv b/tools/testdata/implements/manifest.csv index a9a5a20..b018cf7 100644 --- a/tools/testdata/implements/manifest.csv +++ b/tools/testdata/implements/manifest.csv @@ -9,6 +9,9 @@ sprayer/AGL_SPRAYER_7sec.ddop,sprayer,7,1,static,DDI290,1,1660,ddops.zip (no iss sprayer/JD_740_SPRAYER_9sec.ddop,sprayer,9,0,process-data,DDI161,1,1797,#34,Settable DDI 161. Geometry only as process data (Y + width). sprayer/ME_ECO_SPRITZE_SC_BOX_9sec.ddop,sprayer,9,1,static,DDI141,-,1231,#64,"Mueller SC-Box from 2006. Per-section DDI 141 elements, no condensed DDIs. Static geometry." sprayer/ME_SPRITZE_SPR_MIDI_3_11sec.ddop,sprayer,11,1,static,DDI290,0,5246,bench capture 2026-09-20 (no issue),Full static geometry (DPT). Best reference pool. +sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_newSC_perElement_synthetic.ddop,sprayer,11,1,static,DDI289,0,5286,"synthetic, derived from the bench capture by firmware rules","SYNTHETIC, never seen on the wire. ISO options: new SC DDIs, condensed work state off. Each section links settable DDI 289 + DDI 141." +sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_CWS_synthetic.ddop,sprayer,11,1,static,DDI161,0,5244,"synthetic, derived from the bench capture by firmware rules","SYNTHETIC, never seen on the wire. ISO options: old SC DDIs, condensed work state on. The boom links a settable DDI 161; the original DDI 161 objects stay in the pool, unlinked and not settable." +sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_perElement_synthetic.ddop,sprayer,11,1,static,DDI141,0,5264,"synthetic, derived from the bench capture by firmware rules","SYNTHETIC, never seen on the wire. ISO options: old SC DDIs, condensed work state off. Each section links a settable DDI 141 (translated from 289)." spreader/KVERNELAND_EDW2_ISO_2015_2sec.ddop,spreader,2,0,process-data,DDI290,-,576,#60,Kept as kverneland_en.ddop in the DDOP editor folder. Issue #60 pairs it with the Exacta (a fertilizer spreader); model not confirmed. spreader/SULKY_ISOLINK_SPREADER_12sec.ddop,spreader,12,1,process-data,DDI290,-,1147,"#31, #93",Same client NAME as the 6 section variant: the DDOP changes with machine configuration. Geometry only as process data. spreader/SULKY_ISOLINK_SPREADER_6sec.ddop,spreader,6,1,process-data,DDI290,-,973,#51,Geometry only as process data. Localization label carries a control character (see #61). diff --git a/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_newSC_perElement_synthetic.ddop b/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_newSC_perElement_synthetic.ddop new file mode 100644 index 0000000000000000000000000000000000000000..a00ebf4f85228105d19a977a284b746bca309a41 GIT binary patch literal 5286 zcmai%*>4-i8Nk1fQXR`uY)i3YTMf`nYUi+{6_=!VxIk-LWKh{rAXsbJ?L(ARyq0J# zNrk&qBE^TA)bf#}_0IOs~W(z zHkfX^ye!CneH%A+ocrmvvEpzOz+!S};2N;sx{q$h1ILd5pSJI1aOR=uk+`8$BSD7$og+c&ztD^ru58KxW%7exbl2F z@B^zmo6b?&?J5I2im&+dzrWA?1e-J2Znn(VRRo)|-8Qv+!&_Kn*fe4ksO4MU!ek(j zwUYzM{eTd_RVgc9S|~V5X};i;<}=rTx=aWFVuxOu&rMq9sA<>q9)JO8YG2WB=5mv1 zyEH%g85=VS4z(`V%|huD%XB_xZLdNL2C%)~C>Z-`ZpD#1UxP~!Xz3g%nD*0!^thAG z=ak-j&MD+AWzaDkb8(TT7=t2Bd)FRkBA+v?FW9VUc$TfyU(98U^#1&ptnDBy{sn*3 znluj>Maxl=$v!?h0}({LZ?|a~PHAC6Aw!pud4mnNU=s zZ-^~}=7?zgiZ$ABlZLJwWUO83`I_}O@G14Q4wV+Ba+CQit@RCSF2Wo@%Z8n%W#%Xc z($#PkRqIlwqoox;bV#rbC}O_61dTq`qaJzYB1qPUo&Z-Z=|bDO1<-3N%53=%Z>=ZCMyE z+U#SV(vimU-qF(hI4iu%APonh05%+hSAmECENjzLg{oTVsy$_9X#_M=wNkMggGfLV z1ls6peYH@!nlTHeLlqg;zz7Weivxp#pYG^DW>J{8e_2Xp>M|Ha`!J>-COX4-2F>e_oLVc zf4#OEI{J<0RHde1-ej|P;bSnq4qz?suUEfxmEyZ~7r=UXf9?pgyBuaM<&HL3j<%L^ z2d$NNWPM0FkvmolRaUH_+$)AFE7nl%6(e|&@8X&o$E6e+(0eLG%{)s)N^gbcmO*hv zP_z|v!mSX+7{)PyTahJ7hEgO?2}0W*rEOON0QXh~NS6jv(fGl~XczuYx8b!FM#_vb z5~nTL_m9QJDk9mJkwn?Qib(2ZWGnujUI>5s4n3Af))U%xQ-+eLoMnmx`>Adi#Z?7uwx1k&&4Cy*x^`GvXPO35#BH}}(V z4-Vm8+~@Ywee*NI{q~UGYA-*543Xa_|MBn>$X-7`f$VGK$L>EVeU;?bvy%HgiNp95 z8aU$id(!v(dbr;(`E`2v3FImA`|f`pegZN4`~))6$ghX{t(N?vtGM47rtu(V&~p2Y z`T9k3r@{6tJeg(AA!6Ht${R+N* zG45xRpX%i&5QqHkr_WSfKYa2e&ixLP zU);-2AVfy-sPar4A?`bMsRYdj^$Vopxft+gOm*9SDpm{~56l=kIxZr6#gJ<^&c*$*f!QWEcV`hCV1@i`3zPG~5QZN^NEd_I_p(WjA{tt!_xg7ui literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_CWS_synthetic.ddop b/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_CWS_synthetic.ddop new file mode 100644 index 0000000000000000000000000000000000000000..0cbffa8b7e4a55802d51b5e311fa46efc4b51897 GIT binary patch literal 5244 zcmai%+jA4w9mjt^$t4IofDJC#%}lo)@(_Y$tt9J0XIj8griPgD#ET()z*&dI7FtP; zcICtl7dI*9(vWgX7%-4HNlVpNW` zfGsUB-SYOb5dZaU*wA+F=Uc`~!+wCp)X=~UV83;Lxu5VgncZVLHQnS*0Mozlp-I=b z9w0sre8w9-iO=--eAyfkpNl)*J(Kj|7+v4y*M+uMH~WzF-;u5#r|Y}YHJjlczkcQY zmlM7pnBBQdp4#r$7~oZW!LPsjHv2Pd%I3P+G+)*b^yj)QYWa${u*%Rsq8F*_S zz?ZX91F3z0;KNmEvrt|r+Ddu8XqV@+H-Nef@Bw0*R-VsKn#QPM)%EU&0r0o3=r{8D z$&6K=AHB)$83mhKmup6`{IO}+w^-Yo5I`Tc_UlD`AC0Zpa_0rO3cf(+K+&+CFJ{K= zOd+rI7V>tnU@C*QZX1h>G{hK`XxK+~*%O7lZhpo_&A^Lnrv6est7rBVK4)zQVDazx zef`jq*^%h0`yWXom5MSZq8Urj;(<;Rh~Bui|xd1 zR#Oq+ABcfLV?;E5!5S^NOSi5ZWUN)~`I7b6@G12)50w|D@{@%e&Gi*)F2Ni?pkupX z8hOeMjNTe$mO>pWZc+ZWBCcA>MD5oaj z_ij|Irz@BUacTSs0)KMlVr&oMLSq^}aOL96BPl$74GXSZl03p8;qeN5=*ERZ zdx53e)d(izI5Kd`R$5LF%?IgQ6C zjsC!T2joE(k5d*M87YY;D2a}Yl*5yhLq|qR;cqF0j*OJS*C>PXyP>8fC-4*{&>!e@ z5R>!wI_1xqlGFD)N}n?&XYVv+&zX{wcZQPZOv$-BOSyBV?Ks)buI>EFc)pzd^fc5eUJyd3QrObM2 zX|Wp>N(itPw8SAK}Ge+?WU&Xa`jt@dKp!HOVx^W&5 zX{{NW0)yfsK~Y!G4hJBHaZF$mHzSKmmO?^H9-}2!d;mYJ!qO*$p{RV*|kj?lOJq`dJPnG{VLE$dXWfi^ptuvzM6_r0i zEs|TfnY(Slt+)+)F(tcgA-A8px`nyhR&x8Vo0~wkk=x|IUEBoH>*XepR3kTb5~sCC zCAUZmciWCT@M-MFowD0@Pqzqn+d*#2nq2lxAWxIq?0;O`1k&&2CXk(t+#=j?DldK$eu=S?29L@t&-e&R&uw! zIE>Gsjw7<$UeA5&;cmm^*6HRZkmtzlZ~t>~6Nv8RCXkUvZav&>wd59C#ofj*g9k8+ zrtCK6=@#Q|8FJg;<|dE>5RrpV`;K#r5!^K>-T5ZO&2C%oJQaG;a;NbOp2c%`UY0vu zky}@#>ksb`w>wj@tDW$&ohq2Kq<3_=^c5iMRhV-XzuGu;=eHQ=d_}O+^#Eu8`_fvw wY^bi~hL>;_FXI)wDjQz%HWc3{vz~^6xlD%FR=61o=8C7GV6HYaq;Hh}1H>P=2LJ#7 literal 0 HcmV?d00001 diff --git a/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_perElement_synthetic.ddop b/tools/testdata/implements/sprayer/ME_SPRITZE_SPR_MIDI_3_11sec_oldSC_perElement_synthetic.ddop new file mode 100644 index 0000000000000000000000000000000000000000..ce6b1cf3de971b3078c0e5c763dd673999a624cf GIT binary patch literal 5264 zcmai%*>f938o<9#G9j`}5}Sm?UaEGW!UKn8jU?*`RcsPV6izVZ!V{C_0W&5OThT}| znvuaKhZi=^u^hP;ikM{avK)r>E>(G;ieg_@FX70=@kp`%z&=0~MX^Vjue&9s$C(-F zZMuKc{r$SX>93U;=?7>V%nnbMbM}les&)+zJvur#kQo?_DI<|&SdD}uec@OvtO9Ip zhuQWwS4H@5VDskA(+_N&sEh^ymNO$m7lHjYesnDrXfb;ybZYvPHv!E4CV(bAe|8-S zaNu*^@Q>tNUm%b#5b=e0IbY0|)_Jn>9<-rLwJ57t3~aF@F)L%YXnNc52nd;Jo zb#eSt_Rcui)Vf+T%GFOy!~Tr5y$T@=V8@_d*7wlZiY<4(2InCV>K-Z^)>Gx&q@61j zmHtxEE|*MY*w$@hd6|ZofC>$J^DcX;RMgGS*{C^qhRrlsDdzRup3)brZ67Ruhre#i z7<=`KX)Bq`0Dn6VaYVfRQNz^j>e7@#hAWJW%j|U%?jqr}+wyi;sV>-iOXhTS!Me)2 z3h)qH3*Kexv_=c&v^A3}+Sgcb5niOV>S|12I9F4QUV@h(80uQF(ln82>XRi~=b=u* zo3tF8wpHv>LAP`HiltaHdS16?N+!j^nWv$jV(F$>D)e&BQVMJhteNVfX;^G0F0q)( z0N*2qhK(`N_$6z!;0nFEW|*;7z2_^|W5YkGr){LVG*g@|6=<%nS#t#z076|m4AUr5 z4&)l)Dq*!JtR{FsQ}Abkx?Q1UPdFCk)HCpw7Zr>3gyShrJqv&FqT=bEa3apBX?WX< zO7!)FQxQ(hz&l=4ilR@8=%0gkiTagW^l6G-RYmyw;XT4`cf(Yb;#Z?W-~cRoa#1o+ z<3iyeyzj}y$w5sBi9_&#CzoJ*kQ5rT@K;YR$vo1+<9D#+$)(978WA4P!{5BPXoNhX zQQ`3d{N0m_l1DTyJbn)!dUA2{i1rDO!}OUzsPo2k=36>P`4|jsYz?UckgQ0F0 zF**NUru=iK>^DeGJ*N6ww0oO7q- zjQb;HoI54w+v{)%Fw~(L6NYWrbj-06RN0>Wh8=%r;j&BT&H>&z4;Ls?H}DEHMK95z zYEf-jnygstFwbg8@ABU9>f$6TysIDw`=AUK9EMkchybi=SHlWbwdw_H#wgGm&_dNJ zjJ+5^0)h}|r_=gExq2aQlnt9IGOUL^V8C7&hY2vD2zjcxhhY+4hqLfByaw}dH{OHy zVhLpB-8jeZ0v$WqxXNzDFl}CAbXGpJd+>*6}-9Mqo;D~^JBg^k{W55nX|fDOFgt$t}?itp;p0Gs6XxgpHnT9{61 zWweo6v`%Ve&}wZ(Hbtc!xnag=eMSeha>iJFMhCTW#yFnitGM3bcs+%Nw7xpw80R{X z)!LvXG%T(NirPv$+zJUyVhYo^1zDoxDMdWq{ZH6%p~u=~g5DL7xIc;zw3;&>iGrJhKTO--U zZ{cAR}b=$$vfU1oD`loj`Usvt#$3thQFN>s!O^ zp1@Ij5_KGN+CAaBcYWM$l;&>8*?s$e4?BVAes%&GYi8HS?bbDM?iVf1agqPO z9U(jRYix{M?*wv`?4F`h)j(uBfgJO*6Ugyqc4=<69$H%KrB{vI%Joj*Nj!z8@r+aN zL``pFop+Y=2=_Z#^J~<6*-sVBDH1%qTDlj=f;HxJ&9E_!qoRv(&eRmUJxe Date: Sun, 18 Oct 2026 15:55:56 +0200 Subject: [PATCH 3/3] Resolve section control targets from linked process data only Move the section control method detection and the DDI to element mapping out of activate_object_pool(), request_measurement_commands() and send_section_setpoint_states() into resolve_section_control(), which takes the DDOP and returns the method and where to write it. The client keeps the result as its SectionControlPlan. What changes: - Only process data that a device element links counts. Some implements (Mueller MIDI 3) send every template object and leave the unused ones unreferenced, so a pool can declare DDI 290 or a settable DDI 161 that no element owns. - "Settable" is checked on the linked object for that element, not on the first object in the pool with that DDI. This replaces is_ddi_settable(). - New per-element method: DDI 289 on each section element, actual state on DDI 141 of the same element. Settable DDI 141 stays as the fallback. - The global Setpoint Work State (DDI 289) is only taken from an element that is not a section, so per-element pools no longer get it on their last section. - "DDI 289 (SetpointWorkState) not available" is logged once at activation instead of on every setpoint send. - Device element children are read by index: iterating get_child_object_ids() reads past the end of the list (AgIsoStack DataSpan bug, #110) and mapped DDIs to random elements on the MIDI 3 capture. Order of preference: DDI 290 > settable DDI 161 > per-element DDI 289 > per-element settable DDI 141, matching the fallback rules of the ISO 11783-11 DDI 289/290 attachment. The new test_section_control target resolves every DDOP in tools/testdata/implements and checks the section_control column of manifest.csv, plus the exact targets of the four MIDI 3 pools. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 18 ++ include/section_control_method.hpp | 57 +++++++ include/task_controller.hpp | 10 +- src/section_control_method.cpp | 198 ++++++++++++++++++++++ src/task_controller.cpp | 228 ++++++++++---------------- tools/test_section_control_method.cpp | 203 +++++++++++++++++++++++ 6 files changed, 571 insertions(+), 143 deletions(-) create mode 100644 include/section_control_method.hpp create mode 100644 src/section_control_method.cpp create mode 100644 tools/test_section_control_method.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b49f5af..2093864 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,24 @@ if(AOG_TC_VALIDATE_IOP) add_dependencies(iop_validator test_async_log) add_test(NAME async_log COMMAND test_async_log) + # Section control method detection against every DDOP in + # tools/testdata/implements, expected values from its manifest.csv. Made a + # dependency of iop_validator so the validation workflow also builds it. + add_executable( + test_section_control + ${CMAKE_CURRENT_LIST_DIR}/tools/test_section_control_method.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/section_control_method.cpp) + target_compile_features(test_section_control PRIVATE cxx_std_20) + set_target_properties(test_section_control PROPERTIES CXX_EXTENSIONS OFF) + target_include_directories(test_section_control + PRIVATE ${CMAKE_CURRENT_LIST_DIR}/include) + target_link_libraries(test_section_control PRIVATE isobus::Isobus + isobus::Utility) + add_dependencies(iop_validator test_section_control) + add_test(NAME section_control_method + COMMAND test_section_control + "${CMAKE_CURRENT_LIST_DIR}/tools/testdata/implements") + # PASS_REGULAR_EXPRESSION makes CTest ignore the exit code, which is the part # a CI gate actually reads. add_test(NAME malformed_pool_exit_status COMMAND iop_validator diff --git a/include/section_control_method.hpp b/include/section_control_method.hpp new file mode 100644 index 0000000..f14ad08 --- /dev/null +++ b/include/section_control_method.hpp @@ -0,0 +1,57 @@ +/** + * @brief Works out how to switch an implement's sections from its DDOP + * + * Kept free of the TC server and the CAN stack, so tests can run it against stored DDOPs. + */ + +#pragma once + +#include "isobus/isobus/isobus_device_descriptor_object_pool.hpp" +#include "isobus/isobus/isobus_device_descriptor_object_pool_helpers.hpp" + +#include +#include +#include + +/// The section control methods, in the order the TC prefers them +enum class SectionControlMethod : std::uint8_t +{ + None, ///< The TC cannot switch the sections + CondensedSetpoint, ///< DDI 290+ (Setpoint Condensed Work State), actual states on DDI 161+ + SettableCondensedActual, ///< DDI 161+ (Actual Condensed Work State) written as the setpoint + PerElementSetpointWorkState, ///< DDI 289 on each section element, actual state on DDI 141 of the same element + PerElementActualWorkState ///< DDI 141 on each section element written as the setpoint +}; + +/// @brief Short name of a method, as used in logs and in tools/testdata/implements/manifest.csv +const char *to_string(SectionControlMethod method); + +/// @brief What to write, and to which element, to switch an implement's sections +struct SectionControlPlan +{ + SectionControlMethod method = SectionControlMethod::None; + + /// Condensed methods: element number per condensed DDI (290+ or 161+), one entry per group of 16 sections + std::map condensedTargets; + + /// Per-element methods: the setpoint DDI (289 or 141) and the element to write it to for each + /// section index. isobus::NULL_OBJECT_ID where that section does not link a settable setpoint. + std::uint16_t perElementSetpointDDI = 0; + std::vector perElementTargets; + + /// Global Setpoint Work State (DDI 289) on an element that is not a section, if the implement has one + bool hasSetpointWorkState = false; + std::uint16_t setpointWorkStateElement = 0; + + bool uses_per_element_control() const; +}; + +/// @brief Flattens the implement geometry into section element numbers, in AOG's section order +std::vector get_section_element_numbers(const isobus::DeviceDescriptorObjectPoolHelper::Implement &implement); + +/// @brief Picks the section control method and its targets. +/// Only process data that a device element links counts: some implements send every object of their +/// template and leave the unused ones unreferenced. +/// @param[in] pool The client's deserialized DDOP +/// @param[in] sectionElementNumbers The section element numbers in AOG's section order +SectionControlPlan resolve_section_control(isobus::DeviceDescriptorObjectPool &pool, const std::vector §ionElementNumbers); diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 839a4fc..456fdf9 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -13,6 +13,7 @@ #include "isobus/isobus/isobus_device_descriptor_object_pool.hpp" #include "isobus/isobus/isobus_standard_data_description_indices.hpp" #include "isobus/isobus/isobus_task_controller_server.hpp" +#include "section_control_method.hpp" #include #include @@ -49,9 +50,8 @@ class ClientState bool is_section_control_enabled() const; void set_section_control_enabled(bool state); bool uses_per_element_control() const; - void set_uses_per_element_control(bool state); - std::uint16_t get_per_element_setpoint_ddi() const; - void set_per_element_setpoint_ddi(std::uint16_t ddi); + const SectionControlPlan &get_section_control_plan() const; + void set_section_control_plan(const SectionControlPlan &plan); isobus::DeviceDescriptorObjectPool &get_pool(); bool are_measurement_commands_sent() const; void mark_measurement_commands_sent(); @@ -77,8 +77,7 @@ class ClientState bool actualWorkState = false; ///< The overall work state actual std::map elementWorkStates; ///< Work state per element (element number -> is working) bool isSectionControlEnabled = false; ///< Stores auto vs manual mode setting - bool usesPerElementControl = false; ///< Legacy mode: use per-element setpoint instead of condensed - std::uint16_t perElementSetpointDDI = 0; ///< The DDI to use for per-element setpoints (289 or 141), 0 if not applicable + SectionControlPlan sectionControlPlan; ///< How to switch the sections, resolved from the DDOP at activation }; // Create the task controller server object, this will handle all the ISOBUS communication for us @@ -123,7 +122,6 @@ class MyTCServer : public isobus::TaskControllerServer private: void send_section_setpoint_states(std::shared_ptr client, std::uint8_t ddiOffset); void send_section_control_state(std::shared_ptr client, bool enabled); - bool is_ddi_settable(std::shared_ptr client, std::uint16_t ddi); std::map, ClientState> clients; std::map, std::queue>> uploadedPools; diff --git a/src/section_control_method.cpp b/src/section_control_method.cpp new file mode 100644 index 0000000..a0bff32 --- /dev/null +++ b/src/section_control_method.cpp @@ -0,0 +1,198 @@ +/** + * @brief Works out how to switch an implement's sections from its DDOP + */ +#include "section_control_method.hpp" + +#include "isobus/isobus/isobus_standard_data_description_indices.hpp" + +#include + +using isobus::DataDescriptionIndex; +using isobus::task_controller_object::DeviceElementObject; +using isobus::task_controller_object::DeviceProcessDataObject; +using isobus::task_controller_object::ObjectTypes; + +namespace +{ + constexpr auto SETPOINT_WORK_STATE = static_cast(DataDescriptionIndex::SetpointWorkState); + constexpr auto ACTUAL_WORK_STATE = static_cast(DataDescriptionIndex::ActualWorkState); + constexpr auto SETPOINT_CONDENSED_FIRST = static_cast(DataDescriptionIndex::SetpointCondensedWorkState1_16); + constexpr auto SETPOINT_CONDENSED_LAST = static_cast(DataDescriptionIndex::SetpointCondensedWorkState241_256); + constexpr auto ACTUAL_CONDENSED_FIRST = static_cast(DataDescriptionIndex::ActualCondensedWorkState1_16); + constexpr auto ACTUAL_CONDENSED_LAST = static_cast(DataDescriptionIndex::ActualCondensedWorkState241_256); + + /// One process data object as linked from one device element. A process data object that several + /// elements link appears once per element; one that no element links does not appear at all. + struct LinkedProcessData + { + std::uint16_t ddi; + std::uint16_t elementNumber; + bool settable; + }; + + std::vector get_linked_process_data(isobus::DeviceDescriptorObjectPool &pool) + { + std::vector links; + for (std::uint32_t i = 0; i < pool.size(); i++) + { + auto object = pool.get_object_by_index(i); + if (!object || object->get_object_type() != ObjectTypes::DeviceElement) + { + continue; + } + auto element = std::static_pointer_cast(object); + + // Read the children by index. Iterating get_child_object_ids() reads past the end of the list: + // AgIsoStack's DataSpan scales end() by sizeof(T) twice (see #110). + for (std::uint16_t c = 0; c < element->get_number_child_objects(); c++) + { + auto child = pool.get_object_by_id(element->get_child_object_id(c)); + if (!child || child->get_object_type() != ObjectTypes::DeviceProcessData) + { + continue; + } + auto processData = std::static_pointer_cast(child); + links.push_back({ processData->get_ddi(), + element->get_element_number(), + processData->has_property(DeviceProcessDataObject::PropertiesBit::Settable) }); + } + } + return links; + } + + bool is_in(std::uint16_t value, std::uint16_t first, std::uint16_t last) + { + return value >= first && value <= last; + } + + // The element of each section index that links a settable setpointDDI, NULL_OBJECT_ID where none does. + // Returns an empty list when no section links one. + std::vector get_per_element_targets(const std::vector &links, + const std::vector §ionElementNumbers, + std::uint16_t setpointDDI) + { + std::vector targets; + bool anyTarget = false; + for (auto sectionElement : sectionElementNumbers) + { + bool linked = std::any_of(links.begin(), links.end(), [&](const LinkedProcessData &link) { + return link.ddi == setpointDDI && link.settable && link.elementNumber == sectionElement; + }); + targets.push_back(linked ? sectionElement : isobus::NULL_OBJECT_ID); + anyTarget = anyTarget || linked; + } + return anyTarget ? targets : std::vector(); + } +} + +const char *to_string(SectionControlMethod method) +{ + switch (method) + { + case SectionControlMethod::CondensedSetpoint: + return "DDI290"; + case SectionControlMethod::SettableCondensedActual: + return "DDI161"; + case SectionControlMethod::PerElementSetpointWorkState: + return "DDI289"; + case SectionControlMethod::PerElementActualWorkState: + return "DDI141"; + default: + return "none"; + } +} + +bool SectionControlPlan::uses_per_element_control() const +{ + return method == SectionControlMethod::PerElementSetpointWorkState || method == SectionControlMethod::PerElementActualWorkState; +} + +std::vector get_section_element_numbers(const isobus::DeviceDescriptorObjectPoolHelper::Implement &implement) +{ + // Same order as the geometry rows in MyTCServer::activate_object_pool(): the index is AOG's section index + std::vector elements; + for (const auto &boom : implement.booms) + { + for (const auto &subBoom : boom.subBooms) + { + for (const auto §ion : subBoom.sections) + { + elements.push_back(section.elementNumber); + } + } + for (const auto §ion : boom.sections) + { + elements.push_back(section.elementNumber); + } + } + return elements; +} + +SectionControlPlan resolve_section_control(isobus::DeviceDescriptorObjectPool &pool, const std::vector §ionElementNumbers) +{ + SectionControlPlan plan; + if (sectionElementNumbers.empty()) + { + return plan; // Nothing to switch, e.g. a tractor + } + + const auto links = get_linked_process_data(pool); + auto isSection = [§ionElementNumbers](std::uint16_t elementNumber) { + return std::find(sectionElementNumbers.begin(), sectionElementNumbers.end(), elementNumber) != sectionElementNumbers.end(); + }; + + // A global Setpoint Work State lives above the sections. On a section element it is a per-element setpoint. + for (const auto &link : links) + { + if (link.ddi == SETPOINT_WORK_STATE && link.settable && !isSection(link.elementNumber)) + { + plan.hasSetpointWorkState = true; + plan.setpointWorkStateElement = link.elementNumber; + break; + } + } + + // Setpoint condensed work state (DDI 290+). The first linking element wins if several link the same DDI. + for (const auto &link : links) + { + if (is_in(link.ddi, SETPOINT_CONDENSED_FIRST, SETPOINT_CONDENSED_LAST)) + { + plan.condensedTargets.emplace(link.ddi, link.elementNumber); + } + } + if (!plan.condensedTargets.empty()) + { + plan.method = SectionControlMethod::CondensedSetpoint; + return plan; + } + + // Actual condensed work state (DDI 161+) declared settable, the fallback for implements without setpoint DDIs + for (const auto &link : links) + { + if (is_in(link.ddi, ACTUAL_CONDENSED_FIRST, ACTUAL_CONDENSED_LAST) && link.settable) + { + plan.condensedTargets.emplace(link.ddi, link.elementNumber); + } + } + if (!plan.condensedTargets.empty()) + { + plan.method = SectionControlMethod::SettableCondensedActual; + return plan; + } + + // Per-element: Setpoint Work State on each section, or else a settable Actual Work State on each section + plan.perElementTargets = get_per_element_targets(links, sectionElementNumbers, SETPOINT_WORK_STATE); + if (!plan.perElementTargets.empty()) + { + plan.method = SectionControlMethod::PerElementSetpointWorkState; + plan.perElementSetpointDDI = SETPOINT_WORK_STATE; + return plan; + } + plan.perElementTargets = get_per_element_targets(links, sectionElementNumbers, ACTUAL_WORK_STATE); + if (!plan.perElementTargets.empty()) + { + plan.method = SectionControlMethod::PerElementActualWorkState; + plan.perElementSetpointDDI = ACTUAL_WORK_STATE; + } + return plan; +} diff --git a/src/task_controller.cpp b/src/task_controller.cpp index 614bb8a..ffd2d6c 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -183,7 +183,7 @@ std::uint8_t ClientState::get_section_actual_state(std::uint8_t section) const // from DDI 141 in on_value_command, so we can return it without the // expensive parent-traversal check (which has thread-safety concerns // when called from the heartbeat on the main thread). - if (usesPerElementControl) + if (uses_per_element_control()) { return sectionActualStates[section]; } @@ -243,22 +243,17 @@ void ClientState::set_section_control_enabled(bool state) bool ClientState::uses_per_element_control() const { - return usesPerElementControl; + return sectionControlPlan.uses_per_element_control(); } -void ClientState::set_uses_per_element_control(bool state) +const SectionControlPlan &ClientState::get_section_control_plan() const { - usesPerElementControl = state; + return sectionControlPlan; } -std::uint16_t ClientState::get_per_element_setpoint_ddi() const +void ClientState::set_section_control_plan(const SectionControlPlan &plan) { - return perElementSetpointDDI; -} - -void ClientState::set_per_element_setpoint_ddi(std::uint16_t ddi) -{ - perElementSetpointDDI = ddi; + sectionControlPlan = plan; } isobus::DeviceDescriptorObjectPool &ClientState::get_pool() @@ -571,67 +566,55 @@ bool MyTCServer::activate_object_pool(std::shared_ptr p state.set_element_number_for_section(i, sectionElementNumbers[i]); } - // Scan the DDOP to determine which section control method the device supports - bool hasCondensedSetpoint = false; // Modern: DDI 290+ (paired with DDI 289 for global work state) - bool hasSettableCondensedActual = false; // Old: DDI 161+ settable - bool hasSettableActualWorkState = false; // Oldest: DDI 141 per-element settable - - for (std::uint32_t i = 0; i < state.get_pool().size(); i++) + // Pick the section control method (Modern > Old > per-element) and where to write it + const auto plan = resolve_section_control(state.get_pool(), sectionElementNumbers); + state.set_section_control_plan(plan); + switch (plan.method) { - auto object = state.get_pool().get_object_by_index(i); - if (object && object->get_object_type() == isobus::task_controller_object::ObjectTypes::DeviceProcessData) + case SectionControlMethod::CondensedSetpoint: + case SectionControlMethod::SettableCondensedActual: { - auto processDataObject = std::dynamic_pointer_cast(object); - auto ddi = processDataObject->get_ddi(); - - if (ddi >= static_cast(isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16) && - ddi <= static_cast(isobus::DataDescriptionIndex::SetpointCondensedWorkState241_256)) - { - hasCondensedSetpoint = true; - } - if (ddi >= static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) && - ddi <= static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState241_256) && - processDataObject->has_property(isobus::task_controller_object::DeviceProcessDataObject::PropertiesBit::Settable)) + log("TC Server") << "Attempting Section Control via: " + << (plan.method == SectionControlMethod::CondensedSetpoint ? "DDI 290 (SetpointCondensedWorkState)" : "DDI 161 (ActualCondensedWorkState, settable)") + << " for " << static_cast(numberOfSections) << " sections." << std::endl; + for (const auto &target : plan.condensedTargets) { - hasSettableCondensedActual = true; + log() << " DDI " << target.first << " -> element " << target.second << std::endl; } - if (ddi == static_cast(isobus::DataDescriptionIndex::ActualWorkState) && - processDataObject->has_property(isobus::task_controller_object::DeviceProcessDataObject::PropertiesBit::Settable)) + } + break; + + case SectionControlMethod::PerElementSetpointWorkState: + case SectionControlMethod::PerElementActualWorkState: + { + log("TC Server") << "Attempting Section Control via: " + << (plan.method == SectionControlMethod::PerElementSetpointWorkState ? "DDI 289 (SetpointWorkState per-element, actual on DDI 141)" : "DDI 141 (ActualWorkState, settable per-element)") + << " for " << static_cast(numberOfSections) << " sections." << std::endl; + for (std::size_t i = 0; i < plan.perElementTargets.size(); i++) { - hasSettableActualWorkState = true; + log() << " Section " << i << " -> element " << sectionElementNumbers[i] + << (plan.perElementTargets[i] == isobus::NULL_OBJECT_ID ? " (no settable setpoint, not switched)" : "") << std::endl; } } - } + break; - // Announce and configure the section control method (hierarchy: Modern > Old > Oldest) - if (hasCondensedSetpoint) - { - // Modern: condensed setpoint DDI 290+ (always paired with DDI 289 for global work state) - log("TC Server") << "Attempting Section Control via: DDI 290 (SetpointCondensedWorkState) + DDI 289 (SetpointWorkState)" - << " for " << static_cast(numberOfSections) << " sections." << std::endl; - } - else if (hasSettableCondensedActual) - { - // Old: settable condensed actual DDI 161+ - log("TC Server") << "Attempting Section Control via: DDI 161 (ActualCondensedWorkState, settable)" - << " for " << static_cast(numberOfSections) << " sections." << std::endl; - } - else if (hasSettableActualWorkState) - { - // Oldest: per-element settable DDI 141 - state.set_uses_per_element_control(true); - state.set_per_element_setpoint_ddi(static_cast(isobus::DataDescriptionIndex::ActualWorkState)); - log("TC Server") << "Attempting Section Control via: DDI 141 (ActualWorkState, settable per-element)" - << " for " << static_cast(numberOfSections) << " sections." << std::endl; - for (std::uint8_t i = 0; i < numberOfSections; i++) + default: { - log() << " Section " << static_cast(i) << " -> element " << sectionElementNumbers[i] << std::endl; + log("TC Server") << "WARNING: No supported section control method detected! " + << "Device has no linked DDI 290, settable DDI 161, or settable DDI 289/141 on its sections." << std::endl; } + break; } - else + if (plan.method != SectionControlMethod::None) { - log("TC Server") << "WARNING: No supported section control method detected! " - << "Device has no DDI 290, 161 (settable), or 141 (settable)." << std::endl; + if (plan.hasSetpointWorkState) + { + log() << " DDI 289 (SetpointWorkState) -> element " << plan.setpointWorkStateElement << std::endl; + } + else if (!plan.uses_per_element_control()) + { + log("TC Server") << "DDI 289 (SetpointWorkState) not available, sending section setpoints only." << std::endl; + } } } else @@ -885,8 +868,12 @@ void MyTCServer::request_measurement_commands() { if (elementObjectChild == processDataObject->get_object_id()) { - // TODO: This is a bit of a hack, but it works for now - client.second.set_element_number_for_ddi(static_cast(processDataObject->get_ddi()), elementObject->get_element_number()); + // Where to write DDI 289 and 290+ comes from the section control plan, which only + // counts settable process data and keeps DDI 289 on a section out of the global target + if (processDataObject->get_ddi() == static_cast(isobus::DataDescriptionIndex::SectionControlState)) + { + client.second.set_element_number_for_ddi(isobus::DataDescriptionIndex::SectionControlState, elementObject->get_element_number()); + } const auto &entryB = isobus::DataDictionary::get_entry(processDataObject->get_ddi()); if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::OnChange)) @@ -1034,77 +1021,62 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16) + ddiOffset; - // Legacy ECU? (DDI 161 ActualCondensedWorkState1_16 exists and Settable) - std::uint16_t ddiTargetLegacy = static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; - if (clients[client].has_element_number_for_ddi(static_cast(ddiTarget))) - { - std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); - send_set_value(client, ddiTarget, elementNumber, value); - - bool setpointWorkState = clients[client].is_any_section_setpoint_on(); - if ((clients[client].get_setpoint_work_state() != setpointWorkState) && clients[client].has_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState)) - { - send_set_value(client, static_cast(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); - clients[client].set_setpoint_work_state(setpointWorkState); - } - else if (!clients[client].has_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState)) - { - log("TC Server") << "DDI 289 (SetpointWorkState) not available!" << std::endl; - } - return; // Modern condensed path complete - } - else if (clients[client].has_element_number_for_ddi(static_cast(ddiTargetLegacy))) + const auto &plan = clients[client].get_section_control_plan(); + switch (plan.method) { - if (is_ddi_settable(client, ddiTargetLegacy)) + case SectionControlMethod::CondensedSetpoint: + case SectionControlMethod::SettableCondensedActual: { - send_set_value(client, ddiTargetLegacy, clients[client].get_element_number_for_ddi(static_cast(ddiTargetLegacy)), value); - } - else - { - log("TC Server") << "Legacy DDI " << ddiTargetLegacy << " (ActualCondensedWorkState) is not settable!" << std::endl; - } - return; // Legacy condensed path complete - } - else if (clients[client].uses_per_element_control()) - { - // Per-element control: send setpoints to each section element individually (DDI 141 settable) - std::uint16_t setpointDDI = clients[client].get_per_element_setpoint_ddi(); - for (std::uint8_t i = 0; i < NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; i++) - { - std::uint8_t sectionIndex = sectionOffset + i; - if (sectionIndex >= clients[client].get_number_of_sections()) + // Modern: DDI 290+. Old: settable DDI 161+, which carries the setpoint and the actual state. + auto firstDDI = (plan.method == SectionControlMethod::CondensedSetpoint) ? isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16 : isobus::DataDescriptionIndex::ActualCondensedWorkState1_16; + std::uint16_t ddiTarget = static_cast(firstDDI) + ddiOffset; + auto target = plan.condensedTargets.find(ddiTarget); + if (target != plan.condensedTargets.end()) { - break; + send_set_value(client, ddiTarget, target->second, value); } - std::uint16_t elementNumber = clients[client].get_element_number_for_section(sectionIndex); - if (elementNumber != 0) + else { - std::uint8_t state = clients[client].get_section_setpoint_state(sectionIndex); - send_set_value(client, setpointDDI, elementNumber, (state == SectionState::ON) ? 1 : 0); + log("TC Server") << "DDI " << ddiTarget << " not available for sections " << sectionOffset + 1 << "-" << sectionOffset + NUMBER_SECTIONS_PER_CONDENSED_MESSAGE << "!" << std::endl; } } + break; - // Also send global work state on the boom/device element if DDI 289 is available - bool setpointWorkState = clients[client].is_any_section_setpoint_on(); - if (clients[client].get_setpoint_work_state() != setpointWorkState) + case SectionControlMethod::PerElementSetpointWorkState: + case SectionControlMethod::PerElementActualWorkState: { - if (clients[client].has_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState)) + // Per-element: write DDI 289 (or settable DDI 141) to each section element + for (std::uint8_t i = 0; i < NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; i++) { - send_set_value(client, - static_cast(isobus::DataDescriptionIndex::SetpointWorkState), - clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), - setpointWorkState ? 1 : 0); - clients[client].set_setpoint_work_state(setpointWorkState); + std::uint8_t sectionIndex = sectionOffset + i; + if (sectionIndex >= clients[client].get_number_of_sections() || sectionIndex >= plan.perElementTargets.size()) + { + break; + } + std::uint16_t elementNumber = plan.perElementTargets[sectionIndex]; + if (elementNumber != isobus::NULL_OBJECT_ID) + { + std::uint8_t state = clients[client].get_section_setpoint_state(sectionIndex); + send_set_value(client, plan.perElementSetpointDDI, elementNumber, (state == SectionState::ON) ? 1 : 0); + } } } - return; // Per-element path complete + break; + + default: + { + log("TC Server") << "No supported method to send section setpoint states! " + << "Device has no linked DDI 290, settable DDI 161, or settable DDI 289/141 on its sections." << std::endl; + return; + } } - else + + // Global work state on the element above the sections, if the implement has one + bool setpointWorkState = clients[client].is_any_section_setpoint_on(); + if (plan.hasSetpointWorkState && (clients[client].get_setpoint_work_state() != setpointWorkState)) { - log("TC Server") << "No supported method to send section setpoint states! " - << "Device has no DDI 290, 161 (settable), or 141 (settable)." << std::endl; + send_set_value(client, static_cast(isobus::DataDescriptionIndex::SetpointWorkState), plan.setpointWorkStateElement, setpointWorkState ? 1 : 0); + clients[client].set_setpoint_work_state(setpointWorkState); } } @@ -1113,21 +1085,3 @@ void MyTCServer::send_section_control_state(std::shared_ptr lock(clientsMutex); send_set_value(client, static_cast(isobus::DataDescriptionIndex::SectionControlState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SectionControlState), enabled ? 1 : 0); } - -bool MyTCServer::is_ddi_settable(std::shared_ptr client, std::uint16_t ddi) -{ - std::lock_guard lock(clientsMutex); - for (std::uint32_t i = 0; i < clients[client].get_pool().size(); i++) - { - auto object = clients[client].get_pool().get_object_by_index(i); - if (object && object->get_object_type() == isobus::task_controller_object::ObjectTypes::DeviceProcessData) - { - auto processDataObject = std::dynamic_pointer_cast(object); - if (processDataObject->get_ddi() == ddi) - { - return processDataObject->has_property(isobus::task_controller_object::DeviceProcessDataObject::PropertiesBit::Settable); - } - } - } - return false; -} diff --git a/tools/test_section_control_method.cpp b/tools/test_section_control_method.cpp new file mode 100644 index 0000000..985fa82 --- /dev/null +++ b/tools/test_section_control_method.cpp @@ -0,0 +1,203 @@ +/** + * @brief Checks the section control method the TC picks for every DDOP in tools/testdata/implements. + * + * The expected method is the section_control column of manifest.csv. A method only counts when it + * also leaves something to send, and the MIDI 3 pools are checked down to the target elements. + * + * Usage: test_section_control + * Returns 0 when every check passes, 1 otherwise. + */ + +#include "section_control_method.hpp" + +#include "isobus/isobus/isobus_task_controller_server.hpp" + +#include +#include +#include +#include +#include +#include + +static int failures = 0; + +static void check(bool condition, const std::string &label) +{ + if (!condition) + { + std::fprintf(stderr, " FAIL: %s\n", label.c_str()); + ++failures; + } +} + +// One CSV record per line; fields may be quoted and contain commas. +static std::vector split_csv_line(const std::string &line) +{ + std::vector fields(1); + bool quoted = false; + for (std::size_t i = 0; i < line.size(); i++) + { + char c = line[i]; + if (quoted && c == '"' && i + 1 < line.size() && line[i + 1] == '"') + { + fields.back() += '"'; + i++; + } + else if (c == '"') + { + quoted = !quoted; + } + else if (c == ',' && !quoted) + { + fields.emplace_back(); + } + else if (c != '\r') + { + fields.back() += c; + } + } + return fields; +} + +static bool load_pool(const std::string &path, isobus::DeviceDescriptorObjectPool &pool) +{ + std::ifstream file(path, std::ios::binary); + std::vector bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + // Same compatibility level as MyTCServer::activate_object_pool() + pool.set_task_controller_compatibility_level(static_cast(isobus::TaskControllerServer::TaskControllerVersion::SecondEditionDraft)); + return !bytes.empty() && pool.deserialize_binary_object_pool(bytes.data(), static_cast(bytes.size())); +} + +static std::string describe(const SectionControlPlan &plan) +{ + std::ostringstream text; + for (const auto &target : plan.condensedTargets) + { + text << " " << target.first << "->" << target.second; + } + if (plan.uses_per_element_control()) + { + text << " " << plan.perElementSetpointDDI << "->"; + for (std::size_t i = 0; i < plan.perElementTargets.size(); i++) + { + text << (i ? "," : "") << plan.perElementTargets[i]; + } + } + if (plan.hasSetpointWorkState) + { + text << " 289(global)->" << plan.setpointWorkStateElement; + } + return text.str(); +} + +// True when the plan leaves the TC something to write for the method it picked +static bool has_targets(const SectionControlPlan &plan) +{ + switch (plan.method) + { + case SectionControlMethod::CondensedSetpoint: + case SectionControlMethod::SettableCondensedActual: + return !plan.condensedTargets.empty(); + case SectionControlMethod::PerElementSetpointWorkState: + case SectionControlMethod::PerElementActualWorkState: + for (auto element : plan.perElementTargets) + { + if (element != isobus::NULL_OBJECT_ID) + return true; + } + return false; + default: + return true; + } +} + +static void check_midi3_targets(const std::string &file, const SectionControlPlan &plan) +{ + const std::string base = "sprayer/ME_SPRITZE_SPR_MIDI_3_11sec"; + const std::vector sections = { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; + const std::map ddi290OnBoom = { { 290, 2 } }; + const std::map ddi161OnBoom = { { 161, 2 } }; + + if (file == base + ".ddop") + { + check(plan.condensedTargets == ddi290OnBoom, file + ": DDI 290 on element 2"); + } + else if (file == base + "_oldSC_CWS_synthetic.ddop") + { + check(plan.condensedTargets == ddi161OnBoom, file + ": settable DDI 161 on element 2"); + } + else if (file == base + "_newSC_perElement_synthetic.ddop") + { + check(plan.perElementSetpointDDI == 289 && plan.perElementTargets == sections, file + ": DDI 289 on elements 10..20"); + } + else if (file == base + "_oldSC_perElement_synthetic.ddop") + { + check(plan.perElementSetpointDDI == 141 && plan.perElementTargets == sections, file + ": DDI 141 on elements 10..20"); + } + else + { + return; + } + // DDI 289 is unlinked (condensed modes) or only on the sections (per-element modes): never a global target + check(!plan.hasSetpointWorkState, file + ": no global DDI 289"); +} + +int main(int argc, char **argv) +{ + if (argc < 2) + { + std::fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + const std::string root = argv[1]; + std::ifstream manifest(root + "/manifest.csv"); + check(manifest.is_open(), "open " + root + "/manifest.csv"); + + std::string line; + std::getline(manifest, line); + auto header = split_csv_line(line); + std::size_t fileColumn = header.size(), methodColumn = header.size(); + for (std::size_t i = 0; i < header.size(); i++) + { + if (header[i] == "file") + fileColumn = i; + if (header[i] == "section_control") + methodColumn = i; + } + check(fileColumn < header.size() && methodColumn < header.size(), "manifest has file and section_control columns"); + + int pools = 0; + while (std::getline(manifest, line) && fileColumn < header.size() && methodColumn < header.size()) + { + auto fields = split_csv_line(line); + if (fields.size() < header.size()) + continue; + const auto &file = fields[fileColumn]; + const auto &expected = fields[methodColumn]; + pools++; + + isobus::DeviceDescriptorObjectPool pool; + if (!load_pool(root + "/" + file, pool)) + { + check(false, file + ": failed to load"); + continue; + } + auto sections = get_section_element_numbers(isobus::DeviceDescriptorObjectPoolHelper::get_implement_geometry(pool)); + auto plan = resolve_section_control(pool, sections); + + bool ok = (expected == to_string(plan.method)) && has_targets(plan); + std::printf("%-4s %-70s expected %-6s got %-6s%s\n", ok ? "ok" : "FAIL", file.c_str(), expected.c_str(), to_string(plan.method), describe(plan).c_str()); + check(expected == to_string(plan.method), file + ": expected " + expected + ", got " + to_string(plan.method)); + check(has_targets(plan), file + ": " + to_string(plan.method) + " picked but nothing to send"); + check_midi3_targets(file, plan); + } + check(pools > 0, "manifest lists at least one pool"); + + if (failures != 0) + { + std::fprintf(stderr, "%d check(s) failed\n", failures); + return 1; + } + std::printf("All %d pools resolve to their expected section control method\n", pools); + return 0; +}