Merge pull request 'R1.05 findings from a user project (tested only)' (#35) from docs/r105-dtsx1-findings into main

Reviewed-on: #35
This commit is contained in:
Clio Liu 2026-10-12 14:29:23 +11:00
commit 42240a86d0
15 changed files with 293 additions and 3 deletions

View file

@ -139,3 +139,8 @@ No alarm line from `ALDPRT` appeared in `umhlog.log` (ISS-035).
test item of a non-shared R1.05 system (expected results as in the table above).
- Read `ALARM_CHRONO` only with a filter on item name **and** a time range: big history reads
load the alarm storage process that all clients share (ISS-136).
- 2026-10-12 (user project on the R1.05 test server, 2026-10-09 to 10-12): LOW/HIGH limit alarms on Real items loaded with the item (`LOW_LOW_LIMIT,LOW_LIMIT,
HIGH_LIMIT,HIGH_HIGH_LIMIT` = `-100,-5,5,100`, `ALARMING` 1, `ACKN_TYPE` `"MANUAL"`): a process class
writing the value raised status 8 (LOW) / 10 (HIGH) and one `ALARM_CURRENT` row each (priority 2
from `STATUS_DF`), cleared on return to normal.

View file

@ -128,3 +128,11 @@ and `AUTH_GROUP_DF` keep the same relation, but the DSS processes are arranged d
(ISS-117), so treat ISS-073 as still valid until it is re-tested on a separate system (ISS-152).
The same applies to the password behaviour of ISS-072 (`USER_DF.PASSWORD` unchanged): keep
giving `PASSWORD ""` or setting the password in the Engineering Module.
## R1.05 (2026-10-12, user project on the R1.05 test server, 2026-10-09 to 10-12)
A user with `PASSWORD ""`, factory group `OPERATOR`, `LAYOUT` and `INITIAL_DISPLAY` loaded with 0
errors and logged on to the web client without a password, landing on the display in the layout
(no `?l=` or `?d=` needed). `OPERATOR` includes `ITEM_MODIFY_VALUE` and `ITEM_MODIFY_STRING_VALUE`,
so display buttons that write items work for it. Removing the user (`dssqld -r`) after the user
had exited the web session gave 0 errors.

View file

@ -41,7 +41,10 @@ for a reset report (empty = none; this prompt is not in the manual):
```
printf '2\n8\n<CLASS>\n0\n\n-1\n-1\n' | timeout 30 opcdbg 0
```
(Bash. Interactively, type the same answers.) Keep the time-out: `opcdbg` repeats its last
(Bash. Interactively, type the same answers.) In Windows `cmd`, put the answers in a file
(`2`, `8`, `<CLASS>`, `0`, empty line, `-1`, `-1`, one per line, CRLF) and run
`opcdbg 0 < reset_class.txt` (verified on R1.05 2026-10-12). A `dssqld -m` with `NAME,TRIGGER_1,COMPILE,METHOD`
also changes the class's trigger group (verified on R1.05). Keep the time-out: `opcdbg` repeats its last
prompt forever when the input runs out.
Afterwards `RESET_REQUIRED` = 0 and the object used the new code at once (verified).
@ -63,8 +66,17 @@ and a reset report; click **Reset** (verified by the user 2026-10-01: 1 of 1 obj
## 3. Changing prompts or signals
Only a change in the method bodies was practiced. When the class **prolog** changes (new
Only a change in the method bodies was practiced on R1.03. When the class **prolog** changes (new
signals, prompts, attributes, created items), check `EXTENDED_RESET_REQUIRED` in `CLASS_DF`
after compiling; what an extended reset does was not tested. The safe way is to recreate the
objects (remove, load) — note that removing an object deletes the items it created, with their
history links.
**R1.05, Java (verified 2026-10-12, user project on the R1.05 test server, 2026-10-09 to 10-12): new signals are refused while objects exist.** `dssqld -m` of
the class with extra `define…Signal` fields gives only `DSI-E-OPCCOMERR, Compilation failed with 1
error(s)` / `DSSP-E-UPDREC` in the `.qlo`; the reason is in the newest `%TLS_ROOT_PATH%\tls\lst\opc_t*.lst`:
`**** ERROR: Attribute or signal definitions cannot be changed while objects are derived from the
class`. The old class keeps running. Procedure that worked: remove the object (`@OBJECT_DF`, `-r`),
`dssqld -m` the class, wait for the compile, insert the object again. **The object's attributes are
lost** with it; the tested class rebuilt its data on the first run of the new object from items it
also writes (its log message confirmed the attributes were empty).

View file

@ -104,6 +104,30 @@ refuse bad answers with `cancel("text")` in `create()`.
good or 2 = bad). Use one class with its own name per method object. Procedure: skill
`data-access`, `opcua-client.md` §7, and the L45 summary (§10.6, §11.4). Not practiced.
## 4a. Large classes on R1.05 (verified 2026-10-12, user project on the R1.05 test server, 2026-10-09 to 10-12)
A Java class with about 1,600 signals ran on R1.05 every second without problems:
- **Signal arrays**: `Signal[] t = defineDoubleSignalArray(400);` (also `defineIntegerSignalArray`,
`defineStringSignalArray`), attached in a loop in `create()`:
`for (int k = 0; k < 400; k++) attachItemToSignal(t[k], "<Section>.TEMP." + name(k));`.
Signals without a trigger, plus `TRIGGER_1` = a trigger group and `triggerGroupEvent(TriggerGroup tg)`,
give one run per second for all inputs.
- **Write only on change**: the class compared with `getValueAsDouble()` / `getValueAsString()`
before `setValue()` (and also wrote when `getStatusNumber() == Signal.Status.NOT_INIT`).
- **String signals**: `setValue(String)` on a String item works.
- **Attribute arrays** (`defineDoubleAttributeArray(Attribute.Scope.OBJECT, n)`) keep their values
over a class reset; Java fields do not (initialise them in the first trigger run, not in
`create()`). Attributes are **lost when the object is deleted** (verified: after remove and
insert the new object found them empty and logged that it rebuilt the data from items; see
`change-reset-class.md` §3).
- **Compile check before loading** (no CI Server change):
`"%TLS_ROOT_PATH%\tls\jre\bin\javac.exe" -Xlint:all -cp "%TLS_ROOT_PATH%\tls\exe\jopcsrv.jar" -d cls <Class>.java`.
The exact API: `javap -cp %TLS_ROOT_PATH%\tls\exe\jopcsrv.jar fasttools.processfast.ProcessClass`
(also `Signal`, `Attribute$Scope`, `Signal$Trigger`, `Signal$Status` constants, e.g. `NOT_INIT` 13).
- **Keep quick load lines short**: put long generated arrays (tables in the class source) on several
lines; the class loaded with lines up to 233 characters.
## 5. Pitfalls (manual)
- Definitions (`define…()`) only at class level, without `public`/`private`.

View file

@ -122,3 +122,18 @@ files deleted), reloaded from the sample files and re-tested: all 0 errors. With
`eqpciengslv_start.cmd`/`_stop.cmd` copied to `tls\com` and CI Server restarted, the mirror and
`EQPCIENG` started under the CI Service, `COMM_OK` = 1, and the saved setpoint (2468) was
re-sent to the freshly zeroed slave memory on connect (output method *On change and connect*).
## R1.05: a 400-register device (2026-10-12, user project on the R1.05 test server, 2026-10-09 to 10-12)
- Own line and equipment manager (`EQUIPMENT_MAN` e.g. `EQP<NAME>`) next to another project's
line: loaded, removed and loaded again with 0 errors; removing the line stopped only its manager.
- 400 holding registers as points `RO:1`–`RO:400`, `CONV_TYPE` `Linear` with `HAS_SIGN` 1,
`ELEC_LOW,ELEC_HIGH` = `-32768,32767` and `PHYS_LOW,PHYS_HIGH` = `-327.68,327.67` (raw ÷ 100):
all 400 items read the expected signed values (test pattern from a simulator, quality 0,
status 9).
- Communication loss: status item 0 (status 4), inputs keep their last value with the `off` option;
values and status item recover on the next scan after the device answers again.
- Test without the device: `tools/ModbusSim.java` (Java, runs with CI Server's own JDK; holding
registers with read and write, scenarios through a mode file). A PowerShell script that opens a
TCP listener was blocked by Windows antivirus (AMSI) as malicious content on the engineering PC
(Windows 10).

View file

@ -71,6 +71,21 @@ Module, the Operator Interface and the HTML display, if these rules are kept (sa
| Server-side command | `<action type="actionExecuteCommand">` with `<command>` and `<onServer>true</onServer>` runs on the CI Server node as the service account `CIX_PROCESS`, also from the web; a user in the factory group `OPERATOR` may use it (tested 2026-10-06, sample live-data *Start*) |
| Alarm overview | `autoFit` true in a tab that is hidden while loading squeezes all columns to ~0 px on the web: set `autoFit` false and give `column<Name>Width`; `column…Visible` for `AlarmAttribute` is an array property (a plain value fails the HTML export with `ClassCastException`); the empty part of the list stays **white** on both clients (no property; web: `jquery.dataTables.css` forces it); web rows are 20 px at a 14 px font (2026-10-02/06) |
## R1.05 web client: findings from a generated display (verified 2026-10-12, user project on the R1.05 test server, 2026-10-09 to 10-12)
A generated display with about 1,900 components (6 MB XML, 1 MB HTML) on the R1.05 web client:
| Topic | Finding |
|---|---|
| Text bound to a String item | `<text>` with `text.value` ← `ItemValue` works. `keepOriginalSize` false stretches it to the box (as on R1.03, unreadable for changing text); `true` draws it centred at natural size, good for centred values. For **left-aligned** changing text use a `<textField>`: `textField.value` ← `ItemValue`, `<editable>false`, `<borderType type="border" value="BorderEmpty"/>`, justification `TextJustificationLeft` (or `Right`). `<opaque>false` is ignored: set `backgroundColor` to the colour behind it, or connect `textField.backgroundColor` through a threshold with output type `ColorSet` when the background changes. |
| Fixed text that must align | A `<text>` is centred on its box at its natural width; on one viewer's client long lines started left of their box. A `<textField>` with a fixed `<value>` (no connection) is left-aligned (checked in the test browser). |
| Hidden at start | `<visible>false</visible>` plus a `*.visible` connection: rectangles, texts and numbers appear when the condition becomes true; **ellipses, textFields and numberFields stay hidden**. Leave those visible at start. Without a start value every conditional component is drawn until the first values arrive. |
| Two conditions | Two connections to the same `*.visible` property do not combine (numbers stayed visible after one of their two conditions became false). Encode the condition in one item (e.g. a state item the calculation writes). |
| Display actions on open | Actions placed directly in `coreObjectDefinition` (before the first layer, as in the factory `AOG_ItemFaceplate`) with `<event type="event" event="On entry"/>` run when the display opens **and** on *Reload*; e.g. `actionSetItemAttribute` to reset a selection item. |
| Blinking | `rectangle.blinking` ← item through a threshold (output Boolean) plus a `blinkingPaint` blinks (the component toggles visibility at the display's `blinkDelay`, 500 ms). |
| Also verified | `numberField` with `asDateTime` on `ItemValue` (epoch seconds); `text.fillPaint`, `rectangle.strokePaint`, `ellipse.strokePaint` through thresholds; `actionSetItemAttribute` with event `Clicked b1` on a rectangle and on a number (a number drawn on a clickable shape needs its own action, it catches the click). |
| Size and first load | Once the first values took about a minute on a display this size; later loads took 3–10 s. |
## R1.05: component changes since R1.03 (from docs, IM 36K10C50-01EN 5th ed.)
| Component / sheet | Change (version) |

View file

@ -193,12 +193,14 @@ browser and in the web client code. "n/a" = the browser window is the main layou
| `toolbar` (check) | hides/shows **all** toolbars; `toolbar false` behaves as a toggle too | no effect |
| `statusline` (check) | hides/shows (a piece of the date field stays painted) | no effect |
| `print …` | preview / setup dialogs open | disabled |
| `login` / `logout` / `exit` | — / — / closes | login disabled; logout, exit work |
| `login` / `logout` / `exit` | — / — / closes | login disabled; **`logout` disabled on R1.05** (greyed; see note); `exit` closes the window and ends the session |
| `shell client <cmd>` | runs on the client (Notepad opened) | sent to the server; `shell client ACTION <layout command>` runs a layout command |
| `lookfeel <class>` | — | no effect |
| `playback record` (check) / `view` | — | not supported (view disabled) |
| `help` | **error** *Unrecognized keyword* | — |
**`logout` is a secondary logout (R1.05, verified 2026-10-12).** The factory layout labels it *Secondary logout* (pair of `login`). The R1.05 web client always disables it (`LayoutWindow.js`: `setEnabled(false)` for `logout`; the command calls `secondaryLogout()`), so a toolbar or menu item with `logout` is greyed out. To let a user leave on the web, use **`exit`**: it closes the layout window and, for the last window, ends the session; the browser then closes the tab (reopening the URL shows the login page). Label the item *Exit* rather than *Log out*. Tested on the web client; the desktop Operator Interface was not tried. The sample layout `CIENG_Operator` still has a *Log out* (`logout`) menu item.
"—" = not checked.
### 6.5 Layouts opened from a display, translations, debug key

View file

@ -37,3 +37,8 @@ After a write, check the status: 12 means the text was cut.
The representation (type) and length cannot be changed on an existing item: remove and
load it again.
**Mixed item files (R1.05, 2026-10-12).** `STRING_LENGTH` is checked on every record of an `@FIELDS`
block: `0` (or empty) on a Real/Integer item is refused with `DSS-E-VAL_RANGE ... (Field:
STRING_LENGTH)`. Put String items in their own `@FIELDS` block with `STRING_LENGTH`, and the other
items in a block without it.

View file

@ -40,6 +40,7 @@ Rules for every task:
| Task | File | Status |
|---|---|---|
| Package a project for another CI Server (quick load, display, layout, user, README, pre-check) and test the install | `tasks/package-project.md` | verified on R1.05 with a user project 2026-10-12 (not on CI-Eng-Sample) |
| Export dataset contents (whole or filtered) / dump the data dictionary | `tasks/export-dataset.md` | verified (R1.03; re-tested on R1.05 2026-10-08) |
| Load or remove records with quick load files (`.qli`), e.g. sections and items | `tasks/load-remove-records.md` | verified (R1.03; load/remove used on R1.05 in the CI-Eng-Sample test 2026-10-08) |
| Read / write item values and read limits | `tasks/read-write-item.md` | verified (R1.03; `-g`/`-p` used on R1.05 in the CI-Eng-Sample test, read re-checked 2026-10-08) |

View file

@ -85,6 +85,11 @@ dssqld -e <output>.qli <filter>.qli [-c] -l <listing>.lst
REST API keys out on purpose. `-d API_KEYS_DF` still exports it; treat that file as a secret,
or create new keys on the target system instead (skill `data-access`).
- **Range on `NAME`** (R1.05, 2026-10-12): `'NAME > "<Section>." and NAME < "<Next>"'` (`<Next>` = the section
name with its last character raised by one, e.g. `TANK1.` → `TANK2`) selects all items below a
section on `ITEM_DF` (tested with 1,751 items); a `NAME = "A*"` wildcard does not work.
The same works on `ALARM_CURRENT` with `ITEM_NAME`, to list the current alarms of one section.
## Clean up: `dss_*.tmp` files (R1.05)
Exporting `OBJECT_DF` leaves 0-byte `dss_<n>_<id>.tmp` files in `%TLS_LST_PATH%` (`tls\lst`),

View file

@ -0,0 +1,38 @@
---
task: Package a project for another CI Server (quick load files, display, layout, user) and check the install
version: R1.05
source: IM 36K10J10-01EN §5.6 (moving engineering data); IM 36K10L50-01EN §3; practiced with a user project on the R1.05 test server 2026-10-12 (remove and reload from the package, twice)
status: verified on R1.05 with a user project (not on CI-Eng-Sample)
---
# Package a project for another CI Server
Goal: a folder (zip) a junior engineer can install from, step by step, on another server.
## 1. What goes in
| Part | Notes |
|---|---|
| Quick load files `01_…` to `nn_…` in load order + `load.cmd` | sections, status items, line, station/points, items, trigger, class, object, users. **No node names** (leave `EQUIPMENT_NODE`, `FRONT_END_NODE` out) and no `NSID` / `ID_NUMBER`: the target assigns them. Insert (`-i`) only. |
| Removal files in reverse order + `remove.cmd` | so the install can be undone |
| Displays + a deploy script | copy to `DEPLOY\displays`, `jwuiutl -d -v -i -l … <display>` (must say *No problems found*), copy again (validation re-saves), `jwuiutl -e HTML -i -f DEPLOY <display>`. Displays without `<itemId>` bind by name, so new item numbers on the target do not matter. |
| Layout block + script | only the project's layout(s) between `<!-- <P>-BEGIN` / `<!-- <P>-END -->`, inserted before the first `<translations>`; the script makes a timestamped backup and checks the XML (sample: `examples/CI-Eng-Sample/layouts/layout.ps1`). |
| README | numbered steps with the exact command, the expected output and what to do if it differs |
| Pre-check script (read-only) | CI Server version (`%TLS_ALLUSERS_PATH%\lst\tls_versions.lst`), `dssqld` answers, longest file path ≤ 79 (ISS-020), `tls\jre\bin\javac.exe` present (Java classes compile on load), device reachable (TCP connect), each name free (filtered export per dataset, e.g. `'NAME = "<LINE>"'` on `MODBUS_LINE_DF`, `'EQUIPMENT_MAN = "<EQPNAME>"'`), layout name not used outside the project block |
| Post-install check | `itmval -g` of the station status item, one value to compare with the device's own software, a value the calculation updates (read twice), learn/initial states |
## 2. Script rules (found by testing the README in one Command Prompt)
- **`pushd "%~dp0"` … `popd`**, not `cd /d "%~dp0"`: the engineer runs the scripts one after the other
in the same prompt; a script that leaves the prompt in its own folder breaks the next command.
- `load.cmd`: delete the old `0*.qlo` first and end with `findstr /c:"_DF " 0*.qlo` (one result
line per data set; the last column is *Errors*). Otherwise old results (e.g. of an undo) are listed too.
- Wait after the line (`powershell -NoProfile -Command "Start-Sleep -Seconds 15"`, the equipment
manager starts) and after a class (about 20 s, compile) before the next file.
## 3. Test the package
On a test server: run the README's undo, then every step exactly as written, in one Command Prompt,
from a fresh copy of the zip; point the device address at a simulator (`tools/ModbusSim.java`) if
the device is not reachable. Compare the server against the package afterwards (filtered `ITEM_DF`
export by name range, class source from a `CLASS_DF` export, display file hash).

View file

@ -179,3 +179,10 @@ development system (see "Test system" in `progress.md`); they do not exist on ot
| ISS-179 | 2026-10-09 | PYTOOLS (login method) | pyTOOLS supports only *Manual CI Server* (J10 §2.9.1); the installed R1.05 *Python Interface* lines are Manual CI Server. Behaviour with another method or with the lines missing after an upgrade (ISS-149) is not documented. | open | From docs plus the installed file. | P40 summary (done); `.claude/skills/authorization/tasks/login-methods.md` (done) |
| ISS-180 | 2026-10-09 | PYTOOLS (BUS name routing) | `dss.sup` routes only `PYDSS*` clients to DSSSDAPY; a script that sets its own BUS name with `busSetName` probably falls back to the general DSSSDA00–03 routing. Not in P40. | open | Untested (no Python). | `.claude/skills/pytools/tasks/event-notification.md`, P40 summary (done) |
| ISS-181 | 2026-10-09 | PYTOOLS (event queue) | IM 36K10P40 §3.10 says the event queue size is set in the DSS setup file without naming the keyword: it is `EVENT_Q_SLOTS` in `dss.sup` (default 100, no explicit line). | open | From docs and the `dss.sup` help. | P40 summary, `.claude/skills/pytools/tasks/event-notification.md` (done) |
| ISS-182 | 2026-10-12 | GRAPHICS (layouts, web) | Layout command `logout` is a *secondary* logout and is always disabled on the R1.05 web client (`LayoutWindow.js`); the R1.03 table said it works on the web. | closed | Use `exit` (closes the window; the last window ends the session, the browser closes the tab). Verified on the R1.05 web client 2026-10-12. The sample layout `CIENG_Operator` still has a `logout` item: change it after re-testing on the sample (ISS-188). | `graphics/tasks/layouts.md` §6.4, `examples/CI-Eng-Sample/layouts/CIENG_Operator.xml` |
| ISS-183 | 2026-10-12 | GRAPHICS (web, visibility) | Ellipses, textFields and numberFields that start with `<visible>false</visible>` stay hidden on the R1.05 web client when their visibility connection becomes true (rectangles, texts, numbers work). | closed | Leave those components visible at start (workaround). Verified 2026-10-12. | `graphics/tasks/create-display.md` §R1.05 web client |
| ISS-184 | 2026-10-12 | GRAPHICS (web, String items) | A `<text>` bound to a String item is stretched to its box with `keepOriginalSize` false and centred with true; `textField` `opaque false` is ignored (white background). | closed | Left-aligned text: borderless read-only `textField` with the background colour set or connected. Verified 2026-10-12. | `graphics/tasks/create-display.md` |
| ISS-185 | 2026-10-12 | PROCESS (Java, signals) | Changing the signal or attribute definitions of a Java class while an object exists fails with only `DSI-E-OPCCOMERR` in the `.qlo`; the reason is in `tls\lst\opc_t*.lst`. Recreating the object loses its attributes. | closed | Remove object, modify class, insert object; rebuild attribute data from items. Verified on R1.05 2026-10-12. | `class-object-engineering/tasks/change-reset-class.md` §3, `java-class.md` §4a |
| ISS-186 | 2026-10-12 | TOOLS (Windows security) | A PowerShell script that opens a TCP listener (Modbus test server) is blocked by Windows antivirus/AMSI: *This script contains malicious content and has been blocked* (engineering PC, Windows 10). | closed | Use the Java test server `tools/ModbusSim.java` (CI Server JDK). Seen 2026-10-12. | `tools/README.md`, `equipment/tasks/modbus-station.md` |
| ISS-187 | 2026-10-12 | ITEM (quick load) | `STRING_LENGTH` 0 on a Real item in a mixed `@FIELDS` block is refused (`DSS-E-VAL_RANGE`). | closed | Separate `@FIELDS` blocks for String and other items. Verified on R1.05 2026-10-12. | `item-engineering/tasks/string-items.md` |
| ISS-188 | 2026-10-12 | SAMPLE (R1.05) | The R1.05 findings of 2026-10-12 were verified with a user project, not with CI-Eng-Sample (displays, layouts and users of the sample are not loaded on the shared R1.05 server). | open | Re-test on CI-Eng-Sample when the full sample can be loaded on an R1.05 system, including replacing the sample layout `logout` item with `exit`. | `knowledge/progress.md` R1.05 table |

View file

@ -66,6 +66,16 @@ screenshot.
| `reporting` | none listed (E10 one sentence removed, ISS-166) | re-checked (read-only) | 2026-10-09: report definitions, setup and `.rpt` files identical; hooks (ISS-121); 28 system reports, 6 factory destinations; interactive RQL re-run, new `ITEM_VAL` fields (ISS-126, 167); `API_KEYS_DF` not queried (ISS-168). Generate, print, item trigger, `rptstp`, SPOOL re-tested on R1.05 (V12, 2026-10-09). **Not tested:** Excel Add-in (no Office), report component, hardcopy printers. |
| `examples/CI-Eng-Sample` | — | re-tested | Reduced load (no history group, user or layouts) loaded, tested and removed on the shared R1.05 server 2026-10-08 (V4); scripts in `shared\`. Full load not tested on R1.05. |
### Findings from a user project (2026-10-12)
A Modbus TCP device with 400 registers, a large Java process class, a generated display with a
pop-up, a minimal layout, a password-less user and an install package were built and tested on the
R1.05 test server (own objects only). Added to the skills: `layouts.md` (`logout` vs `exit`),
`create-display.md` (R1.05 web client table), `java-class.md` §4a, `change-reset-class.md` (new
signals, `opcdbg` in cmd), `string-items.md`, `users-groups.md`, `modbus-station.md`,
`export-dataset.md` (name range), `item-alarming.md`, new task `system-management/tasks/package-project.md`,
`tools/ModbusSim.java`. Issues ISS-182 to ISS-188. Not re-tested on CI-Eng-Sample (ISS-188).
## Manuals, in working order
| Phase | Manual | Title | Status |

142
tools/ModbusSim.java Normal file
View file

@ -0,0 +1,142 @@
/*
* ModbusSim: a small Modbus TCP server (slave) for testing a CI Server Modbus station without the
* real device. Java 11 single-file program: runs with the JDK that ships with CI Server, nothing
* to install (a PowerShell TCP listener script is blocked by Windows antivirus/AMSI as
* "malicious content", see knowledge/issues.md).
*
* "%TLS_ROOT_PATH%\tls\jre\bin\java.exe" ModbusSim.java [port] [registers] [folder]
* defaults: 15020, 1000, the current folder. Listens on 127.0.0.1 only.
*
* One table of holding registers (FC03 read, FC06 / FC16 write); FC04 reads the same table as input
* registers (like CI Server's own slave mirror). FC07 answers 0, FC08 echoes, others exception 01.
* Register 1 = protocol address 0 (CI Server point RO:1 / RI:1 = 40001 / 30001).
*
* <folder>/mode.txt is read every 2 s and can be changed while it runs:
* pattern raw(k) = ((k * 157) mod 30000) - 5000 (fixed, signed test values; the default)
* zero all registers 0
* csv values from <folder>/values.csv: one integer per line, line k = register k
* hold keep the current values (use with writes from CI Server)
* stop close and exit
* Messages go to the console and <folder>/sim.log.
* To keep it running after an SSH session ends, start it detached, e.g. with PowerShell
* Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='...'}
* Never run it on a production system or on a port a real device uses.
*/
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ModbusSim {
static int n;
static int[] reg;
static volatile String mode = "";
static Path folder;
public static void main(String[] args) throws Exception {
int port = args.length > 0 ? Integer.parseInt(args[0]) : 15020;
n = args.length > 1 ? Integer.parseInt(args[1]) : 1000;
folder = Paths.get(args.length > 2 ? args[2] : ".").toAbsolutePath();
reg = new int[n + 1];
Path modeFile = folder.resolve("mode.txt");
if (!Files.exists(modeFile)) Files.writeString(modeFile, "pattern");
ServerSocket server = new ServerSocket(port, 10, InetAddress.getLoopbackAddress());
log("listening on 127.0.0.1:" + port + ", " + n + " registers, pid " + ProcessHandle.current().pid());
Thread control = new Thread(() -> {
while (true) {
try {
String m = Files.readAllLines(modeFile).stream().findFirst().orElse("").trim().toLowerCase();
if (m.equals("stop")) { log("stop requested"); System.exit(0); }
if (!m.equals(mode)) { mode = m; log("mode " + m); apply(m); }
Thread.sleep(2000);
} catch (Exception e) { log("control: " + e); }
}
});
control.setDaemon(true);
control.start();
while (true) {
Socket c = server.accept();
new Thread(() -> serve(c)).start();
}
}
static synchronized void apply(String m) throws IOException {
if (m.equals("pattern")) for (int k = 1; k <= n; k++) reg[k] = ((k * 157) % 30000) - 5000;
else if (m.equals("zero")) java.util.Arrays.fill(reg, 0);
else if (m.equals("csv")) {
java.util.List<String> lines = Files.readAllLines(folder.resolve("values.csv"));
for (int k = 1; k <= n && k <= lines.size(); k++) reg[k] = Integer.parseInt(lines.get(k - 1).trim());
}
}
static void serve(Socket c) {
String who = String.valueOf(c.getRemoteSocketAddress());
log("connect " + who);
try (c; DataInputStream in = new DataInputStream(new BufferedInputStream(c.getInputStream()));
OutputStream out = c.getOutputStream()) {
while (true) {
byte[] h = new byte[7];
in.readFully(h);
int len = ((h[4] & 0xFF) << 8) | (h[5] & 0xFF);
if (len < 2 || len > 256) throw new IOException("bad length " + len);
byte[] pdu = new byte[len - 1];
in.readFully(pdu);
byte[] r = handle(pdu);
byte[] resp = new byte[7 + r.length];
System.arraycopy(h, 0, resp, 0, 4);
resp[4] = (byte) ((r.length + 1) >> 8); resp[5] = (byte) (r.length + 1); resp[6] = h[6];
System.arraycopy(r, 0, resp, 7, r.length);
out.write(resp);
out.flush();
}
} catch (EOFException e) {
log("disconnect " + who);
} catch (Exception e) {
log("disconnect " + who + " (" + e + ")");
}
}
static int u16(byte[] b, int i) { return ((b[i] & 0xFF) << 8) | (b[i + 1] & 0xFF); }
static synchronized byte[] handle(byte[] pdu) {
int fc = pdu[0] & 0xFF;
if (fc == 3 || fc == 4) {
int start = u16(pdu, 1), qty = u16(pdu, 3);
if (qty < 1 || qty > 125) return new byte[]{(byte) (fc | 0x80), 3};
if (start + qty > n) return new byte[]{(byte) (fc | 0x80), 2};
byte[] r = new byte[2 + 2 * qty];
r[0] = (byte) fc; r[1] = (byte) (2 * qty);
for (int i = 0; i < qty; i++) {
int v = reg[start + i + 1] & 0xFFFF;
r[2 + 2 * i] = (byte) (v >> 8); r[3 + 2 * i] = (byte) v;
}
return r;
}
if (fc == 6) {
int a = u16(pdu, 1);
if (a >= n) return new byte[]{(byte) 0x86, 2};
reg[a + 1] = (short) u16(pdu, 3);
return pdu.clone();
}
if (fc == 16) {
int start = u16(pdu, 1), qty = u16(pdu, 3);
if (qty < 1 || qty > 123 || start + qty > n) return new byte[]{(byte) 0x90, 2};
for (int i = 0; i < qty; i++) reg[start + i + 1] = (short) u16(pdu, 6 + 2 * i);
return new byte[]{16, pdu[1], pdu[2], pdu[3], pdu[4]};
}
if (fc == 7) return new byte[]{7, 0};
if (fc == 8) return pdu.clone();
return new byte[]{(byte) (fc | 0x80), 1};
}
static synchronized void log(String s) {
String line = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " " + s;
System.out.println(line);
try {
Files.writeString(folder.resolve("sim.log"), line + System.lineSeparator(),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException ignored) { }
}
}

View file

@ -6,4 +6,5 @@ location; take it from an argument or environment variable, with
| Script | Purpose |
|---|---|
| `ModbusSim.java` | Modbus TCP test server (holding registers, read and write, scenarios through `mode.txt`) for testing a Modbus station without the device; runs with CI Server's JDK (`java ModbusSim.java [port] [registers] [folder]`). Verified on R1.05 2026-10-12 |
| `setup_check.ps1` | Read-only check of a machine against the CI Server R1.03 setup and security requirements (skill `installation-security`) |