Skip to content

Latest commit

 

History

History
372 lines (291 loc) · 13.9 KB

File metadata and controls

372 lines (291 loc) · 13.9 KB

AGENTS.md – LD-Lab

This file provides guidance for AI agents (GitHub Copilot, Cursor, Claude, etc.) and new contributors working on the LD-Lab project.


Project Overview

LD-Lab is a desktop Ladder Diagram (LD) simulator for learning, testing, and analyzing PLC programming logic — without requiring physical hardware.

  • Organization: KN Algo @ Wrocław University of Science and Technology (PWr)
  • License: GPLv3
  • Status: Active development

Architecture

LD-Lab uses a hybrid desktop architecture:

┌─────────────────────────────────────────────┐
│              Desktop Window                 │
│  ┌───────────────────────────────────────┐  │
│  │             WebView                   │  │
│  │   React 19 + TypeScript + Tailwind    │  │
│  └──────────────┬──────────────┬─────────┘  │
│                 │ Saucer RPC   │ Binary     │
│                 │ (expose/call)│ Streaming  │
│  ┌──────────────┴──────────────┴─────────┐  │
│  │         C++23 Backend                 │  │
│  │   VariableTable · ApiRegistry · ...   │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

Key components:

Layer Technology
Desktop shell Saucer v8.0.4 (WebView2 on Windows)
Backend language C++23, CMake ≥ 3.25
Frontend language TypeScript 5.9, React 19, Vite 8
UI library Tailwind CSS v4 + shadcn/ui (new-york) + Radix UI
Icons Lucide React
Routing React Router 7
Dependency manager (C++) CPM (CMake Package Manager)

Backend ↔ Frontend Communication

There are two communication channels:

  1. Saucer RPC – request/response calls from the frontend to C++ functions, registered with webview.expose(...). Used for one-off operations (e.g., create variable, get value).
  2. Binary Streaming – C++ pushes real-time variable updates to the frontend via Base64-encoded binary frames. The frontend receives them via window.binaryUpdate(data). Used for real-time signal monitoring.

Repository Structure

LD-Lab/
├── CMakeLists.txt              # Main build config (Saucer, C++23, CPM)
├── AGENTS.md                   # This file
├── README.md
├── src/
│   ├── main.cpp                # Entry point: window, webview, streaming setup
│   └── api/
│       ├── ApiRegistry.cpp     # Registers all API modules with the webview
│       ├── VariableTable.cpp   # Thread-safe variable store (singleton)
│       ├── VariableInitializer.cpp  # Demo variable setup
│       ├── VariableUpdater.cpp # Background service: periodically updates variables
│       ├── examples/           # Example API modules (Greeter, Adder)
│       └── streaming/          # Binary streaming engine
│           ├── BatchQueue.cpp
│           ├── BinaryProtocol.cpp
│           ├── DeltaTracker.cpp
│           └── StreamingApi.cpp
├── include/
│   └── api/
│       ├── ApiRegistry.h
│       ├── VariableTable.h
│       ├── VariableInitializer.h
│       ├── VariableUpdater.h
│       ├── examples/
│       └── streaming/
│           ├── BatchQueue.h
│           ├── BinaryProtocol.h
│           ├── DeltaTracker.h
│           └── StreamingApi.h
├── frontend/
│   ├── src/
│   │   ├── main.tsx
│   │   ├── App.tsx
│   │   ├── context/
│   │   │   └── VariableContext.tsx  # React context for variable state
│   │   ├── features/
│   │   │   └── cpp-api/
│   │   │       └── api/            # Hooks wrapping C++ API calls
│   │   │           ├── index.ts    # Barrel export
│   │   │           ├── use-greeter.ts
│   │   │           ├── use-variable-control.ts
│   │   │           ├── use-variable-push.ts
│   │   │           └── use-variable-subscription.ts
│   │   └── lib/
│   │       └── api-client.ts       # Singleton Saucer RPC client
│   ├── package.json
│   ├── vite.config.ts
│   └── components.json             # shadcn/ui config
├── docs/
│   ├── Project-Setup/README.md     # Dev environment setup guide
│   └── Creating-API/README.md      # How to add new API modules
├── build/                          # CMake output — DO NOT commit
└── embedded/                       # Auto-generated by `saucer_embed` — DO NOT commit

