--- name: plant-model description: "Build a minimal plant model (the simulated physical mechanism) for a new CppModel simulation, and scaffold a starter simulation file wiring it to the controller under test. Use when a customer needs to simulate a new physical mechanism (actuator, motor, sensor pair) that has no model yet." --- ## Reading notes - This is the cppmodel plugin's `plant-model` skill. It was written for Claude Code, but applies to any AI assistant; these notes translate what's specific to Claude Code. - `cppmodel:` ("the `cppmodel:` skill") is this plugin's `` skill, in `/skills//SKILL.md`: when this one says to use it, load that skill and follow it. - "The `cppmodel` MCP server" is the one this plugin installs (`https://mcp.cppmodel.com/mcp`). - `` is the root of this plugin: the directory two levels above this file. - "Ask with `AskUserQuestion`": use your client's multiple-choice question tool if it has one, otherwise ask in a message that lists the options, and wait for the answer. - "Authenticate with `/mcp`": sign in to the `cppmodel` MCP server the way your client does. - Other remarks about Claude Code only apply there. --- ## Goal Build the minimal *logic* model of the real mechanism's physics - just enough behavior to drive and exercise the real controller correctly, not an accurate physics simulation. See "Keep it minimal" below for what that does and doesn't mean. ## Scope: this builds the plant side This skill assumes the **plant** is the component being simulated and the **controller** is the real code under test - see `cppmodel:simulation-testing`'s "which side is being simulated" note. Concretely: the model's `actuators` struct is what the controller commands into it (read via `CppModel_getInput*` in the simulation file), and its `sensors` struct is what it reports back to the controller (published via `CppModel_setOutput*`). If instead the controller is the side being simulated and the plant is real/external, this skill's shape doesn't apply as-is - the roles invert. ## What this produces 1. A model - wherever this project keeps its models. Don't assume a path; ask, or look for an existing `models/` folder near the other simulations first. Shape depends on the language (see below): - **C**: a `.h`/`.c` pair (naming convention: `Model.h` / `Model.c`). - **C++**: usually a single header-only `Model.h` with inline methods (this is the shape already-existing C++ models in this kind of project tend to use), unless the project's own convention already splits declaration/definition into a `.h`/`.cpp` pair - check first. 2. A starter simulation file skeleton wiring the new model's actuators/sensors to the controller under test, with no real test scenarios yet - hand off to the `cppmodel:simulation-testing` skill to fill those in and to know how that skeleton should be shaped for this project. If a single request asks for both a new model and a working simulation in one go, still do these in order - finish and settle the model (including the questions below) before starting the simulation skeleton, don't build them in parallel or sketch the simulation first and backfill the model to fit it. The model is the part the customer's answers actually shape; the simulation skeleton is mechanical once the model's actuators/sensors are fixed. ## Language: C or C++ Use the `cppmodel:language` skill to decide before writing anything below - it covers checking for a stored project preference, detecting the project's existing convention, and asking/storing the answer. ## Questions to ask first Before asking anything, ask once whether the customer already has a description of the machine, such as a spec, a datasheet, notes, or a few sentences. If a document for this mechanism already exists, read it instead. Save their description as the draft document (`cppmodel:documentation`, "Lifecycle"). Then don't re-ask what it already answers clearly, but do push on anything vague in it, as with any other answer. If they have nothing written, skip the draft and go straight to the questions. Don't guess these - ask. Treat this like onboarding a new team member onto the machine: the goal isn't to collect five answers and move on, it's for the customer to actually walk you through how their machine behaves. A thin actuator/sensor count is not a signal that the machine itself is simple - two booleans in and one out can still hide interlocks, multi-stage motion, debounce or settling delays, or an internal state that never surfaces as its own sensor. Don't assume there's "no real model behind" a function just because its signature is small; ask until you understand the behavior, not until the struct fields are filled in. If an answer is vague or doesn't add up with a later one, push on it rather than filling the gap with a guess. 1. **What kind of mechanism is it?** In particular: does it travel between two end-stops (a cylinder, a lift, a gate - bounded 0..max), or does it move past repeating positions continuously (a rotating disc, an indexing wheel, a conveyor - unbounded, wraps around)? This decides which of the two shapes below to use - but if the real behavior doesn't cleanly fit either (e.g. it pauses partway, has more than two directions, or a sensor depends on more than position alone), say so and adapt rather than forcing it into the nearest template. 2. **What does the controller command (actuators)?** Usually one or more booleans (e.g. `motor_up`/`motor_down`, `valve_open`) or a signed speed/PWM value. Match whatever the real controller code already outputs - look at its `outputs` struct. 3. **What does the controller read back (sensors)?** Booleans from limit/proximity switches, a position/pulse counter, a pressure or photocell reading. Match the controller's `inputs` struct. 4. **How does it interact with the operator, the environment, or other mechanisms?** E.g. a manual override or e-stop the operator can trigger independent of the controller, ambient conditions (temperature, load) that change its behavior, or interlocks/shared state with a neighboring mechanism (one machine can't move until another is clear). For each one, ask specifically whether the real controller actually commands or reads it (e.g. an e-stop wired to the controller as a real input) - if so, it's a genuine actuator/sensor and belongs in those structs like anything else. If instead it's something only the simulation itself would inject to drive a scenario (the controller never sees it directly), it's model config/state instead, poked directly by the simulation's step schedule rather than crossing the `CppModel_getInput*`/`setOutput*` boundary - don't force it into `actuators`/`sensors` just because it's related to them. 5. **Timing**: how long does a full traverse take in the real machine (e.g. "fully extends in about 2 seconds")? Different speeds for different directions/actuators? What `task_period_ms` will the simulation run at (existing simulations in this project are usually 1ms - check one)? 6. **Limits and thresholds**: the position range (0..max, or the repeat period for a wraparound mechanism), and at what position(s) each sensor should read true. 7. **Anything that happens between commanding and sensing that isn't obvious from the I/O alone?** E.g. does a direction change require stopping first, is there a delay before a sensor responds, can two actuators be active at once and if so what happens, are there internal faults not already covered above. These won't show up by counting signals - they only surface by asking. ## Two shapes, pick one and adapt Each is shown in both languages - use whichever the `cppmodel:language` step decided. **Bounded, end-stop mechanism** (e.g. a ladder/lift/cylinder): ```c typedef struct ModelActuators_s { bool ; bool ; } ModelActuators_t; typedef struct ModelSensors_s { bool ; } ModelSensors_t; typedef struct ModelConfig_s { uint32 delta_a; uint32 delta_b; uint32 max_position; uint32 sensor_active_position; } ModelConfig_t; typedef struct Model_s { ModelConfig_t config; ModelActuators_t actuators; ModelSensors_t sensors; sint32 position; bool is_moving_up; bool is_moving_down; } Model_t; void ModelCyclic(Model_t *const context, const unsigned long currentTime) { const sint32 previousPosition = context->position; if (context->actuators.) { context->position += context->config.delta_a; if (context->position >= context->config.max_position) { context->position = context->config.max_position; } } else if (context->actuators.) { context->position -= context->config.delta_b; if (context->position <= 0) { context->position = 0; } } context->sensors. = context->position >= context->config.sensor_active_position; context->is_moving_up = context->position > previousPosition; context->is_moving_down = context->position < previousPosition; } ``` C++ equivalent - a class over `cppmodel::Model` (`cppmodel.hpp`, C++17), plain public members instead of separate actuators/sensors/config structs (this is the shape an already-existing C++ model in this kind of project is likely to use - check one if present and match it instead of this exactly; older models derive from `CppModelBase::Model` in `cppmodel/Model.h`, the same class under its pre-0.8 names). `Model` is header-only and lives entirely in the project's code: the SDK library never sees it. `RunCyclic`'s parameter type has changed between SDK releases: copy it from the vendored `dependencies/include/cppmodel/cppmodel.hpp` (`Model.h` before 0.8). Since SDK 0.6 it's `unsigned long long simulationTime_ms`, as below; older releases used `double`. With `override`, a mismatch is a compile error rather than a silently never-called method: ```cpp #include class Model : public cppmodel::Model { public: // actuators - commanded by the controller bool = false; bool = false; // sensors - reported back to the controller bool = false; sint32 position = 0; bool is_moving_up = false; bool is_moving_down = false; uint32 delta_a = 0; uint32 delta_b = 0; uint32 max_position = 0; uint32 sensor_active_position = 0; inline Model() {} inline void RunCyclic(unsigned long long simulationTime_ms) override { const sint32 previousPosition = position; if () { position += delta_a; if (position >= max_position) { position = max_position; } } else if () { position -= delta_b; if (position <= 0) { position = 0; } } = position >= sensor_active_position; is_moving_up = position > previousPosition; is_moving_down = position < previousPosition; } }; ``` Unlike the C shape, nothing here goes through `CppModel_getInput*`/`setOutput*` - the simulation file just instantiates this class as a member and reads/writes its public fields directly each step (see `cppmodel:simulation-testing`). **Unbounded, indexed/rotating mechanism** (e.g. a feed wheel, a conveyor with repeating slots): ```c typedef struct ModelConfig_s { double sensor_ratio; bool negativeDirection; } ModelConfig_t; typedef struct Model_s { ModelActuators_t actuators; ModelSensors_t sensors; ModelConfig_t config; double precise_position; sint32 position; bool is_moving; } Model_t; void ModelCyclic(Model_t *const context, const unsigned long currentTime) { context->precise_position += (double)context->actuators. * context->config.sensor_ratio * (context->config.negativeDirection ? -1 : 1); context->position = (sint32)context->precise_position; context->is_moving = context->actuators. != 0; context->sensors. = context->position % < || context->position % > ; } ``` The `double precise_position` accumulator matters: truncating straight to an integer position each cycle loses fractional motion at low speeds and the position never advances. Always reset both `position` and `precise_position` together in any init/reset path. C++ equivalent: ```cpp #include class Model : public cppmodel::Model { public: = 0; bool = false; double sensor_ratio = 0.; bool negativeDirection = false; double precise_position = 0.; sint32 position = 0; bool is_moving = false; inline Model() {} inline void RunCyclic(unsigned long long simulationTime_ms) override { precise_position += (double) * sensor_ratio * (negativeDirection ? -1 : 1); position = (sint32)precise_position; is_moving = != 0; = position % < || position % > ; } }; ``` ## Converting real-world timing into a delta-per-cycle The model has no notion of seconds or physical units - only "how much does position change per simulated cycle". Derive it: ``` delta_per_cycle = full_range_units / (traverse_time_ms / task_period_ms) ``` Example: a cylinder spans 0..1000 and fully extends in 2000ms, simulated at `task_period_ms = 1`: `delta_per_cycle = 1000 / (2000 / 1) = 0.5` per cycle (round/adjust for integer config types). ## Keep it minimal The goal is the least logic that reproduces control-relevant behavior - not a physically accurate plant. No differential equations, no inertia/friction/acceleration curves, no plant-side PID, no SI units. If a customer's description implies one of those, don't refuse it outright - ask whether a simpler delta/threshold version would already exercise the controller correctly, and build that instead unless they confirm the extra complexity is actually needed. This is about keeping the *implementation* minimal, not the *understanding*. Simplify the code, not the questioning that precedes it - decide what to leave out because the customer told you it doesn't matter for the controller, not because you didn't ask. ## Next step Building the model is not the end of the task - it's not runnable or verifiable on its own. Once the model and starter simulation exist, use the `cppmodel:simulation-testing` skill to add real test scenarios and assertions, build, and run it; that skill in turn hands off to `cppmodel:simulations` to fetch results through the `cppmodel` MCP server. Don't stop after scaffolding the model and call the task done - a customer asking to "simulate X" wants it built, tested, run, and its results checked, not just the model file. If the model stands for a real machine whose behaviour will later be checked against field logs, also offer `cppmodel:experiment-design` once it runs: a plan for collecting those logs, aimed at what this first model is least sure of, with the model's predicted trace for every run. Don't update the documentation while the model is still changing. It is finalized once the plant and controller are settled (see `cppmodel:documentation`, "Lifecycle").