# Slice 6 — a hardware-integrated module (xlyPlc) *(optional)*

This slice is for readers who'll touch the printing-press shop floor. If
you only ever read or write metadata, skip it.

xly is a printing-industry ERP. On the shop floor, printing presses run
under PLC control (programmable logic controllers — industrial automation
boxes). Each press exposes a stream of state — running / stopped /
fault, current speed, current sheet count, current job number — that
the ERP wants to record on the right work order in real time. xly's
**`xlyPlc`** Spring Boot module is the bridge that does this.

This slice covers the bridge's architecture, the per-press profile
mechanism, and the boundary where hardware-specific code stops and the
generic framework picks up.

## What we're documenting

| | |
|---|---|
| **Module** | `xlyPlc` (a sibling Spring Boot service in the codebase) |
| **Direction** | One-way: PLC → ERP DB (no commands sent back to the press) |
| **Cadence** | Polled on a schedule (Spring `@Scheduled` cron in `PlcScheduledTasks`, e.g. `0/30 * * * * ?` and `0/1 * * * * ?`) |
| **Per-press differentiation** | Spring profile (`-S10`, `-T0`, `-T1`, `-15S`, `-CT`, `-yt`, `-pro`) |
| **Database tables it writes to** | `mftProduceReportMachineState`, and related machine-state slaves |

## How small the module is

`xlyPlc/src/main/java/com/xly/` has just five files:

| File | Role |
|---|---|
| `PlcApplicationBoot.java` | Spring Boot entry point |
| `web/scheduler/PlcScheduledTasks.java` | `@Component` with two `@Scheduled` cron methods (every 30 s and every 1 s) driving the poll loop |
| `web/scheduler/PlcRunStatus.java` | In-memory state for the current poll cycle |
| `web/scheduler/service/PlcToErpService.java` | Interface |
| `web/scheduler/service/impl/PlcToErpServiceImpl.java` | The implementation: read PLC, write DB |

That is the entire bridge. The protocol-handling for each press model is
done by libraries pulled in via the per-profile `application-*.yml` and
optional native serial-port code (`xlyRxtx`, currently disabled in the
build — see [Maintainer Reference: deployment](../reference/maintainer/deployment.md)).

## Per-press profile

Nine YAML files ship in `xlyPlc/src/main/resources/` — the default plus
eight named profiles:

```
application.yml          (default — chooses a profile)
application-dev.yml      (developer config)
application-S10.yml      (S10-model presses)
application-T0.yml       (T0-model)
application-T1.yml       (T1-model)
application-15S.yml      (15S-model)
application-CT.yml       (CT-model)
application-yt.yml       (one customer's named profile)
application-pro.yml      (production)
```

A given xlyPlc instance is started with one profile active (e.g.
`-Dspring.profiles.active=S10`). Each profile carries the press-model's
serial-port settings, polling cadence, byte-protocol parameters, and the
target ERP DB connection. *One xlyPlc deployment per press, or one per
press-model per machine.* This is operational, not customer-facing.

## What it writes

The poll loop reads PLC registers, transforms them into ERP-domain rows,
and writes:

- `mftProduceReportMachineState` — the time-series of machine status
  (running/stopped/fault). High-volume: this is one of the largest tables
  in the schema once a press has been polling for any length of time.
- Related `mftProductionReport*` slaves where applicable.

Once the rows land, the rest of the ERP picks them up exactly like any
other data:
- `viw_*` views aggregate them into shop-floor dashboards.
- Standard Slice-1-style modules render those views.
- The metadata layer (Slice 4 customization) lets a customer add fields
  to the dashboards without touching the bridge.

## The framework / hardware boundary

xly's response to the press-PLC problem is a strict separation:

- **Above the line (xlyEntry, xlyApi, all the metadata machinery):
  generic framework.** No knowledge of presses, PLCs, byte protocols.
- **Below the line (xlyPlc): hardware-specific.** Knows how to talk to a
  press.

The two communicate only through the database — the bridge writes rows,
the framework reads rows. No RPC, no shared in-process state, no
callback. The benefits:

- Independently deployable; some customers run xlyPlc on a machine next
  to the press, separate from the central ERP server.