Development Setup

Prerequisites

Windows:

  • Visual Studio 2022/2026 with Desktop development with C++ workload (MSVC v143)
  • Node.js ≥ 24
  • CMake ≥ 3.25
  • VS Code with extensions: CMake Tools and C/C++

Linux/macOS:

  • G++ ≥ 14 or Clang ≥ 17
  • Node.js ≥ 24
  • CMake ≥ 3.25
  • VS Code with extensions: CMake Tools and C/C++

Running in Development Mode (Hot Reload)

Frontend changes are reflected instantly. C++ changes require a rebuild.

Step 1 – start the frontend dev server:

cd frontend
npm install
npm run build   # required first time only
npm run dev     # starts Vite at http://localhost:5173

Step 2 – build and run the C++ app:

  1. Open the root folder in VS Code.
  2. Ctrl+Shift+PCMake: Select VariantDebug
  3. Ctrl+Shift+PCMake: Configure
  4. Press F7 to build.
  5. Press Shift+F5 to run.

The app window loads http://localhost:5173. Press F12 for DevTools.

Building for Production

# Step 1: build the frontend
cd frontend
npm run build       # outputs to frontend/dist/

# Step 2: rebuild C++ in Release mode
# In VS Code: CMake: Select Variant → Release
# → CMake: Configure → CMake: Build

The output binary LD-Lab (or LD-Lab.exe) will be in build/. It is fully self-contained — no Node.js or separate server required.


Core Backend Concepts

VariableTable

A thread-safe singleton (include/api/VariableTable.h) that stores all simulation variables.

  • Supports types: BOOL, INT, FLOAT
  • Values are std::variant<bool, int, float>
  • Observer pattern: register callbacks with subscribe() — called on every value change
  • Use VariableTable::getInstance() to access

ApiRegistry

src/api/ApiRegistry.cpp calls registerApi() on all modules. Any new API module must be registered here.

Streaming Pipeline

VariableTable change → DeltaTracker (detect changes) → BatchQueue (buffer)
                     → flush every N ms → BinaryEncoder → Base64
                     → webview.execute("window.binaryUpdate(...)")
                     → frontend decodes binary frame → React state update

Binary frame format (BinaryProtocol.h):

  • Header (8 bytes): MessageType | reserved | count (u16) | timestamp (u32)
  • Entries (variable length): [1-byte name length][name][type byte][8-byte double value]

How to Add a New API Module

Full guide: docs/Creating-API/README.md

Backend (C++)

  1. Create a header at include/api/<module>/MyModule.h:
#pragma once
#include <saucer/smartview.hpp>

class MyModule {
public:
    static void registerApi(saucer::smartview& webview);
};
  1. Create an implementation at src/api/<module>/MyModule.cpp:
#include "api/<module>/MyModule.h"

void MyModule::registerApi(saucer::smartview& webview) {
    webview.expose("my_function", [](int value) {
        return value * 2;
    });
}
  1. Register in src/api/ApiRegistry.cpp:
#include "api/<module>/MyModule.h"

void api::registerAll(saucer::smartview& webview) {
    // ...existing modules...
    MyModule::registerApi(webview);
}

Frontend (TypeScript/React)

  1. Create a hook at frontend/src/features/cpp-api/api/use-my-function.ts:
import { useState, useCallback } from "react";
import { apiClient } from "@/lib/api-client";

