Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
e0c7ae7
spec: Initial commit
ssweber Feb 5, 2026
7094c46
Add banks, addresses, and validation foundation modules
ssweber Feb 5, 2026
04b2b89
spec: Mark Step 2 as complete in HANDOFF.md
ssweber Feb 5, 2026
8d0e532
Add blocks, dataview, and nicknames modules
ssweber Feb 5, 2026
215378d
Fix _SPARSE_RANGES to match actual CLICK PLC X/Y hardware slots
ssweber Feb 5, 2026
f672524
Add modbus protocol mapping module
ssweber Feb 6, 2026
760a18f
Add ClickClient (async Modbus TCP driver) and ClickServer (Modbus TCP…
ssweber Feb 6, 2026
293f977
Fix _write_txt crash when writing empty string to TXT register
ssweber Feb 6, 2026
ea88432
Unify address parsing around MDB-style indices
ssweber Feb 7, 2026
0018b59
Separate dataview value conversion into data and display layers
ssweber Feb 7, 2026
d94ea7a
spec: light cleanup. Possible future ClickProject object?
ssweber Feb 7, 2026
a0185a6
Add CLAUDE.md and rewrite README with public API docs and usage examples
ssweber Feb 7, 2026
b461903
copy: clarify return values of ClickClient
ssweber Feb 7, 2026
185a9cc
fix: correct starting T address
ssweber Feb 7, 2026
9ae7123
feat: display_to_datatype for Txt returns a string
ssweber Feb 7, 2026
500439d
Create HANDOFF.md
ssweber Feb 7, 2026
bfe7cc1
feat: add ModbusResponse mapping with normalized address keys
ssweber Feb 9, 2026
c5ba2a7
refactor(dataview)!: make CDV type codes private via _CdvStorageCode
ssweber Feb 9, 2026
4cf9c93
Delete HANDOFF.md
ssweber Feb 10, 2026
5a971c3
refactor(types): add generic bank typing and make tests ty-compatible
ssweber Feb 10, 2026
eecf8e3
spec: Client/Server min max plan
ssweber Feb 10, 2026
0749337
feat: enforce strict runtime write validation in client and MemoryDat…
ssweber Feb 11, 2026
f380de0
feat(dataview): add CDV check helpers and tighten root API
ssweber Feb 11, 2026
a222f77
docs: refocus README on core workflows and add MkDocs API reference g…
ssweber Feb 11, 2026
502ca2e
feat: separate port arg
ssweber Feb 11, 2026
83fdba8
fix(modbus): correct packed TXT register reverse mapping in server paths
ssweber Feb 11, 2026
22ef73a
feat: Add `ClickHardwareProfile`
ssweber Feb 13, 2026
ce145df
refactor!: remove clicknick-specific MDB/CDV helper APIs from pyclickplc
ssweber Feb 13, 2026
83e444d
refactor(dataview): remove project-level CDV file scanning helpers
ssweber Feb 14, 2026
798dd34
lint: Fix literal typing in capability tables and tests
ssweber Feb 14, 2026
82366bb
refactor(dataview): unify on DataType and add New Value APIs
ssweber Feb 14, 2026
5b67704
Refactor CDV API to DataviewFile and remove load_cdv/save_cdv
ssweber Feb 15, 2026
f4ca095
feat(modbus): add synchronous ModbusService with polling and batched I/O
ssweber Feb 15, 2026
b3cd4e9
fix(modbus_service): fully stop service loop on disconnect; prevent c…
ssweber Feb 15, 2026
16fb6d1
feat(server): add ClickServer TUI and deterministic client disconnect…
ssweber Feb 15, 2026
f7e0936
feat(modbus-service): add reconnect config and poll-failure state tra…
ssweber Feb 15, 2026
e13155d
refactor!: remove Capabilities (pyrung only used code)
ssweber Feb 19, 2026
427381a
feat: normalize address/tag access across CSV and client APIs" -m "Ad…
ssweber Feb 21, 2026
b6d0cd1
feat(client)!: make XD/YD display-indexed and add xdu/ydu aliases
ssweber Feb 21, 2026
4f82420
feat(validation): add is_system flag to validate_nickname
ssweber Feb 22, 2026
bf05b0c
feat(validation): add is_system flag to validate_nickname
ssweber Feb 22, 2026
3d64686
Merge branch 'dev2' of https://github.com/ssweber/pyclickplc into dev2
ssweber Feb 22, 2026
73aeb56
docs: clean up API surface, fix ghost exports, add constructor docstr…
ssweber Feb 22, 2026
e4df14f
prep for initial release: fix metadata, refactor tag.read_all
ssweber Feb 22, 2026
0f90cd2
chore: remove non-needed spec and scratchpad
ssweber Feb 22, 2026
36ea7ed
copy: update description
ssweber Feb 22, 2026
d7588e3
fix: rename xdu/ydu -> xd0u/yd0u
ssweber Feb 23, 2026
86b1d08
fix: Rename Dataview -> DataView throughout
ssweber Feb 25, 2026
fcdda2a
feat(blocks): keep BlockTag names opaque and add opt-in structured na…
ssweber Feb 26, 2026
e1a465c
feat: Harden nickname validation: remove is_system and require explic…
ssweber Feb 26, 2026
9a0e34e
lint: replace server test ignores with typed casts
ssweber Feb 26, 2026
a5f69bc
docs: rewrite stab
ssweber Feb 27, 2026
d29c593
copy: add server links
ssweber Feb 27, 2026
a535e34
copy: update to use `read_csv` in quickstart
ssweber Feb 27, 2026
e112b85
docs: add handwritten for ClickClient
ssweber Feb 27, 2026
7a461f3
docs: enable llmstxt
ssweber Feb 27, 2026
b038e87
Merge pull request #1 from ssweber/dev-docs
ssweber Feb 27, 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
11 changes: 11 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"permissions": {
"allow": [
"Bash(dir:*)",
"Bash(findstr:*)",
"Bash(make:*)",
"Bash(uv sync:*)",
"WebFetch(domain:raw.githubusercontent.com)"
]
}
}
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

