Programowanie obiektowe w STone
Kompletny przewodnik

Niniejszy artykuł to zbiór zasad i konwencji programowania obiektowego w języku Structured Text (ST) w środowisku STone. Wszystkie opisane wzorce i praktyki bazują na moim wieloletnim doświadczeniu z produkcyjnymi bibliotekami przemysłowymi. Jako przykład przewodni wybrałem bibliotekę sterowania pompownią przemysłową — system zarządzający różnymi typami pomp (VFD, DOL, soft-start) z regulacją ciśnienia, przepływu i poziomu cieczy. Zasady mają jednak zastosowanie uniwersalne.


1. Struktura projektu STone

1.1. Plik projektu/biblioteki .stprj

Każdy projekt/biblioteka STone posiada plik XML .stprj, który definiuje metadane i strukturę kompilacji.

<SToneProject
  name="PumpStation_Lib_v.2.0.0 [Controller_LIB]"
  type="Library"
  version="1.2"
  projectVersion="2.0.0"
  author="Hubert Lepiarczyk"
  vendor="IceLAB"
  libname="PumpStation_Lib">
  <Files>
    <Folder path="Libs">...</Folder>
    <Folder path="Classes">...</Folder>
    <Folder path="Functions">...</Folder>
    <Folder path="Interfaces">...</Folder>
  </Files>
  <Library Ver="1">
    <Info Version="2.0.0" ID="..." Name="PumpStation_Lib">
      <Dependency ID="..." Name="PumpCore" Version="2.0.3.0" />
      <Dependency ID="..." Name="HydraulicCalc" Version="1.0.1" />
    </Info>
  </Library>
</SToneProject>

1.2. Organizacja katalogów

Dobrą praktyką jest podział kodu na katalogi odzwierciedlające elementy OOP:

KatalogZawartośćPrzykłady plików
Classes/Klasy (abstrakcyjne i finalne)BasePump.st, VFD_Pump.st, DOL_Pump.st
Interfaces/Interfejsy (kontrakty)IPump.st, IVariableSpeedPump.st
Functions/Funkcje globalneValidateModbusResults.st, CheckControllerID.st
Libs/Zależności zewnętrzne (.stlib)PumpCore_v.2.0.3.0.stlib

Konwencja: każda klasa/interfejs = osobny plik .st o nazwie identycznej z nazwą klasy/interfejsu.

1.3. Wersjonowanie bibliotek — LibVer.g.st

STone stosuje mechanizm preprocesora do zarządzania wersjami i namespace’ami:

{IF NOT DEF(__LIBRARYNAMESPACEANDVERSION__)}
    {DEFINE __LIBRARYNAMESPACEANDVERSION__}
    {DEFINE LibraryNamespaceAndVersion Libs.PumpStation_Lib_v2_0_0}
    {DEFINE LibraryName 'PumpStation_Lib'}
    {DEFINE LibraryVersion 'PumpStation_Lib_v2_0_0'}
    {DEFINE LibraryVersionMajor 2}
    {DEFINE LibraryVersionMinor 0}
    {DEFINE LibraryVersionBuild 0}
{ENDIF}

Kluczowe zasady:

  • Wersja jest częścią namespace’a (Libs.PumpStation_Lib_v2_0_0) — pozwala na współistnienie wielu wersji biblioteki
  • Include guard (__LIBRARYNAMESPACEANDVERSION__) zapobiega wielokrotnemu definiowaniu
  • Każdy plik .st zaczyna się od {INCLUDE '../LibVer.g.st'}
  • Makro LibraryNamespaceAndVersion jest używane jako nazwa NAMESPACE w każdym pliku

2. Namespace i dyrektywy preprocesora

2.1. Namespace

Każdy plik w bibliotece jest opakowany w NAMESPACE z makrem wersyjnym:

{INCLUDE '../LibVer.g.st'}

NAMESPACE LibraryNamespaceAndVersion
    USING System;
    USING System.IO;
    USING System.Math;
    USING Libs.PumpCore_v2_0_3_0;

    (* All logic goes here: classes, interfaces, functions, types *)

END_NAMESPACE

Konwencje:

  • USING importuje inne namespace’y — zawsze na początku, po otwarciu NAMESPACE
  • Zależne biblioteki importowane przez pełną ścieżkę z wersją: Libs.PumpCore_v2_0_3_0
  • Typy systemowe przez System.IO, System.Math

2.2. Kompilacja warunkowa {IF DEF ...}

STone wspiera dyrektywy preprocesora do kompilacji warunkowej. W praktyce intensywnie je wykorzystuję do obsługi wariantów sprzętowych:

{IF DEF (FEATURE_MODBUS_DRIVES)}
    (* VFD control via Modbus RTU *)
    USING Libs.PumpCore_v2_0_3_0;
{ELSE}
    (* Direct control: 0-10V analog outputs *)
    USING Libs.PumpCore_v1_0_0_0;
{ENDIF}
{IF DEF (FEATURE_ADVANCED_MOTORS)}
    (* Additional drive types: soft-start, DOL with current monitoring *)
{ENDIF}

Praktyczne zastosowania:

DefineEfekt
FEATURE_MODBUS_DRIVESWłącza sterowanie VFD przez Modbus — wymaga rozszerzonego API komunikacyjnego
FEATURE_ADVANCED_MOTORSWłącza pompy soft-start i DOL z rozszerzonym monitoringiem

Dzięki temu jeden codebase obsługuje wiele wariantów sprzętowych. Klasy mogą mieć dwie niezależne implementacje w tym samym pliku, przełączane w compile-time.

2.3. Regiony {REGION ... ENDREGION}

Regiony porządkują sekcje kodu wizualnie w IDE:

{REGION Setters}
    METHOD PUBLIC SetDefaultConfig
        (* ... *)
    END_METHOD

    METHOD PUBLIC SetCustomConfig
        (* ... *)
    END_METHOD
{ENDREGION}

{REGION Getters}
    METHOD PUBLIC GetMotorType : MOTOR_TYPE
        (* ... *)
    END_METHOD
{ENDREGION}

Zalecana konwencja regionów:

RegionZawartość
SettersMetody ustawiające parametry
GettersMetody zwracające wartości
Public / ProtectedPodregiony wewnątrz Setters/Getters grupujące po widoczności
Default paramsStałe domyślne
NAMED VALUESTypy wyliczeniowe z przypisanymi wartościami numerycznymi
ENUMERATORSStandardowe enumeratory
CONFIGURATIONStruktury konfiguracyjne
INFORMATIONStruktury informacyjne/statusowe
Conditional API callsBloki wywołań warunkowanych na feature-flags sprzętu