export const useMyFunction = () => {
  const [result, setResult] = useState<number | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const call = useCallback(async (value: number) => {
    setLoading(true);
    setError(null);
    try {
      const res = await apiClient.call<number>("my_function", [value]);
      setResult(res);
      return res;
    } catch (err) {
      setError(err instanceof Error ? err.message : "Unknown error");
      throw err;
    } finally {
      setLoading(false);
    }
  }, []);

  return { result, loading, error, call };
};
  1. Export from the barrel frontend/src/features/cpp-api/api/index.ts:
export * from "./use-my-function";

Code Conventions

C++

  • Standard: C++23 (std::print, std::expected, coroutines, etc.)
  • Namespaces: snake_case lowercase (e.g., api::, streaming::)
  • Classes: PascalCase
  • Methods/functions: camelCase
  • Headers in include/, implementations in src/ — mirror the same subfolder structure
  • All public APIs must have a registerApi(saucer::smartview&) static method
  • Doxygen-style comments on public methods

TypeScript/React

  • Frontend follows bulletproof-react conventions
  • Feature code lives in features/, shared utilities in lib/
  • API hooks always expose { result, loading, error, call } (or similar) — never raw promises from components
  • Use the apiClient singleton from @/lib/api-client — never call window.__saucer__ or similar directly
  • Component files: PascalCase (MyComponent.tsx)
  • Hook files: kebab-case prefixed with use- (use-my-function.ts)
  • Path alias @/ maps to frontend/src/
  • shadcn/ui components go in frontend/src/components/ui/

Git

  • Branch naming: feature/<description> or fix/<description> (kebab-case)
  • Never commit directly to main — all changes go through a branch + PR
  • Commit messages: follow Conventional Commits:
    • feat: – new feature
    • fix: – bug fix
    • docs: – documentation only
    • chore: – build, tooling, dependencies
    • refactor: – code change that is neither a fix nor a feature
    • style: – formatting, missing semicolons, etc.
    • test: – adding or updating tests

Files and Areas That Must NOT Be Modified Without Explicit Request

File / Area Reason
CMakeLists.txt Changing build configuration can break the entire build
build/ Auto-generated CMake output — never edit manually
embedded/ Auto-generated by saucer_embed — never edit manually
frontend/dist/ Build output — generated by npm run build
Dependency versions in package.json or FetchContent tags Version changes may break API compatibility
CI/CD configuration files Changes affect automated pipelines

Rules for AI Agents

MUST follow

  • Match the existing code style and naming conventions in the file being edited.
  • Place new API modules in the correct include/api/ + src/api/ structure.
  • Export all new frontend hooks through features/cpp-api/api/index.ts.
  • Respect the thread-safety model: always access VariableTable through its public API, never directly touch internal members.
  • After modifying C++ headers included by multiple .cpp files, note that a CMake rebuild is required.

MUST NOT do

  • Commit directly to main — always work on a feature/ or fix/ branch.
  • Modify CMakeLists.txt, build configs, or CI/CD files without an explicit request.
  • Change dependency versions (npm or C++ via FetchContent) without a clear reason and user confirmation.
  • Introduce breaking changes (renamed/removed API endpoints, changed wire protocol) without providing a migration path and updating documentation.
  • Add new libraries unless absolutely necessary — justify the addition and check for security surface area.
  • Refactor unrelated code while implementing a feature — solve the stated problem only.
  • Embed secrets, API keys, passwords, or tokens in source code.
  • Ignore existing conventions (naming, folder structure, hook patterns).
  • Make architectural decisions unilaterally — propose and wait for confirmation.
  • Hide or obscure changes — every modification must be clearly described and reviewable in the diff.

Safety rule

If a change could affect the build, production environment, dependencies, security, or backward compatibility — stop and ask for confirmation before proceeding.


Useful Commands Reference

# Frontend
cd frontend
npm install          # install dependencies
npm run dev          # start dev server (http://localhost:5173)
npm run build        # compile to frontend/dist/
npm run lint         # run ESLint
npm run typecheck    # run TypeScript compiler without emitting

# CMake (from repo root, or use VS Code CMake Tools)
cmake -B build .     # configure
cmake --build build  # build (Debug by default)