## 0.1.0 - 2026-02-26

### Added

- v0.1 documentation structure with capability-oriented API reference pages:
- Client API
- Service API
- Server API
- Files API
- Addressing API
- Validation API
- Advanced API
- New usage guides:
- `guides/types.md` for native Python value contracts
- `guides/addressing.md` for canonical normalized addressing and XD/YD details
- API reference coverage guard in `docs/gen_reference.py` to ensure all exported symbols in `pyclickplc.__all__` are documented exactly once.

### Changed

- README restructured for v0.1 launch:
- clearer async (`ClickClient`) vs sync (`ModbusService`) entry points
- native Python type contract table
- explicit error model (`ValueError` validation vs `OSError` transport)
- `XD0u` / `YD0u` moved out of quickstart to addressing guidance
- stability policy documented as "stable core, evolving edges"
- MkDocs navigation updated to capability-oriented API pages and new core guides.

### Notes

- Stable core APIs are documented in primary navigation.
- Lower-level Modbus mapping and bank metadata APIs are documented under Advanced API and may evolve more quickly.
69 changes: 69 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

`pyclickplc` is a shared utility library for AutomationDirect CLICK PLCs. It provides PLC bank definitions, address parsing, nickname CSV and DataView CDV file I/O, BlockTag parsing, Modbus protocol mapping, and async Modbus TCP client/server. Consumed by ClickNick (GUI editor), pyrung (simulation), and standalone tooling.

## Commands

- **Install**: `uv sync --all-extras --dev`
- **Run all (install + lint + test)**: `make` or `make default`
- **Test**: `make test` (runs `uv run pytest`)
- **Single test**: `uv run pytest tests/test_addresses.py::TestParseAddress::test_xd_basic -v`
- **Lint**: `make lint` (runs `uv run devtools/lint.py` — codespell, ruff check --fix, ruff format, ty check)
- **Build**: `make build` (runs `uv build`)

## Architecture

### Two-Layer Design

PLC knowledge and Modbus protocol are separated into two layers linked by bank name:

1. **`banks.py`** (foundation, zero deps) — `BankConfig`, `BANKS` dict, `DataType` enum, sparse `valid_ranges` for X/Y hardware slots, address validation
2. **`modbus.py`** (depends on banks) — `ModbusMapping`, `MODBUS_MAPPINGS` dict, forward/reverse address mapping, register pack/unpack

### Address System

All address parsing flows through `parse_address()` in `addresses.py`, which returns `(memory_type, mdb_address)` — MDB-style indices for all banks. XD/YD use contiguous MDB indices 0-16; display addresses 0-8 map via `xd_yd_display_to_mdb()`. The `format_address_display()` function handles the reverse. These two functions are the canonical formatting/parsing entry points used by client, server, nicknames, and dataview modules.

### Dependency Graph

```
banks.py ← no deps (foundation)
├── validation.py
├── addresses.py
│ ↑
│ ├── blocks.py
│ ├── dataview.py
│ ├── modbus.py
│ │ ↑
│ │ ├── client.py ──→ nicknames.py
│ │ └── server.py
│ └── nicknames.py ──→ blocks.py, validation.py
```

### Key Modules

- **`client.py`** — `ClickClient` (async Modbus TCP client using pymodbus) with `AddressAccessor`, `AddressInterface`, `TagInterface`
- **`server.py`** — `ClickServer` (async Modbus TCP server) with `DataProvider` protocol and `MemoryDataProvider`
- **`nicknames.py`** — Read/write CLICK nickname CSV files
- **`dataview.py`** — Read/write DataView CDV files (UTF-16 LE CSV format)
- **`blocks.py`** — BlockTag parsing and computation

### Modbus Mapping

`plc_to_modbus(bank, index)` uses `base + index` for 0-based banks (XD/YD) and `base + width * (index - 1)` for 1-based banks. X/Y are sparse coil banks with slot-based offset formulas. `modbus_to_plc()` reverses the mapping.

## Conventions

- `DataType` enum is the single source of truth for PLC types; Modbus properties derive from it
- All dataclasses use `frozen=True`
- Tests in `tests/` mirror source modules (e.g., `test_addresses.py` tests `addresses.py`)
- pytest discovers tests from both `src/` and `tests/` directories (`python_files = ["*.py"]`)
- `asyncio_mode = "auto"` — async tests need no decorator
- Package uses src-layout: source is in `src/pyclickplc/`
- Python 3.11+ required; CI tests 3.11-3.14
- Ruff line-length is 100
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

.DEFAULT_GOAL := default

.PHONY: default install lint test upgrade build clean docs docs-check
.PHONY: default install lint test upgrade build clean docs-serve docs-build docs-check

default: install lint test

Expand All @@ -23,6 +23,14 @@ upgrade:
build:
uv build

docs-serve:
uv run --group docs mkdocs serve

docs-build:
uv run --group docs mkdocs build --strict

docs-check: docs-build

# Improved Windows detection
ifeq ($(OS),Windows_NT)
WINDOWS := 1
Expand Down
135 changes: 128 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,133 @@
# Pyrung
# pyclickplc

Utilities to work with AutomationDirect CLICK Plcs
Utilities for AutomationDirect CLICK PLCs: Modbus TCP client/server, address helpers, nickname CSV I/O, and DataView CDV I/O.

## Status
Documentation: https://ssweber.github.io/pyclickplc/
LLM docs index: https://ssweber.github.io/pyclickplc/llms.txt
LLM full context: https://ssweber.github.io/pyclickplc/llms-full.txt

PLANNING ONLY * INITIAL COMMIT
## Installation

## Goals
```bash
uv add pyclickplc
# or
pip install pyclickplc
```