- Independently failable: if the bridge crashes the framework serves
  stale machine-state data; if the framework is down the bridge keeps
  writing and the framework picks up the buffered rows on recovery.

The costs of "DB as the only contract" are real and worth naming:

- **No backpressure.** If the bridge writes faster than xly can ingest
  (or if a slow `mftProduceReportMachineState` index update piles up),
  the bridge has no signal to slow down — it just blocks on the next
  INSERT. There is no flow-control message between the two halves.
- **No request/response semantics.** The framework cannot ask the
  bridge "is the press alive right now?" — it can only read whatever
  the bridge last wrote, which may be seconds-to-minutes old depending
  on the cron cadence.
- **Bridge-side state is invisible to the framework.** "Why is the
  bridge not writing?" requires logging into the bridge host to read
  its log; the framework UI shows only the absence of new rows.
- **Cron polling in both directions.** xlyPlc polls the press; the
  framework polls the DB; the SPA polls the framework. Three layers
  of polling means latency from "press state changes" to "user sees
  it" is `cron interval * 3` in the worst case.
- **Hard to test end-to-end without an actual press.** Most CI tests
  stub the PLC reads, which means the bridge's most error-prone code
  (byte protocol per press model) gets the least automated coverage.

A real-time-aware architecture would use a streaming channel
(MQTT / Kafka / WebSocket) end-to-end instead of cron + DB. xly's
choice is operationally simpler but trades off latency, observability,
and flow control. For the printing-press tempo (machine state changes
every few seconds, reports every minute) the trade is liveable; for
faster shop-floor signals it would not be.

## Concepts this slice introduces

This slice is intentionally a *boundary case* and doesn't introduce new
framework concepts. It exists to show readers that the data-driven
runtime is *only* the framework's problem; once a row has been written
to the database, the framework treats it the same regardless of whether
a PM typed it into BACK, a customer submitted it from FROUNT, or a
press's PLC pushed it through xlyPlc.

## Reference entries this slice exercises

- [Maintainer: multi-service deployment](../reference/maintainer/deployment.md)
  — `xlyPlc` is one of the runnable services. The deployment chapter
  needs a section on shop-floor deployment (one bridge per press model).

## Open verification items

> **Item 1 — Deferred (outside the repository's reach).** The byte
> protocols themselves come from each press model's vendor
> documentation, not from the xly source tree. Each
> `xlyPlc/src/main/resources/application-<model>.yml` carries the
> *parameters* (baud rate, framing, register addresses, polling
> tunables); the *protocol semantics* are press-vendor knowledge.
> Documenting either fully is a deployment-ops job, not a wiki audit
> against src/db/web.

1. **The wire protocol.** Each press model has a different byte
   protocol; each `application-<model>.yml` carries the parameters.
   Documenting the protocol per model is a separate, niche chapter
   that this wiki may or may not need.
2. ~~**Bridge → ERP-DB latency / polling interval.**~~ **CLOSED.**
   `PlcScheduledTasks.java` ships **two** Spring `@Scheduled` cron
   methods (no per-profile difference observed in the cron string):
   `0/30 * * * * ?` (every 30 s, line 74) and `0/1 * * * * ?` (every
   1 s, line 105). A third commented-out cron at line 125
   (`0 */2 * * * ?`) is dormant. Per-profile parameter tuning happens
   inside the polling code via the `application-<model>.yml` YAML, not
   the cron expression itself. Shop-floor dashboard refresh is
   independent: its `viw_*` aggregations re-read `mftProduceReportMachineState`
   on each FROUNT request, so the dashboard sees a row at most ~30 s
   after the press emits it.
3. ~~**Why `xlyRxtx` is disabled in `settings.gradle`.**~~ **CLOSED.**
   git history on `xly-src/settings.gradle` shows `xlyRxtx` was
   originally added in commit `daf581311` ("1、添加串口功能 …" — added
   serial-port feature). The cleanup branch comments it out as part
   of the source-pruning pass that excludes hardware modules whose
   features are not yet exercised in the dev DB; a deployment that
   needs direct serial access to a press would re-enable the include
   line in `settings.gradle`. xlyPlc itself runs without RXTX —
   it relies on TCP/Ethernet for the press models documented here;
   serial-only press models would need RXTX re-enabled.