3. Typy danych

3.1. Named Values (enumeratory z wartościami)

STone pozwala na definiowanie enumeratorów z jawnie przypisanymi wartościami numerycznymi. Składnia:

TYPE
    (**Type description*)
    TYPE_NAME : BASE_TYPE(
        (**Value 1 description*)
        Value1 := 0,
        (**Value 2 description*)
        Value2 := 1,
        (**Value 3 description*)
        Value3 := 2
    );
END_TYPE

Przykład — wybór pompy:

(**Selection of which pump to control*)
PUMP_SEL : USINT(
    (**Control first pump*)
    Pump1 := 1,
    (**Control second pump*)
    Pump2 := 2,
    (**Control third pump*)
    Pump3 := 3
);

Przykład — stany maszyny sterującej:

(**Pump controller state machine*)
PUMP_STATE : USINT(
    (**No state*)
    None                := 0,
    (**Motor stopped*)
    Stopped             := 1,
    (**Emergency shutdown active*)
    EmergencyStop       := 2,
    (**Standby — waiting for demand*)
    StandBy             := 3,
    (**Ramp-up in progress*)
    RampUp              := 4,
    (**Pre-start system check*)
    SystemCheck      := 5,
    (**Waiting for conditions*)
    WaitConditions      := 6,
    (**PID regulation active*)
    Regulation          := 7,
    (**Low-flow protection active*)
    LowFlowProtection   := 8,
    (**Dry-run protection active*)
    DryRunProtection     := 9,
    (**Power-on self-test*)
    PowerOnTest          := 10,
    (**Waiting after regulation mode change*)
    WaitModeChange       := 11,
    (**Pump re-start sequence*)
    RestartSequence      := 12,
    (* ... protections ... *)
    (**Low inlet pressure protection*)
    LowInletPress       := 18,
    (**High outlet pressure protection*)
    HighOutletPress     := 19,
    (**Overload protection*)
    OverloadProtection   := 20,
    (**Over-temperature protection*)
    OverTemperature      := 21,
    (**External controller not ready*)
    ExtControllerNotReady := 100
);

Kluczowe cechy:

  • Typ bazowy (USINT, UINT, DINT) determinuje rozmiar w pamięci — optymalizacja zasobów PLC
  • Wartości nie muszą być ciągłe (np. PUMP_STATE ma luki: 12→18, 21→100)
  • Modyfikator INTERNAL ogranicza widoczność: TYPE INTERNAL — typ niedostępny spoza biblioteki
  • Dostęp przez kwalifikator: PUMP_STATE#Regulation, PUMP_SEL#Pump1

3.2. Enumeratory standardowe

Bez jawnych wartości — kompilator przypisuje je automatycznie od 0:

TYPE
    (**Motor / drive type*)
    MOTOR_TYPE: (
        (**Variable Frequency Drive*)
        VFD,
        (**External controller — e.g. standalone inverter*)
        ExternalController
        {IF DEF (FEATURE_ADVANCED_MOTORS)}
        ,
        (**Direct on-line motor, no speed control*)
        DOL_Pump,
        (**Soft-start motor controller*)
        SoftStart
        {ENDIF}
    );
END_TYPE

Zwróć uwagę na przecinek przed DOL_Pump wewnątrz {IF DEF} — jest to konieczne, aby zachować poprawną składnię niezależnie od stanu kompilacji warunkowej.

3.3. Struktury (STRUCT)

Struktury grupują powiązane dane. W praktyce stosuję intensywne zagnieżdżanie struktur:

(**Regulation parameters*)
REG_PARAMS : STRUCT
    (**Regulation type: pressure, flow, level...*)
    RegTyp                  : REG_TYPE(...) := REG_TYPE#Pressure;
    (**Enable pump in stand-by mode*)
    StandByEn               : BOOL := FALSE;
    (**Pump speed in stand-by*) {ATTRIBUTE UOM PERCENT}
    StandBySpeedPerc        : UINT(0..100) := 0;
    (**Speed at startup*) {ATTRIBUTE UOM PERCENT}
    StartSpeedRatio         : UINT(0..100) := 50;
    (**Delay before regulation starts*) {ATTRIBUTE UOM SECOND}
    RegStartDelay           : UINT(0..18000) := 6;
    (**Protection thresholds*)
    Protections             : PRESSURE_PROTECTION;
    (**PID parameters*)
    PID_Params              : PID_PARAMS;
END_STRUCT;

Konwencje struktur:

  • Każde pole ma wartość domyślną (:= ...)
  • Zakresy definiowane wprost: UINT(0..100), UINT(0..18000)
  • Zagnieżdżanie jako forma kompozycji — hierarchia danych

3.4. Tablice typowane

(**Polynomial coefficients for pressure sensor linearization*)
CFG_SENSOR_POLY: ARRAY[1..6] OF REAL := [
     0.00124,
    -0.03782,
     1.24560,
    -0.00415,
     0.09832,
    -0.00067
];

3.5. Atrybuty i metadane pól

STone wspiera atrybuty przypisywane do pól i metod:

(**Motor rated current*) {ATTRIBUTE UOM AMPERE}
RatedCurrent : UINT := 12;

(**High outlet pressure threshold*) {ATTRIBUTE UOM BAR} {METADATA MIN_VAL 0} {METADATA MAX_VAL 40}
HighPressThreshold : REAL := 10;

(**Motor impedance*) {ATTRIBUTE UOM OHM}
MotorImpedance : UINT := 40;
AtrybutZnaczenie
{ATTRIBUTE UOM ...}Jednostka miary (CELSIUS, BAR, PERCENT, AMPERE, OHM, RPM, SECOND, MINUTE, HERTZ, CUBICMETERPERHOUR)
{METADATA MIN_VAL ...}Minimalna wartość parametru
{METADATA MAX_VAL ...}Maksymalna wartość parametru

Te atrybuty mogą być odczytywane przez narzędzia IDE (IntelliSense) i systemy supervisory.

3.6. Stałe globalne VAR_GLOBAL CONSTANT

