Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
130585a
fix(pou): show return type selector for graphical function POUs
emerson-d-lopes Jul 22, 2026
48dd93a
fix(pou): use dedicated id for return type selector, cover SFC
emerson-d-lopes Jul 23, 2026
4563b20
fix: issue #745
Jul 27, 2026
cc63f8b
fix(debugger): draw trends as sample-and-hold and keep the window sli…
JoaoGSP Jul 28, 2026
5fab141
style(debugger): keep only the load-bearing trend comments
JoaoGSP Jul 28, 2026
80959a7
Merge pull request #968 from Autonomy-Logic/bugfix/issue-590-trend-st…
JoaoGSP Jul 28, 2026
1c22d07
Merge branch 'development' into bugfix/gh-696-function-return-type-gr…
emerson-d-lopes Jul 29, 2026
e3b95f4
style(variables-editor): apply prettier formatting
emerson-d-lopes Jul 29, 2026
e3f501b
Merge pull request #954 from emerson-d-lopes/bugfix/gh-696-function-r…
JoaoGSP Jul 29, 2026
7a9ccf3
Merge pull request #970 from Autonomy-Logic/chore/node-137-debug-poll…
dcoutinho1328 Jul 29, 2026
d162fcb
fix(debugger): support forcing TIME values (DOPE-331, #634)
JoaoGSP Jul 30, 2026
319f5fd
Merge pull request #971 from Autonomy-Logic/bugfix/DOPE-331-force-tim…
JoaoGSP Jul 30, 2026
62d5bd1
Merge branch 'development' of github.com:Autonomy-Logic/openplc-edito…
Jul 31, 2026
eb79bbf
Merge pull request #972 from Autonomy-Logic/fix/issue-745
JulioSergioFS Jul 31, 2026
3da8507
fix(graphical): never report a POU as saved when its flow write-back …
JoaoGSP Aug 1, 2026
ebf167c
Merge remote-tracking branch 'origin/development' into bugfix/DOPE-49…
JoaoGSP Aug 1, 2026
3d10a3b
fix(modbus): zero IEC buffers on read failure when the I/O group asks…
Aug 2, 2026
bfa9f16
Merge pull request #975 from Autonomy-Logic/fix/issue-691
JulioSergioFS Aug 3, 2026
c3dac15
feat(debugger): always-on serial debugger decoupled from full Modbus
marconetsf Jul 3, 2026
232da71
feat(vpp): resolve VPP select options dynamically from board data
marconetsf Jul 9, 2026
631c895
feat(compile): generate defines for the serial/network/modbus split
marconetsf Jul 9, 2026
cfd7e94
feat(firmware): keep the debugger serial up on Modbus TCP-only builds
marconetsf Jul 9, 2026
74ab1b2
feat(debugger): fall back to the serial channel when no channel matches
marconetsf Jul 9, 2026
4b3c138
feat(firmware): run Modbus RTU on a secondary serial while the debugg…
marconetsf Jul 9, 2026
86940ef
refactor(firmware): split ModbusSlave into per-entity modules
marconetsf Jul 10, 2026
c582131
docs(firmware): add ARCHITECTURE.md for the Modbus slave modules
marconetsf Jul 10, 2026
231e11d
feat(editor): one device connection over serial or Modbus TCP, with r…
marconetsf Aug 4, 2026
8ac7775
style: prettier + import sort over the connection surface
marconetsf Aug 4, 2026
0aa0c8a
fix(editor): align the debugger's baud with the firmware, and sweep o…
marconetsf Aug 4, 2026
ac16ea7
fix(editor): keep the baud out of a candidate's descriptor, and dial …
marconetsf Aug 4, 2026
474c149
fix(compile): name precompiled objects `<source>.cpp.o` so esp8266 li…
marconetsf Aug 4, 2026
976ed4c
fix(editor): stop the liveness poll from killing a busy debug session…
marconetsf Aug 4, 2026
2596f1e
fix(editor): a firmware that reports no unique id is still a firmware
marconetsf Aug 4, 2026
579cc05
fix(connection): reconnect after upload through the device port
marconetsf Aug 4, 2026
4e11afa
fix(graphical): stop an invalid flow from blocking saves and history …
JoaoGSP Aug 4, 2026
cd54f26
Merge branch 'development' into bugfix/DOPE-495-flow-writeback-silent…
JoaoGSP Aug 4, 2026
1c8ee64
refactor(connection): trace the outcome, not every step towards it
marconetsf Aug 4, 2026
32e324c
chore(strucpp): pin v0.6.2 for located-global sync
thiagoralves Aug 4, 2026
32dd325
fix(runtime): give login its own 15s timeout for slow devices
Aug 4, 2026
450c988
Merge pull request #983 from Autonomy-Logic/chore/strucpp-0.6.2-locat…
thiagoralves Aug 4, 2026
46b2b12
Merge branch 'development' into feat/baremetal-connection
marconetsf Aug 5, 2026
38deef9
Merge branch 'development' into fix/issue-526
JulioSergioFS Aug 5, 2026
239ecfe
Merge pull request #984 from Autonomy-Logic/fix/issue-526
JulioSergioFS Aug 5, 2026
ea624e6
Merge branch 'development' into bugfix/DOPE-495-flow-writeback-silent…
JoaoGSP Aug 5, 2026
d282c2d
Merge pull request #973 from Autonomy-Logic/bugfix/DOPE-495-flow-writ…
JoaoGSP Aug 5, 2026
710be54
feat(datatypes): add canonical single-type ST serializer and .dt text…
JoaoGSP Aug 5, 2026
6db06f1
Merge pull request #987 from Autonomy-Logic/feature/DOPE-530-datatype…
JoaoGSP Aug 5, 2026
a5ac732
feat(datatypes): add datatypes/*.dt file-category plumbing
JoaoGSP Aug 5, 2026
e906ce6
style: format project-service after datatypes plumbing
JoaoGSP Aug 5, 2026
7e502e1
Merge pull request #988 from Autonomy-Logic/feature/DOPE-532-dt-file-…
JoaoGSP Aug 5, 2026
87001fd
fix(runtime): address review findings on the device-connection PR
thiagoralves Aug 5, 2026
1b9f166
fix(datatypes): wire undo/redo snapshots and dirty tracking for data …
JoaoGSP Aug 5, 2026
58bdcda
refactor(debug): one published medium drives the debug poll on both p…
thiagoralves Aug 5, 2026
4de3a21
fix(graphical-editor): type new variables from the block's generic pi…
Aug 5, 2026
c2b14ab
fix(datatypes): flag undo/redo divergence dirty and batch initial-val…
JoaoGSP Aug 6, 2026
2106e53
feat(datatypes): switch datatype persistence to datatypes/*.dt with l…
JoaoGSP Aug 6, 2026
d42ba16
test(datatypes): drop non-null assertions from datatype history tests
JoaoGSP Aug 6, 2026
6528fce
fix(datatypes): guard duplicate .dt paths and unvalidated IPC file lists
JoaoGSP Aug 6, 2026
0430901
fix(datatypes): compare data type names case-insensitively
JoaoGSP Aug 6, 2026
8d070f5
fix(datatypes): address review — capture test, name-source unificatio…
JoaoGSP Aug 6, 2026
2768df1
Merge pull request #989 from Autonomy-Logic/feature/DOPE-534-datatype…
JoaoGSP Aug 6, 2026
03fd66f
Merge branch 'development' into feature/DOPE-533-dt-save-load-migration
JoaoGSP Aug 6, 2026
bf7497b
Merge pull request #991 from Autonomy-Logic/feature/DOPE-533-dt-save-…
JoaoGSP Aug 6, 2026
7fd4fd4
Merge branch 'development' into feat/baremetal-connection
thiagoralves Aug 6, 2026
05dc8d7
Merge pull request #980 from Autonomy-Logic/feat/baremetal-connection
thiagoralves Aug 6, 2026
8220a30
Merge branch 'development' of github.com:Autonomy-Logic/openplc-edito…
Aug 6, 2026
f5488dd
feat(datatypes): propagate datatype rename into references behind imp…
JoaoGSP Aug 6, 2026
10e70ff
Merge remote-tracking branch 'origin/development' into feature/DOPE-5…
JoaoGSP Aug 6, 2026
732745c
feat(data-types): add a form/code toggle to the data type editor
JoaoGSP Aug 6, 2026
29c8159
fix(datatypes): reject concurrent rename requests and handle rename r…
JoaoGSP Aug 6, 2026
8c419a1
fix(data-types): make a failed code commit fire once, and keep unread…
JoaoGSP Aug 6, 2026
a47552e
fix(runtime-v4): report a switch in STOP as a warning, not a failed u…
thiagoralves Aug 4, 2026
20ff67e
fix(runtime-v4): block run/stop while the runtime is mid-transition
thiagoralves Aug 5, 2026
dfef0a3
fix(device): restore the Connect button's font size and drop the dupl…
thiagoralves Aug 6, 2026
a45b99c
fix(ui): teach cn() the cp-* font scale instead of resizing the button
thiagoralves Aug 6, 2026
92536a5
feature(RTOP-193): VPP licensing flow ported onto the baremetal conne…
marconetsf Aug 7, 2026
4ffaa00
improvement(RTOP-193): portal the licence popover and drop the duplic…
marconetsf Aug 7, 2026
5a4cada
fix(RTOP-193): use vi.mock so the licence WS test runs in both runners
marconetsf Aug 7, 2026
14d015d
Merge pull request #995 from Autonomy-Logic/feature/DOPE-535-datatype…
JoaoGSP Aug 7, 2026
853b005
Merge branch 'development' into feature/RTOP-193-vpp-licensing
marconetsf Aug 7, 2026
4fce378
Merge remote-tracking branch 'origin/development' into feature/RTOP-1…
marconetsf Aug 7, 2026
9096d24
Merge remote-tracking branch 'origin/development' into feature/DOPE-5…
JoaoGSP Aug 7, 2026
c421355
Merge remote-tracking branch 'origin/feature/RTOP-193-vpp-licensing' …
marconetsf Aug 7, 2026
f4238f0
Merge pull request #994 from Autonomy-Logic/feature/DOPE-536-datatype…
JoaoGSP Aug 7, 2026
2b148ce
fix(RTOP-193): drop the licence when the selected board changes
marconetsf Aug 7, 2026
4389e37
Merge branch 'development' into feature/RTOP-193-vpp-licensing
marconetsf Aug 7, 2026
50219cf
test: cover deployReachedDevice and the cp-* font scale
thiagoralves Aug 7, 2026
ecd880b
fix(ui): answer the run/stop tooltip in the order the button blocks
thiagoralves Aug 7, 2026
3bcbdda
Merge branch 'development' into feat/runtime-v4-switch-stop-ux
JoaoGSP Aug 7, 2026
6aab007
Merge pull request #996 from Autonomy-Logic/feat/runtime-v4-switch-st…
JoaoGSP Aug 7, 2026
b4cdf14
Merge branch 'development' into fix/issue-479
JulioSergioFS Aug 7, 2026
df3452c
Merge pull request #990 from Autonomy-Logic/fix/issue-479
JulioSergioFS Aug 7, 2026
97bf8ed
refactor(semver): one parser for every version comparison (DOPE-448)
marconetsf Aug 6, 2026
193d710
feat(vpp): enforce minEditorVersion when installing a package (DOPE-448)
marconetsf Aug 6, 2026
893b617
feat(compile): block upload when a declared version floor is not met …
marconetsf Aug 6, 2026
8e0898b
docs(compat): record the editor/runtime/VPP compatibility strategy (D…
marconetsf Aug 6, 2026
23762be
docs(compat): mark the strategy as shipped, not planned (DOPE-448)
marconetsf Aug 6, 2026
20df50e
fix(compat): one version parser, one comparator, one board lookup (DO…
thiagoralves Aug 6, 2026
097a336
docs(compat): mark review items 3, 4 and 6 resolved (DOPE-448)
thiagoralves Aug 6, 2026
72d2ea0
fix(compat): reject version components too large to hold exactly (DOP…
thiagoralves Aug 6, 2026
5ef73f8
fix(compat): tolerate an unreadable version floor on the load path (D…
marconetsf Aug 7, 2026
291aded
fix(compile): install vendor cores from the VPP's board manager URL
thiagoralves Aug 7, 2026
fed8616
Merge branch 'development' into feature/RTOP-193-vpp-licensing
marconetsf Aug 7, 2026
5ceec8e
fix(RTOP-193): find license_blob.h in either repo layout
marconetsf Aug 7, 2026
9b3149e
Merge remote-tracking branch 'origin/feature/RTOP-193-vpp-licensing'
marconetsf Aug 7, 2026
72d1287
feat(data-types): wire the .dt code view into the ST language server
JoaoGSP Aug 7, 2026
e545b2b
Merge remote-tracking branch 'origin/development' into feature/DOPE-5…
JoaoGSP Aug 7, 2026
528c046
fix(st-lsp): stop a malformed synthetic URI from throwing out of the …
JoaoGSP Aug 7, 2026
63ad4b2
fix(st-lsp): stop an unknown .dt name from resolving as the first type
JoaoGSP Aug 7, 2026
e1c9ea6
Merge pull request #998 from Autonomy-Logic/feature/DOPE-537-datatype…
JoaoGSP Aug 7, 2026
6c39d4c
Merge branch 'development' into feature/RTOP-193-vpp-licensing
marconetsf Aug 7, 2026
e5fbc33
Merge pull request #997 from Autonomy-Logic/feature/RTOP-193-vpp-lice…
marconetsf Aug 10, 2026
27fbc4a
fix(package-manager): gate the build on VPP package integrity (DOPE-539)
marconetsf Aug 10, 2026
5b878d3
feat(console): render arduino-cli colour and collapse progress redraws
thiagoralves Aug 10, 2026
2dfbb55
fix(console): keep a progress redraw on one visual line
thiagoralves Aug 10, 2026
38f0f78
Revert "fix(console): keep a progress redraw on one visual line"
thiagoralves Aug 10, 2026
4f05cf6
Merge pull request #1000 from Autonomy-Logic/bugfix/DOPE-539-vpp-inte…
marconetsf Aug 10, 2026
326e97a
fix(review): format, and require https for boardManagerUrl
thiagoralves Aug 10, 2026
7312f62
Merge remote-tracking branch 'origin/development' into fix/vpp-board-…
thiagoralves Aug 10, 2026
94c28eb
fix(review): drop the unnecessary devices type assertion
thiagoralves Aug 10, 2026
6e7584c
Merge pull request #1001 from Autonomy-Logic/fix/vpp-board-manager-url
thiagoralves Aug 10, 2026
d301457
Merge development into feat/console-terminal-output
thiagoralves Aug 11, 2026
1f06b1f
fix(review): CRLF handling, escape stripping, YAML guards, atomic write
thiagoralves Aug 11, 2026
dd8e882
Merge pull request #1002 from Autonomy-Logic/feat/console-terminal-ou…
thiagoralves Aug 11, 2026
2c69ddf
chore: back-merge main into development
thiagoralves Aug 11, 2026
ab26d25
chore(release): bump version to 4.2.11
thiagoralves Aug 11, 2026
32e46b8
Merge pull request #1007 from Autonomy-Logic/chore/back-merge-main-in…
thiagoralves Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion binary-versions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"strucpp": {
"version": "v0.6.1",
"version": "v0.6.2",
"repository": "Autonomy-Logic/STruCpp"
}
}
5 changes: 5 additions & 0 deletions configs/webpack/webpack.config.renderer.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ const configuration: webpack.Configuration = {
// `npm run dev` to point at staging or localhost:
// `VPP_CATALOG_URL=http://localhost:3333 npm run dev`
VPP_CATALOG_URL: '',
// Same mechanism for the Edge WEB app (the `/buy` license page), which is
// a DIFFERENT origin from the API above. Falls back to the production
// host hardcoded in `system-adapter.ts` when unset:
// `OPENPLC_EDGE_WEB_URL=http://localhost:5173 npm run dev`
OPENPLC_EDGE_WEB_URL: '',
}),

new webpack.DefinePlugin({
Expand Down
4 changes: 4 additions & 0 deletions configs/webpack/webpack.config.renderer.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ const configuration: webpack.Configuration = {
// (`https://api.autonomylogic.com`) wins. Local/staging
// builds can prepend `VPP_CATALOG_URL=...` to override.
VPP_CATALOG_URL: '',
// Same for the Edge WEB app host (the `/buy` license page) — a different
// origin from the API above. Unset in release builds → `system-adapter.ts`
// falls back to https://edge.autonomylogic.com.
OPENPLC_EDGE_WEB_URL: '',
}),

new MiniCssExtractPlugin({
Expand Down
473 changes: 473 additions & 0 deletions docs/version-compatibility-strategy.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "open-plc-editor",
"description": "OpenPLC Editor - IDE capable of creating programs for the OpenPLC Runtime",
"version": "4.2.10",
"version": "4.2.11",
"license": "GPL-3.0",
"author": {
"name": "Autonomy Logic"
Expand Down
126 changes: 126 additions & 0 deletions resources/sources/Baremetal/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Modbus slave — module architecture

The `ModbusSlave` layer is split into 10 cohesive `modbus_*` translation units,
each owning one concern and its own build gate. Dependencies point **inward
only** (transport → protocol → handlers → data), and everything is glued by a
single shared buffer, `mb_frame`.

`Baremetal.ino` is unchanged: it `#include "ModbusSlave.h"` (the umbrella) and
calls `mbtask()` once per scan cycle.

## Layers

```
Baremetal.ino
│ (#include "ModbusSlave.h"; calls mbtask())
┌─────────▼──────────┐
│ ModbusSlave.* │ umbrella header + mbtask() facade
└───┬────────────┬───┘
┌─────────────▼──┐ ┌──▼──────────────┐
TRANSPORT │ modbus_serial │ │ modbus_tcp │ own the "wire"
│ (RTU single/dual)│ │ (Eth/WiFi/ETH) │
└──────┬──────────┘ └────────┬───────┘
│ fill mb_frame, │
│ ask for frame shape, │
└───────────┬──────────────┘
┌────────▼─────────┐
PROTOCOL │ modbus_pdu │ dispatch + per-FC frame shape
└───┬──────────┬───┘
┌─────────────▼─┐ ┌──▼──────────────┐
HANDLERS │ modbus_registers│ │ modbus_debug │
│ (store + op FCs)│ │ (0x41-0x48 + …) │
└──────┬─────────┘ └─────────────────┘
┌─────────▼───────────────────────────────────────────────┐
BASE │ modbus_frame (seam) · modbus_crc · modbus_types · modbus_config │
└─────────────────────────────────────────────────────────┘
```

## Modules

| Module | Responsibility | Build gate | Depends on |
|--------|----------------|------------|------------|
| **`modbus_config.h`** | Build configuration. Pulls in the generated `defines.h` (which has **no include guard**) and derives the composite gates (`MB_SERIAL_ACTIVE`, `DEBUG_*` defaults). The single guarded path through which `defines.h` reaches every TU. | — | `defines.h` |
| **`modbus_types.h`** | Shared contracts: FC / exception enums, `struct MBinfo`, `MAX_MB_FRAME`, `MBAP_SIZE`, `MB_DEBUG_*` status codes, bit helpers. Pure declarations, no storage. | — | `modbus_config` |
| **`modbus_frame.*`** | The **seam**: the global `mb_frame` / `mb_frame_len` buffer, the `modbus` instance (slave id + register banks) and `exceptionResponse()`. Every transport fills it, every handler writes into it. | — | `types` |
| **`modbus_crc.*`** | Modbus RTU CRC-16 (`calcCrc`) + the two lookup tables, defined **once** in the `.cpp` (they used to live in a header → one flash copy per TU). | — (RTU) | `frame` |
| **`modbus_registers.*`** | Register store + the standard **operation** FCs (`0x01`–`0x10`): `init_mbregs`, `get/write_discrete`, `read*`/`write*`. Compiled out of debug-only builds. | `MODBUS_ENABLED` | `frame` |
| **`modbus_debug.*`** | The always-on **debugger** FCs (`0x41`–`0x48`): info / set / get / md5 / status / version / board-id. Growth home for future custom FCs (e.g. the `0x49+` licensing set). | — | `frame`, `arduino_runtime_glue`, `ArduinoUniqueID` |
| **`modbus_pdu.*`** | The **protocol** layer: `process_mbpacket()` dispatches each FC to its handler, and it owns the **per-FC frame shape** — `mb_pdu_request_len()` (RTU length by FC) and `mb_pdu_skips_crc()` (which FCs bypass CRC). Single source of truth for "the set of function codes". | — | `registers`, `debug` |
| **`modbus_serial.*`** | The **RTU** transport (single- and dual-serial). Declared-length framing (robust over USB-CDC), one-byte resync, RS485 tx-enable timing, per-port RX assembly buffers. | `MB_SERIAL_ACTIVE` | `pdu`, `crc`, `frame` |
| **`modbus_tcp.*`** | The **TCP** transport (Ethernet / WiFi / ESP ETH). Brings the network stack up, accepts up to `MAX_SRV_CLIENTS`, services MBAP-framed requests. | `MBTCP` | `pdu`, `frame` |
| **`ModbusSlave.*`** | **Umbrella** header (re-includes every `modbus_*.h`, so `Baremetal.ino` is untouched) + the `mbtask()` facade that fans out to `handle_tcp()` / `handle_serial()`. | — | all |

## Build gates

Which TUs actually compile is driven by `defines.h` (generated per build) and the
composite gates in `modbus_config.h`:

- `MODBUS_ENABLED` — full Modbus operations. A debug-only build compiles
`modbus_registers.cpp` to an empty TU (the debugger reads IEC variables
directly through the strucpp debug table, needing no operation buffers).
- `MB_SERIAL_ACTIVE` = `MBSERIAL || DEBUGGER_ENABLED` — the serial transport.
Always on for baremetal (the debugger is always on).
- `MBTCP` (+ `MBTCP_ETHERNET` / `MBTCP_WIFI`) — the TCP transport.
- `MBSERIAL_ON_SECONDARY` — dual-serial: Modbus RTU on a distinct UART while the
debugger keeps the default serial (each with its own RX buffer). Otherwise
single-serial (`MBSERIAL_SHARES_DEBUG_SERIAL`), where RTU/debugger share the
default serial and `mb_frame` doubles as the RX-assembly buffer.

> **Rule:** any gated TU must see `defines.h`. Because `defines.h` has no include
> guard, it reaches a TU through exactly one guarded path: `modbus_config.h`
> (via `modbus_types.h`). Every `modbus_*` header includes that chain.

## Request lifecycle

**RTU (single-serial):**
1. `mbtask()` → `handle_serial()` → `handle_serial_port(mb_serialport, …, mb_frame, …)`.
2. Drain available bytes into `mb_frame`; **ask `modbus_pdu`** via
`mb_pdu_request_len()` how many bytes the frame should be (derived per FC).
3. Unless the FC is a debug FC (`mb_pdu_skips_crc()`), validate the CRC with
`modbus_crc::calcCrc()`.
4. `process_mbpacket()` dispatches: operation FC → `modbus_registers`; debug FC →
`modbus_debug`. The response is built back into `mb_frame`.
5. `handle_serial_port` appends the CRC and writes to the serial port.

**TCP:** same from step 4 onward, but `handle_tcp` reads/writes with an MBAP
header (no CRC) instead of RTU framing.

**Dual-serial:** `handle_serial()` services two ports with dedicated RX buffers
(`mb_rx_dbg` / `mb_rx_rtu`); `mb_frame` is only transient process/TX scratch.

## Invariants

1. **Transports do not know the function-code set.** They ask `modbus_pdu`
(`mb_pdu_request_len` + `mb_pdu_skips_crc`). Adding a function code touches
only `modbus_debug` (the handler) and `modbus_pdu` (dispatch + shape) — never
the transports.
2. **`mb_frame` is the one seam.** Every transport fills it, calls
`process_mbpacket()`, and reads the response back out. Single-threaded
cooperative scheduling means the transports time-slice within a scan; there
are no data races, but persistent partial state in `mb_frame` is a hazard —
see the note below.

## Adding a function code (e.g. custom `0x49+`)

1. Add the handler in **`modbus_debug.cpp`** (+ prototype in `modbus_debug.h`).
2. In **`modbus_pdu.cpp`**:
- add a `case` in `process_mbpacket()` that calls the handler;
- add the FC's request length to `mb_pdu_request_len()`;
- if the FC should bypass CRC on RTU, add it to `mb_pdu_skips_crc()`.
3. Add the FC constant to the enum in **`modbus_types.h`**.

That is the whole surface. `modbus_serial.*` and `modbus_tcp.*` are untouched.

## Known constraint — single-serial + TCP

`mb_frame` is shared between `handle_tcp()` and the single-serial assembly path.
In **single-serial** builds `mb_frame` doubles as the RX-assembly buffer and
holds a partial RTU/debug frame **across scan cycles**; since `mbtask()` runs
`handle_tcp()` first, an incoming TCP request can clobber that partial frame.
The framing logic resyncs, but the in-flight transaction is lost → intermittent
glitches under concurrent TCP load. Dual-serial + TCP is safe (dedicated RX
buffers; `mb_frame` only transient). The original design assumed a single Modbus
operation transport per board; the editor allowing RTU + TCP together violates
that. Fix is planned separately (dedicated single-serial RX buffer scoped to
`MBSERIAL && MBTCP`).
86 changes: 83 additions & 3 deletions resources/sources/Baremetal/Baremetal.ino
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
#include "openplc.h"
#include "defines.h"
#include "arduino_runtime_glue.h"
#include "license_gate.h"
#include "license_store.h" // license_store_read + LIC_BLOB_SIZE (via license_blob.h)

#ifdef MODBUS_ENABLED
#if defined(MODBUS_ENABLED) || defined(DEBUGGER_ENABLED)
#include "ModbusSlave.h"
#endif

Expand Down Expand Up @@ -128,8 +130,55 @@ void setup()
// Initialize hardware (HAL -- unchanged)
hardwareInit();

// Establish the run/stop state. Must follow hardwareInit() so the HAL has
// already configured its mode-switch pin: a board powered up with the
// switch in STOP must never execute a scan. Boards with no mode switch
// read RUN and start immediately, as they always have.
runtime_init_plc_state();

// -----------------------------------------------------------------------
// License gate. Hand the stored license blob to the license-core so it can
// verify it and arm its demo timer.
//
// With no license-core linked, `license_gate_init()` is the weak default in
// license_gate_weak.cpp and this whole block is a harmless no-op: actuation
// then stays unconditionally allowed, i.e. a board that never had licensing
// behaves exactly as before. `millis()` gives the core the same time base the
// runtime uses, with no esp_timer dependency.
//
// THE HARDWARE ANCHOR IS NOT PASSED IN. An earlier design read `UniqueID`
// here and handed the bytes over, which made the board's IDENTITY a claim
// made by the OPEN firmware: licensing hardware you do not own cost one edit
// to this file, substituting the target's anchor. The license-core reads the
// silicon itself, inside the closed artifact, so there is nothing here to
// substitute. (FC 0x48 still REPORTS the anchor to the editor, so a purchase
// can be bound to this board — reporting an identity and asserting one are
// different things.)
// -----------------------------------------------------------------------
{
// Zero-initialised: on a failed read the core is handed length 0, and a
// buffer of indeterminate bytes behind a zero length is the kind of detail
// that turns into a hard-to-place bug the first time someone reads past it.
uint8_t lic_blob[LIC_BLOB_SIZE] = {0};
size_t lic_len = 0;
if (license_store_read(lic_blob, sizeof(lic_blob), &lic_len) != LIC_STORE_OK)
{
// EMPTY / CORRUPT / UNSUPPORTED / any error: nothing usable was read,
// so present a zero-length blob. The core's verify rejects it and
// starts the demo window; with no core the weak gate ignores the args.
lic_len = 0;
}

license_gate_init(lic_blob, lic_len, (uint32_t)millis());
}

#ifdef MODBUS_ENABLED
#ifdef MBSERIAL
#ifdef MBSERIAL_ON_SECONDARY
// Dual-serial: Modbus RTU runs on a secondary UART (below) while
// the always-on debugger keeps the default serial — bring it up.
DEBUG_IFACE.begin(DEBUG_BAUD);
#endif
#ifdef MBSERIAL_TXPIN
// Disable TX pin from OpenPLC hardware layer
for (int i = 0; i < NUM_DISCRETE_INPUT; i++)
Expand All @@ -155,6 +204,18 @@ void setup()
mbconfig_serial_iface(&MBSERIAL_IFACE, MBSERIAL_BAUD, -1);
#endif
modbus.slaveid = MBSERIAL_SLAVE;
// NOTE (single-serial model): the debugger and Modbus RTU share one
// mb_serialport. When MBSERIAL_SHARES_DEBUG_SERIAL is defined the RTU
// port IS the debugger's default serial, so this single begin() also
// brings up the debugger. Running the debugger on the default USB
// serial while RTU uses a *different* UART simultaneously would need
// a second serial handler — a documented follow-up.
#elif defined(DEBUGGER_ENABLED)
// Modbus TCP-only build: no MBSERIAL, but the always-on debugger
// still needs the default serial up on mb_serialport to respond.
DEBUG_IFACE.begin(DEBUG_BAUD);
mbconfig_serial_iface(&DEBUG_IFACE, DEBUG_BAUD, -1);
modbus.slaveid = DEBUG_SLAVE;
#endif

#ifdef MBTCP
Expand All @@ -178,6 +239,15 @@ void setup()

init_mbregs(MAX_ANALOG_OUTPUT + MAX_MEMORY_WORD, MAX_MEMORY_DWORD, MAX_MEMORY_LWORD, MAX_DIGITAL_OUTPUT, MAX_ANALOG_INPUT, MAX_DIGITAL_INPUT);
mapEmptyBuffers();
#elif defined(DEBUGGER_ENABLED)
// Always-on debugger without full Modbus: bring up the serial port and
// the Modbus RTU framing/slave id ONLY. The debugger reads/writes IEC
// variables directly through the strucpp debug table (openplc_debug_*),
// so it needs NO operation buffers — init_mbregs()/mapEmptyBuffers() are
// deliberately not called here, saving SRAM on small boards.
DEBUG_IFACE.begin(DEBUG_BAUD);
mbconfig_serial_iface(&DEBUG_IFACE, DEBUG_BAUD, -1);
modbus.slaveid = DEBUG_SLAVE;
#endif

setupCycleDelay(base_tick_ns);
Expand Down Expand Up @@ -369,8 +439,12 @@ void scheduler()
sketch_loop();
#endif

#ifdef MODBUS_ENABLED
#if defined(MODBUS_ENABLED)
modbusTask();
#elif defined(DEBUGGER_ENABLED)
// Debug-only: poll the serial transport for debugger requests. No buffer
// sync (modbusTask's mirror loops) because there are no operation buffers.
mbtask();
#endif

if (!first_cycle)
Expand All @@ -392,12 +466,18 @@ void loop()
last_run += scan_cycle;
}

#ifdef MODBUS_ENABLED
#if defined(MODBUS_ENABLED)
// Only run Modbus task again if we have at least 10ms gap until the next cycle
if ((micros() - last_run) >= 10000)
{
modbusTask();
}
#elif defined(DEBUGGER_ENABLED)
// Debug-only: give the debugger extra serial-poll time between cycles too.
if ((micros() - last_run) >= 10000)
{
mbtask();
}
#endif

#ifdef SIMULATOR_MODE
Expand Down
Loading
Loading