- Provide shared utility library for reading/writing Nickname csv files,
Dataview .cdv file, communicating via Modbus, and parsing the BlockTag comment specification
Requires Python 3.11+. Modbus client/server functionality depends on [pymodbus](https://github.com/pymodbus-dev/pymodbus).

## Choose Your Interface

- `ClickClient`: async API for direct `asyncio` applications.
- `ModbusService`: sync + polling API for UI/event-driven applications that do not want to manage an async loop.

## Native Python Type Contract

All read/write APIs operate on native Python values.

| Bank family | Python type | Examples |
| --- | --- | --- |
| `X`, `Y`, `C`, `T`, `CT`, `SC` | `bool` | `True`, `False` |
| `DS`, `DD`, `DH`, `TD`, `CTD`, `SD`, `XD`, `YD` | `int` | `42`, `0x1234` |
| `DF` | `float` | `3.14` |
| `TXT` | `str` | `"A"` |

`read()` methods return `ModbusResponse`, keyed by canonical normalized addresses (`"DS1"`, `"X001"`):

- lookups are normalized (`resp["ds1"]` resolves `"DS1"`)
- single index reads like `await plc.ds[1]` return a bare native Python value

## Quickstart (`ClickClient`, async)

```python
import asyncio
from pyclickplc import ClickClient, read_csv

tags = read_csv("nicknames.csv")

async def main():
async with ClickClient("192.168.1.10", 502, tags=tags) as plc:
# Bank accessors
await plc.ds.write(1, 100)
ds1 = await plc.ds[1]
df_values = await plc.df.read(1, 3)
await plc.y.write(1, True)

# String address interface
await plc.addr.write("df1", 3.14)
df1 = await plc.addr.read("DF1")

# Tag interface (case-insensitive)
await plc.tag.write("mytag", 42.0)
tag_value = await plc.tag.read("MyTag")

print(ds1, df_values, df1, tag_value)

asyncio.run(main())
```

## `ModbusService` (sync + polling)

```python
from pyclickplc import ModbusService, ReconnectConfig

def on_values(values):
print(values) # ModbusResponse keyed by canonical normalized addresses

svc = ModbusService(
poll_interval_s=0.5,
reconnect=ReconnectConfig(delay_s=0.5, max_delay_s=5.0),
on_values=on_values,
)
svc.connect("192.168.1.10", 502, device_id=1, timeout=1)

svc.set_poll_addresses(["DS1", "DF1", "Y1"])
print(svc.read(["DS1", "DF1"]))
print(svc.write({"DS1": 100, "Y1": True, "X1": True})) # X1 is not writable

svc.disconnect()
```

## Error Model

- Validation and address parsing failures raise `ValueError`.
- Transport/protocol failures raise `OSError` for reads.
- `ModbusService.write()` returns per-address outcomes (`ok` + `error`) instead of raising per-item validation failures.

## Addressing Nuances

- Address strings are canonical normalized (`x1` -> `X001`, `ds1` -> `DS1`).
- `X`/`Y` are sparse address families; not every numeric value is valid.
- `XD`/`YD` accessors are display-indexed (`0..8`) by default.
- `XD0u`/`YD0u` are explicit upper-byte aliases for advanced use cases.

See the addressing guide for details:
- https://ssweber.github.io/pyclickplc/guides/addressing/

## Other Features

- Modbus simulator: `ClickServer`, `MemoryDataProvider`, `run_server_tui`
- CLICK nickname CSV I/O: `read_csv`, `write_csv`, `AddressRecordMap`
- CLICK DataView CDV I/O: `read_cdv`, `write_cdv`, `verify_cdv`, `check_cdv_file`

Server docs:
- Guide: https://ssweber.github.io/pyclickplc/guides/server/
- API: https://ssweber.github.io/pyclickplc/reference/api/server/

## v0.1 API Stability

`pyclickplc` v0.1 follows a “stable core, evolving edges” policy.

- Stable core: client/service/server/file I/O/address/validation APIs surfaced in the docs site primary navigation.
- Advanced/evolving: low-level Modbus mapping helpers and bank metadata (`ModbusMapping`, `plc_to_modbus`, `BANKS`, etc.).

## Development

```bash
uv sync --all-extras --dev # Install dependencies
make test # Run tests (uv run pytest)
make lint # Lint (codespell, ruff, ty)
make docs-build # Build docs (mkdocs + mkdocstrings)
make docs-serve # Serve docs locally
make # All of the above
```
Loading