VAR_GLOBAL CONSTANT INTERNAL
    TARGET_CONTROLLER_ID            : UINT := 17;
    {REGION Default motor params}
    (**Default motor params*)
    DEFAULT_MAX_SPEED               : UINT := 1450;
    DEFAULT_MIN_SPEED               : UINT := 300;
    DEFAULT_RAMP_UP_TIME            : UINT := 10;
    DEFAULT_RAMP_DOWN_TIME          : UINT := 8;
    DEFAULT_RATED_CURRENT           : UINT := 12;
    DEFAULT_OVERLOAD_CURRENT        : UINT := 15;
    DEFAULT_RATED_FREQUENCY         : UINT := 50;
    DEFAULT_MIN_FLOW_SPEED          : UINT := 600;
    DEFAULT_STARTUP_DELAY           : UINT := 30;
    DEFAULT_DRY_RUN_DELAY           : UINT := 5;
    {ENDREGION}
END_VAR

Konwencje:

  • INTERNAL — stałe widoczne tylko wewnątrz biblioteki
  • Nazwy: UPPER_SNAKE_CASE z prefiksem tematycznym (np. DEFAULT_)
  • Wartości domyślne są źródłem prawdy dla parametrów fabrycznych

4. Interfejsy (INTERFACE)

4.1. Definicja interfejsu

Interfejs definiuje kontrakt — zestaw metod, które klasa musi zaimplementować:

(**Common interface defining the contract between the regulation logic
  and pump control implementations.
  *Exposes lifecycle control, configuration setters and status getters
  *that are implemented by concrete pump classes such as VFD, DOL or soft-start*)
INTERFACE IPump
    (**Executes one control cycle of the regulation logic*)
    METHOD Run
    END_METHOD

    (**Resets the internal control state and forces a clean restart*)
    METHOD Reset
    END_METHOD

    {REGION Setters}
    (**Sets whether battery-backed emergency stop is enabled*)
    METHOD SetEmergencyStop
        VAR_INPUT
            En_Backup : BOOL;
        END_VAR
    END_METHOD

    (**Sets regulation parameters*)
    METHOD SetRegParams
        VAR_IN_OUT CONSTANT
            RegParams : REG_PARAMS;
        END_VAR
    END_METHOD

    (**Sets sensor readings*)
    METHOD SetSensors
        VAR_INPUT
            {ATTRIBUTE UOM BAR}
            (**Inlet pressure*)
            PS1 : REAL;
            {ATTRIBUTE UOM CELSIUS}
            (**Motor temperature*)
            TS1 : REAL;
            {ATTRIBUTE UOM BAR}
            (**Outlet pressure*)
            PS2 : REAL;
            {ATTRIBUTE UOM CUBICMETERPERHOUR}
            (**Flow rate*)
            FS1 : REAL;
        END_VAR
        VAR_IN_OUT CONSTANT
            (**Sensor linearization coefficients*)
            SensorCoeff : CFG_SENSOR_POLY;
        END_VAR
    END_METHOD
    {ENDREGION}

    {REGION Getters}
    (**Returns TRUE if the pump is ready to accept a command*)
    METHOD GetReadyToCommand : BOOL
    END_METHOD

    {ATTRIBUTE UOM BAR}
    (**Returns the current outlet pressure*)
    METHOD GetOutletPressure : REAL
    END_METHOD

    (**Returns the current controller state*)
    METHOD GetState : PUMP_STATE
    END_METHOD

    (**Returns the alarm status structure*)
    METHOD GetAlarmStatus : ALARM_STATUS
    END_METHOD
    {ENDREGION}
END_INTERFACE

4.2. Dziedziczenie interfejsów (EXTENDS)

Interfejsy mogą rozszerzać inne interfejsy:

(**Specialized interface extending IPump for variable-speed implementations.
  *Adds the contract required to select the output channel
  *and to expose speed data meaningful only for VFD-driven motors*)
INTERFACE IVariableSpeedPump EXTENDS IPump

    {REGION Setters}
    (**Sets which output channel the implementation should address*)
    METHOD SetPumpChannel
        VAR_INPUT
            PumpSel : PUMP_SEL;
        END_VAR
    END_METHOD

    (**Sets the default factory configuration*)
    METHOD SetDefaultConfig
    END_METHOD
    {ENDREGION}

    {REGION Getters}
    (**Returns the current motor speed*) {ATTRIBUTE UOM RPM}
    METHOD GetMotorSpeed : UINT
    END_METHOD

    (**Returns the current speed as a percentage of rated speed*)
    {ATTRIBUTE UOM PERCENT}
    METHOD GetSpeedPercent : REAL
    END_METHOD

    (**Returns the complete motor data structure*)
    METHOD GetMotorData : T_MOTOR_DATA
    END_METHOD
    {ENDREGION}
END_INTERFACE

Konwencje interfejsów:

RegułaPrzykład
Prefix I w nazwieIPump, IVariableSpeedPump
Brak ciał metodTylko sygnatury
Grupowanie w regiony{REGION Setters}, {REGION Getters}
Atrybuty na metodach{ATTRIBUTE UOM BAR} przed METHOD
VAR_IN_OUT CONSTANT dla strukturPrzekazanie przez referencję bez kopii, read-only

4.3. Wzorzec VAR_IN_OUT CONSTANT

Kluczowy wzorzec optymalizacji pamięci PLC:

METHOD SetRegParams
    VAR_IN_OUT CONSTANT
        RegParams : REG_PARAMS;
    END_VAR
END_METHOD
  • VAR_IN_OUT — przekazanie przez referencję (brak kopiowania dużych struktur)
  • CONSTANT — zabezpieczenie przed modyfikacją wewnątrz metody
  • Idealny do przekazywania dużych struktur konfiguracyjnych do setterów

5. Klasy (CLASS)

5.1. Hierarchia klas

IPump (interface)
├── IVariableSpeedPump (interface, extends IPump)
├── BasePump (abstract, implements IPump)
│   ├── BaseVariableSpeedPump (abstract, extends BasePump, implements IVariableSpeedPump)
│   │   ├── VFD_Pump (final) — analog output control
│   │   ├── VFD_PumpModbus (final) — Modbus RTU control
│   │   └── SoftStartPump (final)
│   ├── ExternalController (final) — standalone inverter interface
│   └── DOL_Pump (final) — direct on-line, no speed control

5.2. Klasa abstrakcyjna — BasePump

