{"id":50656921,"url":"https://github.com/tumbleowlee/ferrowl","last_synced_at":"2026-07-05T02:00:39.137Z","repository":{"id":240320122,"uuid":"802314996","full_name":"TumbleOwlee/ferrowl","owner":"TumbleOwlee","description":"CLI application for Modbus and OCPP CS/CSMS simulation.","archived":false,"fork":false,"pushed_at":"2026-07-04T23:24:45.000Z","size":2813,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-07-05T00:10:56.472Z","etag":null,"topics":["cli","client","modbus","modbus-rtu","modbus-tcp","ocpp","ocpp16","ocpp16j","ocpp201","ocpp201j","ocpp21","rtu","rust","server","tcp","tui"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TumbleOwlee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-05-18T01:00:38.000Z","updated_at":"2026-07-04T23:23:30.000Z","dependencies_parsed_at":"2024-07-18T13:25:59.573Z","dependency_job_id":"244df82c-7d51-4c42-9438-2aa1b4321605","html_url":"https://github.com/TumbleOwlee/ferrowl","commit_stats":null,"previous_names":["tumbleowlee/modbus-cli-rs","tumbleowlee/ferrowl"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/TumbleOwlee/ferrowl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TumbleOwlee%2Fferrowl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TumbleOwlee%2Fferrowl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TumbleOwlee%2Fferrowl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TumbleOwlee%2Fferrowl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TumbleOwlee","download_url":"https://codeload.github.com/TumbleOwlee/ferrowl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TumbleOwlee%2Fferrowl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35141083,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","client","modbus","modbus-rtu","modbus-tcp","ocpp","ocpp16","ocpp16j","ocpp201","ocpp201j","ocpp21","rtu","rust","server","tcp","tui"],"created_at":"2026-06-08T00:04:48.314Z","updated_at":"2026-07-05T02:00:38.975Z","avatar_url":"https://github.com/TumbleOwlee.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Banner](./images/ferrowl-banner.svg)\n\n# Ferrowl - TUI Modbus + OCPP Server/Client\n\n[![Claude](https://img.shields.io/badge/Claude-D97757?logo=claude\u0026logoColor=fff)](#) [![status-badge](https://github-ci.code-ape.dev/api/badges/3/status.svg?workflow=check)](https://github-ci.code-ape.dev/repos/3) [![status-badge](https://github-ci.code-ape.dev/api/badges/3/status.svg?workflow=nightly)](https://github-ci.code-ape.dev/repos/3)\n\nFerrowl is a TUI application, written in Rust, to simulate both **Modbus** (Client and Server) and **OCPP** (Charging Station / CSMS) devices. Create configurations on the fly, save and load configurations or sessions to set up multiple Modbus or OCPP instances side by side. The aim is to provide a technical but intuitive interface that can run on any device without an available GUI environment.\n\nIf you prefer a GUI application, this tool is not the right choice. For Modbus, refer to a GUI application like [QModbus](https://github.com/ed-chemnitz/qmodbus/).\n\n\u003e [!WARNING]\n\u003e Prior to **v0.4.0** the application was based on a draft implementation. Over time additional features were added but messed up the architecture and made it difficult to add new views, dialogs and support of multiple instances. Thus, starting with **v0.4.0** the application got a full rewrite. This also affects the configuration files and their management. You can migrate configuration files created by versions prior to **v0.4.0** using the `migrate` subcommand - e.g. `ferrowl migrate -i old-config.json -o new-config.json` (supports JSON and TOML as input and output).\n\n## Goal\n\nProvide a CLI application to simulate Modbus Servers and Clients as well as OCPP Charging Stations and Central Systems (CSMS, OCPP 1.6, 2.0.1 and 2.1), visualize the states of all registers and charging-station fields, make manipulation available and provide script based simulation capabilities - e.g. utilize the tool to simulate EVSEs over Modbus or OCPP.\n\n## Architecture\n\nThe project is organized as a Cargo workspace and builds the `ferrowl` binary. See the following image for the dependencies between the different crates of the workspace and their provided functionality.\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"./images/architecture.svg\"\u003e\n\u003c/p\u003e\n\n\n| Crate | Responsibility |\n| ----- | ----- |\n| `ferrowl` | Binary. Event/redraw loop, tabs, views, dialogs, `:` commands, session \u0026 device configuration, `migrate` subcommand. |\n| `ferrowl-ui` | Reusable [ratatui](https://ratatui.rs) building blocks: widgets with their state types, styling and alternate-screen handling. |\n| `ferrowl-ui-derive` | Proc macros for the UI layer: `#[derive(TableEntry)]`, `#[derive(Overlay)]` and `#[derive(Focus)]` (keyboard focus cycling and event dispatch for views). |\n| `ferrowl-lua-derive` | Proc macro `#[derive(Module)]` that bridges Rust host types into Lua modules. |\n| `ferrowl-codec` | Register descriptions (slave id, function code, address, access, format) and the codec between raw `u16` words and typed values. |\n| `ferrowl-store` | In-memory model of a Modbus register space — access-checked value cells shared as `Arc\u003cRwLock\u003cMemory\u003e\u003e`. |\n| `ferrowl-modbus` | Modbus client and server tasks over TCP and RTU, built on [tokio-modbus](https://github.com/slowtec/tokio-modbus). |\n| `ferrowl-ocpp` | OCPP protocol types and actions with a version-generic `Version` trait; Charging Station (CS) and Central System (CSMS) over JSON-on-WebSocket, wrapping [rust-ocpp](https://github.com/codelabsab/rust-ocpp). Supports OCPP 1.6, 2.0.1, and 2.1. |\n| `ferrowl-lua` | Embedded Lua runtime ([mlua](https://github.com/mlua-rs/mlua)) exposing the `C_Register`, `C_Time`, `C_OCPP` and `C_Log` modules to simulation scripts. |\n| `ferrowl-ring` | Fixed-capacity ring buffer generic over the element type; backs the per-module log pane (as `Ring\u003c(u64, String), N\u003e`). |\n| `ferrowl-util` | Shared helpers: config (de)serialization, tracked tokio task spawning, small macros and traits. |\n\nAll runtime interaction meets in the shared memory of a module: the network task polls a remote server (client role) or answers incoming requests (server role) against it, the Lua simulation thread reads and writes it through the `C_Register` bridge, and the UI decodes its raw words into the typed values shown in the register table.\n\n## OCPP\n\nAlongside Modbus, Ferrowl simulates **OCPP** charging infrastructure over JSON-on-WebSocket. Both protocol versions and both roles are supported:\n\n- **Versions:** OCPP **1.6**, **2.0.1**, and **2.1**.\n- **Roles:** **Charging Station** (client, connects out to a CSMS) and **Central System / CSMS** (server, accepts incoming stations and tracks each connection).\n\nSupported capabilities (grouped by area):\n\n- **Transactions \u0026 metering** — start/stop transactions, `MeterValues`, live connector state (status, phases, voltage, per-phase current, power, energy).\n- **Reservations** — `ReserveNow` / `CancelReservation`, per connector.\n- **Authorization** — RFID accept-lists, both station-wide and per-connector, plus local-list management.\n- **Smart charging** — charging profiles and per-purpose charge limits, including stack-level reject.\n- **Remote control** — remote (1.6) / requested (2.0.1, 2.1) start \u0026 stop, availability changes, reset, firmware update and diagnostics.\n- **OCPP 2.0.1 extras** — variable get/set and monitoring, display messages, certificate management, and the EVSE/connector object model.\n- **OCPP 2.1 extras** — DER control (get/clear/set), tariffs, priority charging, settlement and web-payment notifications, battery swapping. Most 2.1-only actions get a typed send dialog; a few whose payload is an inherently nested/repeated structure (battery data, certificate-chain-status requests, DER-control reports, full tariff objects, periodic-event-stream setup, dynamic-schedule updates) stay on the raw-JSON editor.\n\nIn the TUI each OCPP module shows a connector/station table, a scope-filtered action list with per-version send dialogs (typed value editors plus a raw-JSON mode), and a capped message log that can be mirrored to a file via `:log`. Simulation behaviour is scripted in Lua for both roles — see [Lua Support](#lua-support).\n\n## Nightly Build\n\nThis repository provides an updated Nightly build - available on the Release page. Prebuilt executables are provided for Unix and Windows.\n\n## Quickstart\n\nThis project is written in Rust, thus you will have to install the Rust toolchain to compile it. Just follow the instructions on [rustup.rs](https://rustup.rs/) to set up the environment. Afterwards you are able to compile this project from source using the following command.\n\n```sh\ncargo build --release\n```\n\nAlternatively, you can also run it directly using the following command. Please refer to `--help` for all available runtime options and to the Release page for prebuilt binaries.\n\n```bash\n# Build and run\ncargo run --release\n\n# Or with the application already built\nferrowl\n\n# Or in demo mode\nferrowl --demo\n\n# Or with an existing session file\nferrowl --session session.toml\n\n# Or with a device configuration only (starts a TCP client polling 127.0.0.1:5020,\n# matching the --demo server; use --module for a custom endpoint or role)\nferrowl --device device.toml\n```\n\nIf started without any additional parameters, the module setup dialog is shown. After the module is created, you can add registers using the `:add` command. To create an OCPP module instead, choose the OCPP type in the `:new` setup dialog.\n\nThe bundled `session.toml` wires up a CSMS plus a Charging Station pair (`csms-demo.toml` / `cs-demo.toml`), so `ferrowl --session session.toml` brings both OCPP modules online at once.\n\n\u003e [!IMPORTANT]\n\u003e You can use *VIM*-like table navigation or alternatively the arrow keys. You can exit using the `:qa` command. Typing `:` will automatically switch to command mode. See the shown overlay for all available commands.\n\n## Commands\n\n| Command | Description |\n| ----- | ----- |\n| `:q \\| :quit` | Quit tab / Close active module |\n| `:qa \\| :qall` | Close all tabs / Exit application |\n| `:e \\| :edit` | Edit current module |\n| `:n \\| :new` | Create new module |\n| `:l \\| :load [PATH]` | Load device configuration |\n| `:a \\| :add` | Add new register to module |\n| `:start` | Start module execution |\n| `:stop` | Stop module execution |\n| `:restart` | Restart module execution |\n| `:set \u003creg\u003e \u003cval\u003e` | Write register value |\n| `:s \\| :save \\| :w \\| :write [PATH]` | Save session |\n| `:wd \\| :write-device [PATH]` | Save device configuration |\n| `:log \u003cFILE\u003e\\|clear` | Set log output file for the active tab (`:log clear` clears the ring log) |\n| `:lua start\\|stop` | Start/Stop lua execution |\n| `:scripts` | Manage Lua scripts (create, edit, toggle, delete) |\n| `:reload` | Reload device configuration |\n| `:compact` | Toggle compact table mode |\n| `:order [col] [asc\\|desc]` | Sort table by column |\n\n## Keybindings\n\n| Keybind | Description |\n| ----- | ----- |\n| `Enter` | Open/Confirm dialog |\n| `Escape` | Cancel dialogs |\n| `k \\| Up` | Select previous table entry |\n| `h \\| Left` | Scroll left in table view |\n| `l \\| Right` | Scroll right in table view |\n| `j \\| Down` | Select next table entry |\n| `Tab` | Focus next dialog element |\n| `Shift-Tab` | Focus previous dialog element |\n| `Space` | Click focused button |\n| `G` | Move to bottom of table |\n| `g` | Move to top of table |\n| `0` | Move to left edge of table |\n| `$` | Move to right edge of table |\n| `z` | Toggle compact table mode |\n| `ctrl + t, l` | Switch to next tab |\n| `ctrl + t, h` | Switch to previous tab |\n| `ctrl + d` | Clear input field |\n| `ctrl + f` | Accept autofill in input field |\n\n## Impressions\n\n### General\n\n#### Command Help\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/command-help.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Dialog: New Module\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/new-module.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n### Modbus\n\n#### Dialog: New Module\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/modbus/new-module.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Dialog: Add Register\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/modbus/add-register.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Dialog: Edit Register\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/modbus/edit-register.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Dialog: Edit Selection Register\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/modbus/edit-selection-register.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n### OCPP \n\n#### Dialog: New Module\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/new-module.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Dialog: Action\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/action.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Client View - General\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/client.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Client View - CS\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/client-cp.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Client View - Connector\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/client-con.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Server View - General\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/server.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Server View - CS\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/server-cp.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n#### Server View - Connector\n\n\u003cp align=\"center\"\u003e\n    \u003cp align=\"center\"\u003e\n        \u003cimg src=\"./images/ocpp/server-con.png\" style=\"border-radius: 8px\"\u003e\n    \u003c/p\u003e\n\u003c/p\u003e\n\n## Configuration\n\n### Session Configuration\n\nThe session configuration can be saved using `:write` and contains the module configuration consisting of the name, path to the device configuration, the role and endpoint information. Timings (`timeout_ms`, `delay_ms`, `interval_ms`) are part of the device configuration, not the session.\n\n```toml\n[[modules]]\nname = \"evse-1\"\ndevice = \"configs/evse.toml\"\nrole = \"server\"\n\n[modules.endpoint]\ntransport = \"tcp\"\nip = \"127.0.0.1\"\nport = 5020\n```\n\nBesides TCP, a serial RTU endpoint is supported. `parity` (`even`, `odd` or `none`, case-insensitive), `data_bits` and `stop_bits` are optional; `baud_rate` defaults to `19200`.\n\n```toml\n[modules.endpoint]\ntransport = \"rtu\"\npath = \"/dev/ttyUSB0\"\nbaud_rate = 19200\nparity = \"none\"\ndata_bits = 8\nstop_bits = 1\n```\n\nAn **OCPP** module session entry is tagged `type = \"ocpp\"` and carries only the name, the\ndevice-config path and the websocket endpoint (`protocol` is `ws` or `wss`); the OCPP version,\nrole, timeout and Lua scripts live in the referenced device file.\n\n```toml\n[[modules]]\ntype = \"ocpp\"\nname = \"cs-1\"\ndevice = \"configs/cs.toml\"\nprotocol = \"ws\"\nip = \"127.0.0.1\"\nport = 9000\n```\n\n### Device Configuration\n\nThe device configuration can be saved using `:write-device` and contains the register information of the device and all necessary timings.\n\n```toml\n[definitions.setpoint]\nslave_id = 1\nread_code = 4          # 4 = holding register\naddress = 0\ntype = \"U16\"\naccess = \"ReadWrite\"\ndescription = \"charge setpoint (W)\"\ndefault = 0            # start at zero watts on every load\n\n[definitions.power]\nslave_id = 1\nread_code = 4\naddress = 1\ntype = \"U16\"\naccess = \"ReadWrite\"\ndescription = \"active power (W)\"\ndefault = 0\n\n[definitions.state]\nslave_id = 1\nread_code = 4\naddress = 2\ntype = \"I16\"\naccess = \"ReadWrite\"\ndescription = \"charge state\"\ndefault = 0            # start in the \"waiting\" state\nvalues = [\n    { name = \"waiting\", value = 0 },\n    { name = \"charging\", value = 2 },\n    { name = \"error\", value = -1 },\n]\n\n# Global Lua scripts, run every simulation cycle while enabled (see :scripts).\n[[scripts]]\nname = \"mirror setpoint\"\ncode = \"\"\"\nC_Register:Set(\"power\", C_Register:Get(\"setpoint\"))\n\"\"\"\nenabled = true\n```\n\n\u003e [!IMPORTANT]\n\u003e It's possible to create virtual registers using `virtual = true` to store values not accessible over Modbus.\n\n#### Register fields\n\n| Field | Default | Description |\n| ----- | ----- | ----- |\n| `type` | *(required)* | Value encoding: `U8`…`U128`, `I8`…`I128`, `F32`, `F64`, `Ascii`. |\n| `slave_id` | `0` | Modbus unit / slave id. |\n| `read_code` | `3` | Read function code: `1`=Coil, `2`=DiscreteInput, `3`=InputRegister, `4`=HoldingRegister. |\n| `address` | *(none)* | Start address. Omit (or set `virtual = true`) for a virtual register. |\n| `virtual` | `false` | Hold the value locally instead of mapping it to a Modbus address. |\n| `access` | `ReadWrite` | `ReadOnly`, `WriteOnly` or `ReadWrite`. |\n| `endian` | `Big` | Byte order: `Big` or `Little`. |\n| `resolution` | `1.0` | Scaling factor applied to the raw value for display; edit dialogs and `:set` take the unscaled raw value. |\n| `bitmask` | *(none)* | Bit-field mask for integer types, as a hex (`\"0xFF00\"`) or decimal string; the shift is derived from the mask's trailing zeros. Ignored for float and ASCII types. |\n| `length` | `1` | ASCII width in registers (ignored for numeric types). |\n| `alignment` | `Left` | ASCII alignment: `Left` or `Right`. |\n| `values` | `[]` | Named values for selection-style registers. |\n| `default` | *(none)* | Default value written to memory on startup / configuration load. |\n| `update` | *(none)* | *Deprecated.* Legacy per-register Lua snippet; migrated on load into the global `[[scripts]]` list (named after the register) and never written back. |\n| `description` | `\"\"` | Free-text description. |\n\n#### Device-level options\n\nAlongside `definitions`, a device file may carry a `version` (stamped automatically on save), default timings, and explicit client read ranges:\n\n```toml\ndescription = \"EVSE charge point\"\ntimeout_ms = 2000      # per-request timeout\ndelay_ms = 1000        # delay before the first read\ninterval_ms = 1000     # poll interval\n\n# Client read batching per function code. Each value is a comma-separated list of inclusive\n# address ranges (a bare \"5\" is the single address 5). When unset, contiguous registers are\n# auto-merged into requests.\n[read_ranges]\nholding = \"0-100,140-160\"\ninput = \"0-10\"\n# coils / discrete are also available\n```\n\nTiming precedence is device → built-in defaults (3000/1000/1000 ms).\n\nAn **OCPP** device file (saved with `:write-device`) describes the charge point: its OCPP version,\nrole, reply timeout and the Lua simulation scripts. Endpoint (ip/port/protocol) is per-instance and\nlives in the session, not here.\n\n```toml\nversion = \"0.4.4\"        # ferrowl version, stamped on save\nocpp_version = \"1.6\"     # or \"2.0.1\" or \"2.1\"\nrole = \"client\"          # client = charging station, server = management system\ntimeout_ms = 30000       # awaited-reply timeout\n\n[[scripts]]\nname = \"ramp\"\nenabled = true\ncode = \"C_OCPP:Set(\\\"Power\\\", C_OCPP:Get(\\\"Power\\\") + 100)\"\n```\n\n## Lua Support\n\nAs an additional feature, the tool also includes a Lua runtime to execute custom scripts that drive a simulation. For **Modbus** modules scripts are attached to the device config as a global, toggleable list managed from the `:scripts` dialog (legacy per-register `update` snippets are migrated into it on load); all enabled scripts run each simulation cycle, interacting with the registers through `C_Register` and able to print to the module log via `C_Log`. For **OCPP** modules — in both the Charging Station (client) and CSMS (server) roles — scripts are attached to the device config and managed from the *Lua Scripts* dialog (the button under the state table); all enabled scripts run about once per second and interact with the OCPP state and actions through `C_OCPP`, and may print to the module log via `C_Log`. Besides the standard Lua libraries, the exposed modules are `C_Time` and `C_Log` (both), `C_Register` (Modbus only), and `C_OCPP` (OCPP only).\n\n### Module C_Time\n\n```\nMethod:   C_Time:Get()\n\nArguments: None\n\nReturn: Time in seconds since startup.\n```\n```\nMethod:   C_Time:GetMs()\n\nArguments: None\n\nReturn: Time in milliseconds since startup.\n```\n\n### Module C_Log\n\n```\nMethod:   C_Log:Print(message)\n\nArguments:\n               Name: message\n               Type: String\n        Description: A line to append to the module's log (the on-screen log\n                     pane, and the file sink when `:log \u003cfile\u003e` is active).\n\nReturn: nil\n```\n\n### Module C_Register\n\n```\nMethod:   C_Register:Get(name)\n\nArguments:\n               Name: name\n               Type: String\n        Description: Name of the register as defined in the configuration.\n\nReturn: Value of the register, typed to match it: a number for integer and\n        floating-point registers, a string for strings and a boolean for booleans.\n```\n```\nMethod:   C_Register:Set(name, value)\n\nArguments:\n               Name: name\n               Type: String\n        Description: Name of the register as defined in the configuration.\n\n               Name: value\n               Type: String | bool | integer | float\n        Description: Value to set for the specified register\n\nReturn: nil\n```\n\n### Module C_OCPP\n\nExposed to the Lua scripts of an **OCPP** module, in both the charging-station (client) and CSMS\n(server) roles. All loaded, enabled scripts run about once per second. `C_Time` is also available;\n`C_Register` is **not** (it is Modbus-only).\n\nThe module has a flat surface plus role-specific scope accessors:\n\n- **Client** — bare `Get`/`Set`/`\u003cAction\u003e` address the charging station itself; `Connector(id)`\n  returns an accessor scoped to one connector with the same `Get`/`Set`/`\u003cAction\u003e` surface.\n- **Server (CSMS)** — `GetChargingStations()` and `GetConnectors(cs)` enumerate the connected\n  stations and their connectors; `ChargingStation(cs)` and `Connector(cs, id)` return accessors\n  scoped to one station or one of its connectors.\n\n`Get`/`Set` read and write the addressed scope's state by name. Supported names (compact forms of\nthe state-table labels):\n\n```\nConnectorId, Phases, Voltage, Current (= CurrentL1), CurrentL1, CurrentL2, CurrentL3,\nPower, TotalEnergy, SessionEnergy, Status, Rfid, Model, Vendor\n```\n\nOCPP 2.0.1 additionally exposes `EvseId`.\n\n```\nMethod:   C_OCPP:Get(name)\n\nArguments:\n               Name: name\n               Type: String\n        Description: State field name (see the list above).\n\nReturn: Value of the field — a number for numeric fields, a string for textual ones — or an\n        error for an unknown name.\n```\n```\nMethod:   C_OCPP:Set(name, value)\n\nArguments:\n               Name: name\n               Type: String\n        Description: State field name. Numeric fields accept an integer or float; textual\n                     fields accept a string.\n\n               Name: value\n               Type: integer | float | string\n\nReturn: nil (errors on an unknown name or a type mismatch).\n```\n\nIn addition, every supported OCPP action is callable as `C_OCPP:\u003cAction\u003e(overrides?)`. The set of\nactions is version-specific (OCPP 1.6, 2.0.1 and 2.1 differ), so a script must match the device's OCPP\nversion. The action's payload is built from the current state exactly like the on-screen action\nbuttons; an optional table of overrides is shallow-merged over it. The call returns `true` once the\naction is queued, or `false` on an argument error. The result of the exchange with the CSMS appears\nin the module's Messages table, not in the return value.\n\n```\nMethod:   C_OCPP:\u003cAction\u003e(overrides?)\n          e.g. C_OCPP:Authorize(), C_OCPP:StartTransaction(), C_OCPP:MeterValues()\n               C_OCPP:BootNotification({ chargePointModel = \"Custom\" })\n\nArguments:\n               Name: overrides\n               Type: table (optional)\n        Description: Key/value fields shallow-merged over the state-derived payload.\n\nReturn: true when the action was queued, false on an argument error.\n```\n\n#### Scope accessors\n\nThe same `Get`/`Set`/`\u003cAction\u003e` surface is reachable on a narrower scope. On the **client** a\nconnector accessor is obtained by id; on the **server** the connected stations and their\nconnectors are enumerated and then addressed by id.\n\n```\nMethod:   C_OCPP:Connector(id)                 -- client role\nReturn:   accessor scoped to connector `id`, exposing Get/Set/\u003cAction\u003e.\n\nMethod:   C_OCPP:GetChargingStations()         -- server role\nReturn:   list of connected charging-station ids.\n\nMethod:   C_OCPP:GetConnectors(cs)             -- server role\nReturn:   list of connector ids seen for station `cs`.\n\nMethod:   C_OCPP:ChargingStation(cs)           -- server role\nReturn:   accessor scoped to station `cs` (or nil if unknown), exposing Get/Set/\u003cAction\u003e.\n\nMethod:   C_OCPP:Connector(cs, id)             -- server role\nReturn:   accessor scoped to connector `id` of station `cs` (or nil if unknown),\n          exposing Get/Set/\u003cAction\u003e.\n```\n\n#### Example\n\n```lua\n-- Ramp the charging current while a transaction is running, then report it.\nlocal target = 16.0\nlocal current = C_OCPP:Get(\"CurrentL1\")\nif current \u003c target then\n    C_OCPP:Set(\"CurrentL1\", current + 0.5)\n    C_OCPP:Set(\"CurrentL2\", current + 0.5)\n    C_OCPP:Set(\"CurrentL3\", current + 0.5)\n    C_OCPP:MeterValues()\nend\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftumbleowlee%2Fferrowl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftumbleowlee%2Fferrowl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftumbleowlee%2Fferrowl/lists"}