CLASS ABSTRACT INTERNAL BasePump IMPLEMENTS IPump
    VAR PRIVATE
        CoreInit            : CORE_ControllerInit;
        CoreController      : CORE_Controller;
        CoreEmergency       : CORE_EmergencyMng;
        RefRegParams        : REF_TO REG_PARAMS;
        RefAdvRegParams     : REF_TO ADV_REG_PARAMS;
        Demand              : UINT;
        PS1, TS1, PS2, FS1  : REAL;
        PressureSetpoint    : REAL;
        ReadyToCmd          : BOOL;
    END_VAR

    VAR PROTECTED
        ControllerParams   : CONTROLLER_DATA_PARAMS;
        ControllerVars     : CONTROLLER_DATA_VARS;
        Motor           : T_MOTOR_DATA;
        AlarmStatus     : ALARM_STATUS;
        IsConfigChanged : BOOL;
    END_VAR

    (*Extension point for derived classes*)
    METHOD ABSTRACT PROTECTED OnRun
    END_METHOD

    (**Resets internal logic, disables motor, clears alarms*)
    METHOD PUBLIC Reset
        VAR
            EmptyAlarmStatus : ALARM_STATUS;
        END_VAR
        THIS.Motor.Enable := 0;
        THIS.SetCanGo(FALSE);
        THIS.CoreProcessingPhase();
        THIS.UpdateReadyToCommand();
        THIS.UpdateAlarmStatus();
        (* ... clear regulation variables ... *)
        THIS.AlarmStatus := EmptyAlarmStatus;
        THIS.IsConfigChanged := FALSE;
    END_METHOD

    (**Executes a full regulation cycle*)
    METHOD PUBLIC Run
        THIS.ApplyConfiguration();
        IF THIS.IsConfigChanged THEN
            THIS.Reset();
            RETURN;
        END_IF;
        THIS.UpdateDemand();
        THIS.UpdateSensorsAndParams();
        THIS.CoreProcessingPhase();
        THIS.UpdateReadyToCommand();
        THIS.OnRun();  (*Abstract — implemented by subclass*)
        THIS.UpdateAlarmStatus();
    END_METHOD

    (* ... CoreProcessingPhase, WriteMotorData, ReadMotorData,
       UpdateDemand, UpdateReadyToCommand, UpdateAlarmStatus,
       UpdateSensorsAndParams — omitted for brevity ... *)

    {REGION Setters}
    METHOD PUBLIC SetRegParams
        VAR_IN_OUT CONSTANT
            RegParams : REG_PARAMS;
        END_VAR
        THIS.RefRegParams := REF(RegParams);
        THIS.ControllerParams.RegulationType := TO_BYTE(RegParams.RegTyp);
        THIS.SetPID_Params();
        THIS.SetEngineParams();
    END_METHOD

    METHOD PUBLIC SetAdvRegParams
        VAR_IN_OUT CONSTANT
            AdvRegParams : ADV_REG_PARAMS;
        END_VAR
        THIS.RefAdvRegParams := REF(AdvRegParams);
    END_METHOD

    (**Overloaded SetDemand — UINT version delegates to USINT version*)
    METHOD PUBLIC SetDemand
        VAR_INPUT
            Demand : UINT(0..100);
        END_VAR
        THIS.SetDemand(TO_USINT(Demand));
    END_METHOD

    METHOD PUBLIC SetDemand
        VAR_INPUT
            Demand : USINT(0..100);
        END_VAR
        THIS.Demand := TO_UINT(Demand);
    END_METHOD
    {ENDREGION}

    {REGION Getters}
    METHOD PUBLIC GetReadyToCommand : BOOL
        GetReadyToCommand := THIS.ReadyToCmd;
    END_METHOD

    METHOD PUBLIC GetAlarmCode : ALARM_CODE
        IF THIS.AlarmStatus.PowerFailure THEN
            GetAlarmCode := ALARM_CODE#PowerFailure;
        ELSIF THIS.AlarmStatus.DryRun THEN
            GetAlarmCode := ALARM_CODE#DryRun;
        ELSIF THIS.AlarmStatus.Overload THEN
            GetAlarmCode := ALARM_CODE#Overload;
        (* ... remaining priority checks ... *)
        ELSE
            GetAlarmCode := ALARM_CODE#None;
        END_IF;
    END_METHOD
    {ENDREGION}

    METHOD PROTECTED ApplyConfiguration
        THIS.ControllerVars.Engine_vars.Motor_vars.Enable := 1;
    END_METHOD

    METHOD PROTECTED SetCanGo
        VAR_INPUT
            Enable : BOOL;
        END_VAR
        THIS.ControllerParams.CanGo := Enable;
    END_METHOD
END_CLASS

Kluczowe wzorce:

  • ABSTRACT — nie można tworzyć instancji bezpośrednio
  • INTERNAL — klasa widoczna tylko wewnątrz biblioteki
  • IMPLEMENTS IPump — implementacja kontraktu interfejsu
  • METHOD ABSTRACT PROTECTED OnRun — metoda abstrakcyjna, punkt rozszerzenia (Template Method Pattern)
  • THIS. — jawne odwołanie do członków instancji

5.3. Modyfikatory widoczności zmiennych

VAR PRIVATE
    (*Accessible ONLY within this class*)
    CoreInit        : CORE_ControllerInit;
    RefRegParams    : REF_TO REG_PARAMS;
END_VAR

VAR PROTECTED
    (*Accessible in this class and its subclasses*)
    Motor           : T_MOTOR_DATA;
    AlarmStatus     : ALARM_STATUS;
END_VAR
ModyfikatorZakres widoczności
PRIVATETylko w danej klasie
PROTECTEDW klasie i jej podklasach
PUBLICWszędzie (domyślny dla metod interfejsu)
INTERNALWewnątrz biblioteki

Konwencja: zmienne instancji FB i referencje → PRIVATE; struktury współdzielone z podklasami → PROTECTED.

5.4. Klasa pośrednia — BaseVariableSpeedPump

CLASS INTERNAL ABSTRACT BaseVariableSpeedPump EXTENDS BasePump IMPLEMENTS IVariableSpeedPump
    VAR PROTECTED
        (**Identifies which output channel the controller should address*)
        PumpSel     : PUMP_SEL;
        (**Stores the current motor configuration variant*)
        MotorCfgVar : MOTOR_CFG_VARIANT;
    END_VAR

    (**Resets the complete internal control logic and clears estimated speed*)
    METHOD OVERRIDE PUBLIC Reset
        SUPER.Reset();
        THIS.Motor.CurrentSpeed := 0;
    END_METHOD

    {REGION Setters}
    (**Sets which output channel the controller should address*)
    METHOD PUBLIC SetPumpChannel
        VAR_INPUT
            PumpSel : PUMP_SEL;
        END_VAR
        THIS.PumpSel := PumpSel;
    END_METHOD
    {ENDREGION}

    {REGION Getters}
    (**Returns the current motor speed*) {ATTRIBUTE UOM RPM}
    METHOD PUBLIC GetMotorSpeed : UINT
        GetMotorSpeed := MIN(THIS.Motor.MaxSpeed, THIS.Motor.CurrentSpeed);
    END_METHOD

    {ATTRIBUTE UOM PERCENT}
    (**Returns the current speed as a percentage of max speed*)
    METHOD PUBLIC GetSpeedPercent : REAL
        IF THIS.Motor.MaxSpeed <> 0 THEN
            GetSpeedPercent := MIN(100,
                TO_REAL(THIS.Motor.CurrentSpeed) * 100
                / TO_REAL(THIS.Motor.MaxSpeed));
        END_IF;
    END_METHOD

    (**Returns the complete motor data structure*)
    METHOD PUBLIC GetMotorData : T_MOTOR_DATA
        GetMotorData := THIS.Motor;
    END_METHOD
    {ENDREGION}
END_CLASS

Wzorce:

  • EXTENDS BasePump IMPLEMENTS IVariableSpeedPump — dziedziczenie po jednej klasie + implementacja interfejsu
  • OVERRIDE — nadpisanie metody klasy bazowej
  • SUPER.Reset() — wywołanie implementacji rodzica

5.5. Klasa finalna — VFD_Pump

CLASS FINAL PUBLIC VFD_Pump EXTENDS BaseVariableSpeedPump
    VAR PRIVATE
        (**Manages VFD communication and speed commands*)
        VFD_Driver  : System.IO.ANALOG_OUT_DRV;
        (**Internal copy of VFD configuration used to detect parameter changes*)
        InternalCfg : CFG_VFD_MOTOR;
    END_VAR

    (**Sends speed command to the VFD analog output*)
    METHOD PRIVATE Driver
        THIS.VFD_Driver(
            ChannelNum := TO_BYTE(THIS.PumpSel),
            MotorData  := THIS.Motor
        );
    END_METHOD

    (**Executes the runtime sequence: write data, send to driver,
      *read back feedback, enable regulation*)
    METHOD PROTECTED OnRun
        THIS.WriteMotorData();
        THIS.Driver();
        THIS.ReadMotorData();
        THIS.SetCanGo(TRUE);
    END_METHOD

    (**Resets the driver channel and reinitializes internal control logic*)
    METHOD OVERRIDE PUBLIC Reset
        THIS.Driver();
        SUPER.Reset();
    END_METHOD

    (**Applies InternalCfg to controller params*)
    METHOD OVERRIDE PROTECTED ApplyConfiguration
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.MaxSpeed       := THIS.InternalCfg.MaxSpeed;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.MinSpeed       := THIS.InternalCfg.MinSpeed;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RampUpTime     := THIS.InternalCfg.RampUpTime;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RampDownTime   := THIS.InternalCfg.RampDownTime;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RatedCurrent   := THIS.InternalCfg.RatedCurrent;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.OverloadCurr   := THIS.InternalCfg.OverloadCurrent;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RatedFrequency := THIS.InternalCfg.RatedFrequency;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.DryRunDelay    := THIS.InternalCfg.DryRunDelay;

        SUPER.ApplyConfiguration();
    END_METHOD

    {REGION Setters}
    (**Sets the default factory configuration*)
    METHOD PUBLIC SetDefaultConfig
        VAR
            CfgDefault : CFG_VFD_MOTOR;  (*Initialized with struct defaults*)
        END_VAR
        THIS.IsConfigChanged := THIS.IsConfigChanged
                                OR THIS.MotorCfgVar <> MOTOR_CFG_VARIANT#Factory;
        THIS.MotorCfgVar := MOTOR_CFG_VARIANT#Factory;
        THIS.InternalCfg := CfgDefault;
    END_METHOD

    (**Sets user-defined configuration*)
    METHOD PUBLIC SetCustomConfig
        VAR_INPUT
            CfgCustom : CFG_VFD_MOTOR;
        END_VAR
        THIS.IsConfigChanged := THIS.IsConfigChanged
            OR (THIS.MotorCfgVar <> MOTOR_CFG_VARIANT#Custom
                OR THIS.InternalCfg <> CfgCustom);
        THIS.MotorCfgVar := MOTOR_CFG_VARIANT#Custom;
        THIS.InternalCfg := CfgCustom;
    END_METHOD
    {ENDREGION}

    {REGION Getters}
    (**Returns the motor type*)
    METHOD PUBLIC GetMotorType : MOTOR_TYPE
        GetMotorType := MOTOR_TYPE#VFD;
    END_METHOD

    {ATTRIBUTE UOM RPM}
    (**Returns the current motor speed*)
    METHOD OVERRIDE PUBLIC GetMotorSpeed : UINT
        GetMotorSpeed := SUPER.GetMotorSpeed();
    END_METHOD
    {ENDREGION}
END_CLASS

Kluczowe cechy klas finalnych:

  • FINAL — klasa nie może być dalej dziedziczona
  • PUBLIC — widoczna dla użytkowników biblioteki
  • Implementuje OnRun() — konkretna logika sterowania
  • OVERRIDE PROTECTED ApplyConfiguration — rozszerza bazową konfigurację, zawsze wywołuje SUPER.ApplyConfiguration()

6. Wzorce projektowe (Design Patterns)

6.1. Template Method Pattern

Najważniejszy wzorzec, który stosuję w bibliotekach sterowania. Klasa bazowa BasePump.Run() definiuje szkielet algorytmu, a klasy pochodne dostarczają implementację OnRun():

BasePump.Run():
  ┌─ ApplyConfiguration()        ← VIRTUAL (override w podklasach)
  ├─ Check IsConfigChanged
  ├─ UpdateDemand()               ← VIRTUAL
  ├─ UpdateSensorsAndParams()
  ├─ CoreProcessingPhase()
  ├─ UpdateReadyToCommand()       ← VIRTUAL
  ├─ OnRun()                      ← ABSTRACT (implementacja w podklasach)
  └─ UpdateAlarmStatus()          ← VIRTUAL

Każda klasa finalna implementuje OnRun() inaczej:

KlasaLogika OnRun()
VFD_PumpWriteMotorData → Driver(analog out) → ReadMotorData → CanGo
SoftStartPumpWriteMotorData → Driver(soft-start) → ReadMotorData → CanGo
VFD_PumpModbusWriteMotorData → Modbus calls (WriteSpeed, ReadFeedback…) → ReadMotorData → Error check
ExternalControllerSynchronizacja ze standalone-inverterem przez REF_TO T_MOTOR_DATA
DOL_PumpWriteMotorData → ReadMotorData → CanGo (direct on/off, no speed regulation)

6.2. Wzorzec detekcji zmiany konfiguracji

Każda klasa z konfiguracją implementuje wzorzec wykrywania zmian:

METHOD PUBLIC SetCustomConfig
    VAR_INPUT
        CfgCustom : CFG_VFD_MOTOR;
    END_VAR
    (*Detect change: variant changed OR parameters differ*)
    THIS.IsConfigChanged := THIS.IsConfigChanged
        OR (THIS.MotorCfgVar <> MOTOR_CFG_VARIANT#Custom
            OR THIS.InternalCfg <> CfgCustom);
    THIS.MotorCfgVar := MOTOR_CFG_VARIANT#Custom;
    THIS.InternalCfg := CfgCustom;
END_METHOD

Logika: flaga IsConfigChanged jest akumulowana (OR) — raz ustawiona, nie zostanie zresetowana aż do Reset(). Sprawdzana w Run() → jeśli TRUE, wykonuje Reset() i RETURN.

6.3. Wzorzec domyślnej konfiguracji przez zmienną lokalną

METHOD PUBLIC SetDefaultConfig
    VAR
        CfgDefault : CFG_VFD_MOTOR;  (*Struct with default values*)
    END_VAR
    THIS.InternalCfg := CfgDefault;
END_METHOD

Zmienna lokalna CfgDefault jest inicjalizowana wartościami domyślnymi ze struktury CFG_VFD_MOTOR. Nie trzeba ich ustawiać ręcznie — wystarczy przypisać świeżą instancję.

6.4. Wzorzec referencji dla dużych struktur

VAR PRIVATE
    RefRegParams    : REF_TO REG_PARAMS;
    RefAdvRegParams : REF_TO ADV_REG_PARAMS;
END_VAR

METHOD PUBLIC SetRegParams
    VAR_IN_OUT CONSTANT
        RegParams : REG_PARAMS;
    END_VAR
    THIS.RefRegParams := REF(RegParams);
    THIS.ControllerParams.RegulationType := TO_BYTE(RegParams.RegTyp);
    THIS.SetPID_Params();
    THIS.SetEngineParams();
END_METHOD

Zasada: duże struktury konfiguracyjne są przechowywane jako REF_TO — dane pozostają w jednym miejscu, klasa trzyma tylko wskaźnik. Walidacja NULL przed użyciem:

IF THIS.RefAdvRegParams <> NULL THEN
    (* ... use THIS.RefAdvRegParams^.FlowSetpoint ... *)
END_IF;

6.5. Wzorzec Getter/Setter

Konsekwentny wzorzec Get/Set:

(*SETTER — prefix Set*)
METHOD PUBLIC SetDemand
    VAR_INPUT
        Demand : UINT(0..100);
    END_VAR
    THIS.Demand := Demand;
END_METHOD

(*GETTER — Get prefix, return value via method name*)
METHOD PUBLIC GetReadyToCommand : BOOL
    GetReadyToCommand := THIS.ReadyToCmd;
END_METHOD

(*GETTER with logic — compute before returning*)
{ATTRIBUTE UOM PERCENT}
METHOD PUBLIC GetSpeedPercent : REAL
    IF THIS.Motor.MaxSpeed <> 0 THEN
        GetSpeedPercent := MIN(100,
            TO_REAL(THIS.Motor.CurrentSpeed) * 100
            / TO_REAL(THIS.Motor.MaxSpeed));
    END_IF;
END_METHOD

Kluczowe: wartość zwracana przez przypisanie do nazwy metody: GetXxx := wartość;

6.6. Przeciążanie metod (Method Overloading)

STone wspiera przeciążanie — ta sama nazwa metody, różne sygnatury:

(*UINT overload*)
METHOD PUBLIC SetDemand
    VAR_INPUT
        Demand : UINT(0..100);
    END_VAR
    THIS.SetDemand(TO_USINT(Demand));
END_METHOD

(*USINT overload*)
METHOD PUBLIC SetDemand
    VAR_INPUT
        Demand : USINT(0..100);
    END_VAR
    THIS.Demand := TO_UINT(Demand);
END_METHOD

Oraz przeciążanie z różnymi typami parametrów (na przykładzie obsługi błędów):

METHOD PRIVATE SetError
    VAR_INPUT
        ID   : ERROR_ID;
        Code : ERROR_CODE;
    END_VAR
    THIS.LastError.ID := ID;
    THIS.LastError.Code := Code;
END_METHOD

METHOD PRIVATE SetError
    VAR_INPUT
        Error : ERROR_INFO;
    END_VAR
    THIS.SetError(Error.ID, Error.Code);
END_METHOD

7. Modyfikatory klas i metod

7.1. Modyfikatory klas

ModyfikatorZnaczeniePrzykład
ABSTRACTNie można instancjonować; wymaga podklasBasePump, BaseVariableSpeedPump
FINALNie można dziedziczyćVFD_Pump, SoftStartPump, DOL_Pump
INTERNALWidoczna tylko wewnątrz bibliotekiBasePump, BaseVariableSpeedPump
PUBLICWidoczna dla użytkowników bibliotekiVFD_Pump, ExternalController

Stosowane kombinacje:

DeklaracjaRola
CLASS ABSTRACT INTERNALKlasa bazowa, ukryta przed użytkownikiem
CLASS FINAL PUBLICKlasa końcowa, API publiczne

7.2. Modyfikatory metod

ModyfikatorZnaczenie
ABSTRACTBrak ciała — musi być zaimplementowana w podklasie
OVERRIDENadpisuje metodę klasy bazowej
PUBLICDostępna z zewnątrz
PROTECTEDDostępna w klasie i podklasach
PRIVATEDostępna tylko w danej klasie

Przykład łańcucha override:

(*BasePump — base implementation*)
METHOD PROTECTED UpdateAlarmStatus
    THIS.AlarmStatus.PowerFailure :=
        THIS.Motor.Alarms = ALARM_POWER_SUPPLY_FAILURE;
END_METHOD

(*SoftStartPump — extends base: adds motor thermal alarm*)
METHOD OVERRIDE PROTECTED UpdateAlarmStatus
    SUPER.UpdateAlarmStatus();  (*Base logic first*)
    THIS.AlarmStatus.MotorOverTemp :=
        (THIS.MotorSoftStart.CommonData.Alarms <> 0)
        AND (THIS.MotorSoftStart.CommonData.Alarms <> ALARM_POWER_SUPPLY_FAILURE);
END_METHOD

8. Funkcje (FUNCTION)

Funkcje są samodzielne, bezstanowe i operują na danych wejściowych:

(**Checks array elements for error codes, returns first error found*)
FUNCTION INTERNAL ValidateModbusResults : ERROR_INFO
    VAR_IN_OUT CONSTANT
        Results : ARRAY [*] OF ErrorCode;  (*Variable-length array*)
    END_VAR
    VAR
        i : DINT;
    END_VAR

    FOR i := LOWER_BOUND(Results, 1) TO UPPER_BOUND(Results, 1) DO
        IF Results[i] <> ErrorCode#None THEN
            ValidateModbusResults.ID   := TO_USINT(i);
            ValidateModbusResults.Code := Results[i];
            RETURN;  (*Early return — first error found*)
        END_IF;
    END_FOR;
END_FUNCTION

Cechy:

  • ARRAY [*] — tablica o dynamicznym rozmiarze (variable-length array)
  • LOWER_BOUND() / UPPER_BOUND() — intrinsics do granic tablicy
  • Wartość zwracana przez przypisanie do nazwy funkcji: ValidateModbusResults.ID := ...
  • Dostęp do pól zwracanej struktury przez . na nazwie funkcji
  • RETURN jako early exit

Kolejny przykład — sprawdzanie kompatybilności sprzętowej:

(**Checks whether the current controller matches the expected hardware ID*)
FUNCTION INTERNAL CheckControllerID : BOOL
    VAR_INPUT
        ExpectedID : UINT;
    END_VAR
    VAR
        DevID       : UINT;
        BoardType   : UINT;
        MachineType : UINT;
        HwCode      : UINT;
    END_VAR

    GetModel(DevID, BoardType, MachineType, HwCode);
    CheckControllerID := DevID = ExpectedID;
END_FUNCTION

9. Komentarze i dokumentacja

9.1. Typy komentarzy

SkładniaCelWidoczność
(*komentarz*)Komentarz blokowy standardowyKod
(**komentarz*)Komentarz dokumentacyjny (IntelliSense)IDE + podpowiedzi
//komentarzKomentarz liniowyKod

9.2. Komentarze dokumentacyjne (**...*)

Każdy publiczny element powinien mieć komentarz (**...*) bezpośrednio przed deklaracją:

(**Abstract class implementing the IPump interface.
*It manages the complete regulation cycle of the pump, including:
*- controller initialization,
*- sensor acquisition,
*- regulation parameter management,
*- execution of the core algorithm,
*- data synchronization with the core,
*- alarm management,
*- system state evaluation.*)
CLASS ABSTRACT INTERNAL BasePump IMPLEMENTS IPump
(**Emergency stop current used when operating
  with reduced power from backup supply*)
EmergencyStopCurrent : UINT := 350;
(**Returns the current speed as a percentage of max speed*)
METHOD PUBLIC GetSpeedPercent : REAL

Konwencje dokumentacji:

  • Klasy: opis roli i odpowiedzialności (multi-line, z * na początku kontynuacji)
  • Metody: jeden wiersz opisujący akcję (zaczyna się od czasownika: Returns, Sets, Executes, Resets)
  • Zmienne: krótki opis przeznaczenia
  • Parametry VAR_INPUT / VAR_IN_OUT: opis każdego parametru

9.3. Komentarze operacyjne (*...*)

Wewnątrz ciał metod — wyjaśniają logikę:

(*Disable motor output to shut off the drive*)
THIS.Motor.Enable := 0;

(*Force a processing phase to return to initial state*)
THIS.CoreProcessingPhase();

(*Configuration change detected — Reset required
  to restart the control flow in a clean and consistent state*)
IF THIS.IsConfigChanged THEN
    THIS.Reset();
    RETURN;
END_IF;

10. Konwencje nazewnictwa

10.1. Podsumowanie

ElementKonwencjaPrzykłady
InterfejsI + PascalCaseIPump, IVariableSpeedPump
Klasa bazowaBase + PascalCaseBasePump, BaseVariableSpeedPump
Klasa finalnaPascalCase (bez prefixu)VFD_Pump, SoftStartPump, ExternalController
Metoda publicznaGet/Set + PascalCaseGetMotorSpeed, SetRegParams
Metoda chronionaPascalCaseOnRun, ApplyConfiguration, UpdateAlarmStatus
Metoda prywatnaPascalCaseDriver, CoreProcessingPhase
Zmienna lokalnaPascalCaseCfgDefault, InternalMotorType, ErrCode
Zmienna instancjiPascalCasePumpSel, IsConfigChanged, ReadyToCmd
Stała globalnaUPPER_SNAKE_CASEDEFAULT_MAX_SPEED, TARGET_CONTROLLER_ID
Typ (struct)UPPER_SNAKE_CASEREG_PARAMS, CFG_VFD_MOTOR, PRESSURE_PROTECTION
EnumeratorUPPER_SNAKE_CASEMOTOR_TYPE, REG_TYPE, MOTOR_CFG_VARIANT
Wartość enumeratoraPascalCaseVFD, ExternalController, Factory
Named valuePascalCasePump1, EmergencyStop, Regulation
Prefiks BoolIs/En/AlIsConfigChanged, En_Backup, Al_Present
ReferencjaRef + nazwaRefRegParams, RefAdvRegParams

10.2. Separatory w nazwach

W środowisku STone stosuję podkreślnik _ jako separator w złożonych nazwach technicznych:

  • PumpSel — Pump Selection
  • PID_Params — PID Parameters
  • HighPress_Ti — High Pressure Integral Time
  • AlarmStatus — Alarm Status
  • IsConfigChanged — z prefixem Is

11. Wzorce konwersji typów

STone wymaga jawnych konwersji typów za pomocą funkcji TO_xxx:

(*USINT → BYTE*)
ChannelNum := TO_BYTE(THIS.PumpSel)

(*ENUM → USINT for use in CASE*)
CASE TO_USINT(THIS.ControllerParams.RegulationType) OF
    REG_TYPE#Pressure, REG_TYPE#PressureCascade: ...
END_CASE;

(*UINT → REAL for floating-point arithmetic*)
SpeedPercent := TO_REAL(THIS.Motor.CurrentSpeed) * 100
                / TO_REAL(THIS.Motor.MaxSpeed);

(*ENUM → BYTE*)
THIS.ControllerParams.RegulationType := TO_BYTE(RegParams.RegTyp);

(*Loop index → USINT*)
ValidateModbusResults.ID := TO_USINT(i);

12. Obsługa błędów

12.1. Wzorzec sprawdzania wyników komunikacji

METHOD PROTECTED OnRun
    VAR
        MbResults : ARRAY [101..105] OF ErrorCode;
        RunError  : ERROR_INFO;
    END_VAR

    MbResults[101] := WriteFrequency(THIS.PumpSel, THIS.Motor.TargetFrequency);
    MbResults[102] := WriteSpeedRef(THIS.PumpSel, THIS.Motor.SpeedReference);
    MbResults[103] := WriteRampTime(THIS.PumpSel, THIS.Motor.RampTime);
    MbResults[104] := ReadActualSpeed(THIS.PumpSel, THIS.Motor.CurrentSpeed);
    MbResults[105] := ReadMotorCurrent(THIS.PumpSel, THIS.Motor.ActualCurrent);

    RunError := ValidateModbusResults(MbResults);

    THIS.ReadMotorData();

    IF RunError.Code = ERROR_CODE#None THEN
        THIS.SetCanGo(TRUE);
    END_IF;

    IF THIS.ControllerParams.CanGo THEN
        THIS.SetError(RunError);
    END_IF;
END_METHOD

12.2. Wzorzec kaskadowej walidacji

METHOD OVERRIDE PROTECTED ApplyConfiguration
    VAR
        ErrCode : ErrorCode;
    END_VAR

    IF THIS.ConfigRequired THEN
        {REGION Check hardware compatibility}
        ErrCode := GetDriverInfo(THIS.PumpSel, THIS.DriverInfo);

        IF NOT CheckControllerID(TARGET_CONTROLLER_ID) THEN
            THIS.SetError(ERROR_ID#HW, ERROR_CODE#NotCompatible);

        ELSIF ErrCode = ErrorCode#NotAvailable THEN
            THIS.SetError(ERROR_ID#OS, ERROR_CODE#NotCompatible);

        ELSIF ErrCode <> ErrorCode#None THEN
            THIS.SetError(ERROR_ID#GetDriverInfo, ErrCode);

        ELSIF NOT THIS.DriverInfo.AvailableFeatures.Bits.ModbusExtMode THEN
            THIS.SetError(ERROR_ID#FW, ERROR_CODE#NotCompatible);
        END_IF;

        IF THIS.LastError.ID <> ERROR_ID#None THEN
            RETURN;  (*Abort on error*)
        END_IF;
        {ENDREGION}

        {REGION Load configuration}
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.MaxSpeed       := THIS.InternalCfg.MaxSpeed;
        THIS.ControllerParams.Engine_params.MOTOR_PARAMS.RampUpTime     := THIS.InternalCfg.RampUpTime;
        (* ... *)
        SUPER.ApplyConfiguration();

        THIS.ApplyConfigurationToDriver();
        IF THIS.LastError.ID <> ERROR_ID#None THEN
            RETURN;
        END_IF;
        {ENDREGION}

        {REGION Enable operation}
        ErrCode := EnableOperation(THIS.PumpSel, TRUE);
        IF ErrCode <> ErrorCode#None THEN
            THIS.SetError(ERROR_ID#EnableOperation, ErrCode);
            RETURN;
        END_IF;
        {ENDREGION}

        THIS.ConfigRequired := FALSE;
    END_IF;
END_METHOD

Wzorzec: seria kroków z RETURN po każdym potencjalnym błędzie — fail-fast bez zagnieżdżonych IF-ów.

12.3. Warunkowe wywołania API

IF THIS.DriverInfo.AvailableFeatures.Bits.RampControl THEN
    MbResults[5] := WriteRampUpTime(THIS.PumpSel,
                                     THIS.InternalCfg.RampUpTime);
END_IF;

IF THIS.DriverInfo.AvailableFeatures.Bits.CurrentMonitoring THEN
    MbResults[6] := WriteOverloadThreshold(THIS.PumpSel,
                                            THIS.InternalCfg.OverloadCurrent);
END_IF;

Zasada: sprawdzaj feature-flags drivera/falownika przed wywołaniem opcjonalnych poleceń — dzięki temu kod działa z różnymi modelami i wersjami firmware.


13. Podsumowanie najlepszych praktyk

Architektura

  1. Jeden plik = jeden element (klasa, interfejs, funkcja)
  2. Interfejsy jako kontrakty — definiuj INTERFACE przed implementacją
  3. Hierarchia: abstrakcyjna baza → klasy finalne — użytkownik widzi tylko FINAL PUBLIC
  4. Ukrywaj implementację — klasy bazowe jako INTERNAL
  5. Template Method PatternMETHOD ABSTRACT PROTECTED OnRun jako metoda abstrakcyjna i punkt rozszerzenia

Pamięć i wydajność

  1. VAR_IN_OUT CONSTANT dla dużych struktur przekazywanych do metod
  2. REF_TO do przechowywania referencji — unikaj kopiowania
  3. Najmniejszy typ bazowyUSINT zamiast UINT gdy wystarczy zakres 0-255
  4. Zakresy typówUINT(0..100) zamiast gołego UINT

Konwencje

  1. Prefiksy: I (interfejs), Base (klasa abstrakcyjna), Get/Set (metody), Is/En (bool)
  2. Komentarze (**...*) (IntelliSense) na każdym publicznym elemencie
  3. {REGION} do organizacji kodu w IDE
  4. Kompilacja warunkowa {IF DEF} do obsługi wariantów sprzętowych
  5. Jawne konwersje typówTO_BYTE(), TO_REAL(), TO_USINT()

Bezpieczeństwo

  1. Walidacja NULL przed dereferencją REF_TO
  2. Fail-fast z RETURN zamiast głębokiego zagnieżdżania
  3. Detekcja zmiany konfiguracji — flaga IsConfigChanged + Reset()
  4. Dzielenie przez zero — sprawdzenie <> 0 przed dzieleniem

Autor: Hubert | Artykuł oparty na doświadczeniu z produkcyjnymi bibliotekami w środowisku STone.