UniSet 2.45.1
Содержание директории st2js
Директория графа зависимостей st2js:
st2js

Директории

 
st2js
 
tests

Подробное описание

Конвертирует программы на языке Structured Text (ST) в JavaScript для исполнения на UniSet2 JScript (QuickJS). Поддерживает стандартные функциональные блоки IEC 61131-3, пользовательские типы данных, маппинг переменных на датчики UniSet через YAML-конфиг.

Быстрый старт

# Установка
cd extensions/JScript/tools/st2js
pip install -e .
# Конвертация
python -m st2js program.st -m mapping.yaml -o output.js
# Запуск на UniSet
uniset2-jscript --js-file output.js --confile configure.xml --js-name JSProxy1

Входные форматы

Расширение Формат Описание
.st, .txt Plain ST Текстовый файл Structured Text
.xml PLCopen XML Экспорт из Codesys / OpenPLC
.TcPOU TwinCAT XML Экспорт из TwinCAT / Beckhoff

Формат определяется автоматически по расширению файла.

Примеры

Простая программа

Исходный ST-код (program.st):

PROGRAM Main
VAR_INPUT
Temperature : REAL;
StartButton : BOOL;
END_VAR
VAR_OUTPUT
Heater : BOOL;
Alarm : BOOL;
END_VAR
VAR
state : INT := 0;
END_VAR
IF StartButton THEN
Heater := TRUE;
END_IF;
IF Temperature > 80.0 THEN
Alarm := TRUE;
END_IF;
END_PROGRAM

Маппинг датчиков (mapping.yaml):

inputs:
Temperature:
sensor: AI_Temp_S
type: REAL
scale: 100 # датчик целочисленный, делим на 100
StartButton:
sensor: DI_Start_S
outputs:
Heater:
sensor: DO_Heater_C
Alarm:
sensor: AO_Alarm_C
type: REAL
scale: 10

Результат (python -m st2js program.st -m mapping.yaml):

// Generated by st2js from Main
load("uniset2-iec61131.js");
uniset_inputs = [
{ name: "AI_Temp_S" },
{ name: "DI_Start_S" }
];
uniset_outputs = [
{ name: "DO_Heater_C" },
{ name: "AO_Alarm_C" }
];
// Local variables
let state = 0;
function uniset_on_step() {
let Temperature = in_AI_Temp_S / 100;
if (in_DI_Start_S) {
out_DO_Heater_C = true;
}
if ((Temperature > 80.0)) {
Alarm = true;
}
out_AO_Alarm_C = Math.round(Alarm * 10);
}

Функциональные блоки

ST-код с таймерами, счётчиками, детекторами фронтов и триггерами:

Исходный ST-код:

PROGRAM Main
VAR_INPUT
Enable : BOOL;
Pulse : BOOL;
ResetCmd : BOOL;
END_VAR
VAR_OUTPUT
Output : BOOL;
Count : INT;
END_VAR
VAR
myTimer : TON;
myCounter : CTU;
myTrigger : R_TRIG;
myLatch : RS;
END_VAR
myTimer(IN := Enable, PT := T#3000ms);
Output := myTimer.Q;
myCounter(CU := Pulse, RESET := ResetCmd, PV := 10);
Count := myCounter.CV;
myTrigger(CLK := Enable);
myLatch(SET := myTrigger.Q, RESET1 := ResetCmd);
END_PROGRAM

Результат:

// Generated by st2js from Main
load("uniset2-iec61131.js");
uniset_inputs = [
{ name: "DI_Enable_S" },
{ name: "DI_Pulse_S" },
{ name: "DI_Reset_S" }
];
uniset_outputs = [
{ name: "DO_Output_C" },
{ name: "AI_Count_C" }
];
// Function block instances
const myTimer = new TON(3000); // задержка включения 3с
const myCounter = new CTU(10); // счётчик до 10
const myTrigger = new R_TRIG(); // детектор нарастающего фронта
const myLatch = new RS(); // RS-триггер
function uniset_on_step() {
myTimer.update(in_DI_Enable_S);
out_DO_Output_C = myTimer.Q;
myCounter.update(in_DI_Pulse_S, in_DI_Reset_S);
out_AI_Count_C = myCounter.CV;
myTrigger.update(in_DI_Enable_S);
myLatch.update(myTrigger.Q, in_DI_Reset_S);
}

Полный пример: термостат

Исходный ST-код (thermostat.st):

TYPE TempConfig :
STRUCT
setpoint : REAL := 20.0;
hysteresis : REAL := 2.0;
maxTemp : REAL := 95.0;
END_STRUCT
END_TYPE
PROGRAM Thermostat
VAR_INPUT
CurrentTemp : REAL;
Enable : BOOL;
Reset : BOOL;
END_VAR
VAR_OUTPUT
HeaterOn : BOOL;
AlarmHigh : BOOL;
CycleCount : INT;
END_VAR
VAR
config : TempConfig;
onDelay : TON;
offDelay : TOF;
startEdge : R_TRIG;
cycleCounter : CTU;
state : INT := 0;
i : INT;
END_VAR
startEdge(CLK := Enable);
IF startEdge.Q THEN
state := 1;
END_IF;
CASE state OF
0: (* Idle *)
HeaterOn := FALSE;
1: (* Running *)
IF CurrentTemp < config.setpoint - config.hysteresis THEN
HeaterOn := TRUE;
ELSIF CurrentTemp > config.setpoint + config.hysteresis THEN
HeaterOn := FALSE;
END_IF;
onDelay(IN := HeaterOn, PT := T#5s);
cycleCounter(CU := onDelay.Q, RESET := Reset, PV := 100);
CycleCount := cycleCounter.CV;
2: (* Alarm *)
HeaterOn := FALSE;
END_CASE;
IF CurrentTemp > config.maxTemp THEN
AlarmHigh := TRUE;
state := 2;
END_IF;
IF Reset THEN
state := 0;
AlarmHigh := FALSE;
END_IF;
END_PROGRAM

Маппинг (thermostat_mapping.yaml):

inputs:
CurrentTemp:
sensor: AI_Temperature_S
type: REAL
scale: 100
Enable:
sensor: DI_Enable_S
Reset:
sensor: DI_Reset_S
outputs:
HeaterOn:
sensor: DO_Heater_C
AlarmHigh:
sensor: DO_AlarmHigh_C
CycleCount:
sensor: AO_CycleCount_C

Результат (python -m st2js thermostat.st -m thermostat_mapping.yaml):

// Generated by st2js from Thermostat
load("uniset2-iec61131.js");
uniset_inputs = [
{ name: "AI_Temperature_S" },
{ name: "DI_Enable_S" },
{ name: "DI_Reset_S" }
];
uniset_outputs = [
{ name: "DO_Heater_C" },
{ name: "DO_AlarmHigh_C" },
{ name: "AO_CycleCount_C" }
];
// Local variables
let config = { setpoint: 20.0, hysteresis: 2.0, maxTemp: 95.0 };
let state = 0;
let i = 0;
// Function block instances
const onDelay = new TON(0);
const offDelay = new TOF(0);
const startEdge = new R_TRIG();
const cycleCounter = new CTU(0);
function uniset_on_step() {
let CurrentTemp = in_AI_Temperature_S / 100;
startEdge.update(in_DI_Enable_S);
if (startEdge.Q) {
state = 1;
}
switch (state) {
case 0:
out_DO_Heater_C = false;
break;
case 1:
if ((CurrentTemp < (config.setpoint - config.hysteresis))) {
out_DO_Heater_C = true;
} else if ((CurrentTemp > (config.setpoint + config.hysteresis))) {
out_DO_Heater_C = false;
}
onDelay.update(out_DO_Heater_C);
cycleCounter.update(onDelay.Q, in_DI_Reset_S);
out_AO_CycleCount_C = cycleCounter.CV;
break;
case 2:
out_DO_Heater_C = false;
break;
}
if ((CurrentTemp > config.maxTemp)) {
out_DO_AlarmHigh_C = true;
state = 2;
}
if (in_DI_Reset_S) {
state = 0;
out_DO_AlarmHigh_C = false;
}
}

Маппинг датчиков (YAML)

Конфиг-файл связывает ST-переменные с датчиками UniSet:

inputs:
<ST_переменная>:
sensor: <имя_датчика_UniSet> # обязательное
type: REAL # опционально: IEC тип
scale: 100 # опционально: множитель для REAL
outputs:
<ST_переменная>:
sensor: <имя_датчика_UniSet>
type: REAL
scale: 10
options:
struct_flatten: false # true: поля структур → отдельные датчики
var_prefix: "" # префикс для датчиков, переменных, FB
func_prefix: "" # префикс для функций и классов FB

Scale factor

Датчики UniSet целочисленные (int64). Для REAL-переменных задаётся множитель:

Направление Формула Пример
Вход (sensor → ST) stVar = in_Sensor / scale Temperature = in_AI_Temp_S / 100
Выход (ST → sensor) out_Sensor = Math.round(stVar * scale) out_AO_Press_C = Math.round(Pressure * 10)

Struct flatten

При struct_flatten: true поля структуры маппятся на отдельные датчики:

inputs:
sensor.value:
sensor: AI_Value_S
sensor.valid:
sensor: DI_Valid_S
options:
struct_flatten: true

Префиксы для изоляции нескольких программ

Если несколько ST-программ конвертируются для одного JScript-процесса, нужно избежать конфликтов имён. Для этого каждой программе задаётся свой набор префиксов:

# mapping_plc1.yaml
inputs:
Temperature:
sensor: AI_Temp_S
scale: 100
outputs:
Heater:
sensor: DO_Heater_C
options:
var_prefix: "plc1_"
func_prefix: "plc1_"
# mapping_plc2.yaml
inputs:
Pressure:
sensor: AI_Press_S
outputs:
Valve:
sensor: DO_Valve_C
options:
var_prefix: "plc2_"
func_prefix: "plc2_"

Что получает prefix:

Элемент var_prefix func_prefix Пример
Имя датчика в uniset_inputs/outputs да { name: "plc1_AI_Temp_S" }
Ссылка in_/out_ в коде да in_plc1_AI_Temp_S
Локальная переменная да let plc1_state = 0;
FB instance да const plc1_myTimer = new TON(3000);
Secondary PROGRAM да const plc1_Alarms = (function() { ... })();
User FUNCTION_BLOCK class да class plc1_Sprite { ... }
uniset_on_step нет нет Всегда без prefix (системный callback)

Пример результата (python -m st2js program.st -m mapping_plc1.yaml):

load("uniset2-iec61131.js");
uniset_inputs = [
{ name: "plc1_AI_Temp_S" }
];
uniset_outputs = [
{ name: "plc1_DO_Heater_C" }
];
let plc1_state = 0;
const plc1_myTimer = new TON(3000);
function uniset_on_step() {
let Temperature = in_plc1_AI_Temp_S / 100;
plc1_myTimer.update((Temperature > 80.0));
if (plc1_myTimer.Q) {
out_plc1_DO_Heater_C = true;
}
}

Для объединения нескольких JS-файлов в один скрипт: массивы uniset_inputs/uniset_outputs нужно объединить вручную, а из uniset_on_step вызвать функции каждой программы.

Extra load modules

The mapping YAML can request extra load("...") calls in the generated file. Two insertion points:

  • load_head: — emitted near the top, right after the core runtime loads (uniset2-iec61131.js, optional codesys library) and before the sensor arrays. Use for helper libraries that project code depends on during parse.
  • load_on_start: — emitted inside the body of the generated uniset_on_start() function. Fires after the whole file has been parsed, so uniset_inputs, FB classes, and globals are already populated. Use for simulators, debug instrumentation, or post-processors.

Example:

load_head:
- helpers/utils.js
load_on_start:
- project_sim.js

Equivalent CLI flags (repeatable, appended to YAML):

st2js input.st --load-head helpers/utils.js --load-on-start project_sim.js

Entries are de-duplicated by exact string match (first occurrence wins) when global and per-file lists mention the same path.

Поддерживаемые конструкции ST

Управляющие структуры

ST JS
IF .. THEN .. ELSIF .. ELSE .. END_IF if (..) { .. } else if (..) { .. } else { .. }
CASE x OF 1: .. 2: .. ELSE .. END_CASE switch (x) { case 1: ..; break; .. default: ..; break; }
FOR i := 1 TO 10 BY 2 DO .. END_FOR for (let i = 1; i <= 10; i += 2) { .. }
WHILE cond DO .. END_WHILE while (cond) { .. }
REPEAT .. UNTIL cond END_REPEAT do { .. } while (!(cond))

Типы данных

IEC тип JS тип Значение по умолчанию
BOOL boolean false
INT number 0
DINT number 0
REAL number 0.0
TIME number (мс) 0
STRING string ""
STRUCT object { field: default, ... }
ARRAY[n..m] OF T Array new Array(size).fill(default)

Операторы

ST JS
:= =
= ===
<> !==
AND &&
OR \|\|
NOT !
MOD %
+, -, *, /, >, <, >=, <= без изменений

Стандартные функциональные блоки (IEC 61131-3)

FB Конструктор update() Выходы
TON new TON(PT) update(IN) .Q, .ET
TOF new TOF(PT) update(IN) .Q, .ET
TP new TP(PT) update(IN) .Q, .ET
CTU new CTU(PV) update(CU, RESET) .Q, .CV
CTD new CTD(PV) update(CD, LOAD) .Q, .CV
CTUD new CTUD(PV) update(CU, CD, RESET, LOAD) .QU, .QD, .CV
RS new RS() update(SET, RESET1) .Q1
SR new SR() update(SET1, RESET) .Q1
R_TRIG new R_TRIG() update(CLK) .Q
F_TRIG new F_TRIG() update(CLK) .Q

Параметры командной строки

python -m st2js <input> [-m <mapping.yaml>] [-o <output.js>] [--lib types.yaml] [--ignore-undefined] [--strict] [--struct-flatten] [--debug]
Параметр Описание
input ST-файл(ы) (.st, .txt, .xml, .TcPOU, .TcDUT, .TcGVL)
-m, --mapping YAML-файл маппинга датчиков
-o, --output Выходной JS-файл (по умолчанию stdout)
--lib YAML-файл с определениями внешних FB (можно указать несколько раз)
--ignore-undefined Необъявленные переменные — warning вместо error (тип INT)
--strict Warnings = errors (перекрывает --ignore-undefined)
--struct-flatten Маппинг полей структур на отдельные датчики
--programs Имена PROGRAM через запятую (по умолчанию все)
--main Имя главной программы (по умолчанию Main)
--debug Генерировать отладочную инструментацию
--version Версия st2js

Поддержка CODESYS проектов

GVL (Global Variable Lists)

Переменные из VAR_GLOBAL секций PLCopen XML автоматически преобразуются в JavaScript-объекты. Каждый именованный GVL становится отдельным объектом:

// ST: GRU_G1.ErrorConnect_r1
let GRU_G1 = { ErrorConnect_r1: false, ErrorConnect_r2: false, ... };

Переменные доступны и через GVL-имя (GRU_G1.ErrorConnect_r1), и напрямую (ErrorConnect_r1GRU_G1.ErrorConnect_r1).

ACTION (действия)

Действия POU автоматически конвертируются в отдельные JS-функции с qualified-именем:

ST: Preparing.MB_GUI()
JS: Preparing_MB_GUI()

Auto-маппинг I/O по префиксу

Необъявленные переменные с I/O-префиксами автоматически маппятся на uniset_inputs/uniset_outputs:

Префикс Направление Описание
MBI_* input Modbus Bus Input
RDI_* input Read Digital Input
RAI_* input Read Analog Input
MBO_* output Modbus Bus Output
RDO_* output Read Digital Output
RAO_* output Read Analog Output

Внешние библиотеки (–lib)

Типы из внешних библиотек (OwenTypes, OSCAT и др.) не содержатся в PLCopen XML. Без --lib конвертор генерирует auto-stub классы:

class OwenTypes_TRG_Buzzer { constructor() {} execute() {} } // auto-stub

Для точных определений создайте YAML-файл:

# owen_types.yaml
function_blocks:
OwenTypes.TRG_Buzzer:
inputs:
Enable: BOOL
Duration: TIME
outputs:
Q: BOOL
OwenTypes.TRG_Watchdog:
inputs:
Enable: BOOL
Timeout: TIME
outputs:
Q: BOOL

Использование:

python -m st2js project.xml --ignore-undefined --lib owen_types.yaml -o output.js

Пример файла: examples/owen_types.yaml

Debug-визуализатор

При запуске с --debug генерируется расширенная мета-информация, и из сгенерированного JS можно поднять встроенный HTTP-сервер с веб-интерфейсом отладки (модуль uniset2-debug.js). Открывается на /debug/ui указанного порта.

Возможности

Схема (FBD)

  • Интерактивная SVG-схема блоков (FB) и операторных узлов (AND/OR/NOT/…) в режиме Connected (только связанные FB/local/operator — I/O и GVL отображаются как CODESYS-style stub-подписи на рёбрах)
  • Рёбра call (входы FB) и assign (запись в GVL) с правильными цветами и маркерами
  • Автоматическое построение layout по колонкам с компактной per-row высотой; операторные узлы для сложных выражений
  • Полные порты пользовательских FB (inputs/outputs из codegen + augment из observed usage — покрывает VAR_IN_OUT)
  • Per-field assign-рёбра: src.field → dst.field к нужному пину GVL
  • Collapse нескольких рёбер в один …N badge с DOM-tooltip (для случаев, когда несколько источников приходят в один пин)
  • CODESYS-style подписи источников у входов (GVL.field, имена операторов), более светлые чем pin labels
  • Program group backgrounds — полупрозрачные фоны вокруг блоков одной программы с цветной пунктирной рамкой и заголовком

Live debugging

  • Port lamps — цвет пина по текущему значению (зелёный/серый)
  • Live values — зелёные значения (T/F, числа) под именем каждого порта (и входы, и выходы); данные user-FB доступны через _debug_meta
  • Pulse animation — оранжевая вспышка-halo вокруг порта при изменении значения (450ms fade)
  • Mini sparkline — при hover на порт (задержка 250ms) появляется tooltip с графиком последних 60 значений (step-line для BOOL, polyline для числовых)
  • Double-click edge — tooltip с source/dest, kind, program и текущим значением сигнала
  • Автоматическое обновление на каждый snapshot poll
  • LOD: значения скрываются при zoom < 70%, подписи портов — при zoom < 50%
  • View settings (View ▾) — dropdown с чекбоксами: Wires, Values, Backgrounds, Minimap. Скрытие/показ одним кликом

Навигация

  • Zoom — Ctrl+wheel с якорем под курсором (Google Maps стиль), кнопки +//Fit, индикатор процента
  • Pan — drag мышью по фону схемы
  • Minimap — плавающая панель обзора с прямоугольником viewport; клик центрирует, drag рамки — pan; кнопки × / сворачивают/разворачивают; адаптивный размер под aspect ratio схемы
  • Click-to-highlight — клик по блоку подсвечивает его связи и соседей оранжевым, остальное dim-ится; работает даже при скрытых Wires. Повторный клик или Esc снимает
  • Breadcrumb — в тулбаре отображается <program> > <nearest FB> к центру viewport, обновляется при scroll/zoom. Клик по program → фильтр, по FB → scroll + highlight
  • Фильтр по программе — searchable combobox со списком всех программ/действий (стрелки, ввод для фильтра, Enter выбирает)
  • Поиск по имени — инпут в тулбаре, совпадающие блоки подсвечиваются. Префикс : — поиск по FB type (:TON → все TON-инстансы, :Q_Control → все Q_Control)
  • Persistent view state — zoom, scroll, program filter и состояние тумблеров (Wires/Values/Backgrounds/Minimap) сохраняются в localStorage per-project и восстанавливаются при следующем открытии
  • Export SVG — кнопка SVG в тулбаре скачивает схему как .svg файл

Хоткеи (работают на вкладке Schema, игнорируются при вводе в текстовое поле — кроме Esc)

Клавиша Действие
F Fit schema to view
+ / = Zoom in
- Zoom out
0 Reset zoom to 100%
Home Scroll to top-left
W Toggle wires
V Toggle live values
G Toggle program backgrounds
M Toggle minimap
/ Фокус в schema search (: для поиска по типу)
P Фокус в program filter
Esc Снять selection / закрыть popups / снять фокус с поля ввода
? Показать/скрыть справку по хоткеям

Переменные и тренды

  • Вкладка Variables — live-таблица всех переменных с форсированием (force/unforce), группировка по префиксу
  • Вкладка Trends — графики истории значений (click по строке переменной добавляет её в график), окно 30s/1m/5m/30m, экспорт CSV
  • FB Status — правая панель с live-состоянием всех FB-инстансов (Q, ET и т.д.). Поле фильтра вверху панели (sticky) прячет не-совпадающие карточки; двойной клик по карточке переключает на вкладку Schema и центрирует viewport на этом блоке (+ подсветка)

HTTP endpoints

Путь Метод Описание
/debug/ui GET Веб-интерфейс (HTML)
/debug/info GET Версия, счётчики, uptime
/debug/snapshot GET Текущие значения всех watch-переменных
/debug/history GET История значений за окно
/debug/schema GET Граф схемы (nodes + edges + operators + programs)
/debug/objects GET Список экспортированных объектов
/debug/force POST Форсировать значение
/debug/unforce POST Снять форс
/debug/config POST Изменить настройки debug runtime

Подключение из JS

// В сгенерированном с --debug JS-файле
load("uniset2-debug.js");
uniset_debug_start(8088); // HTTP server на порту 8088

После этого визуализатор доступен на http://<host>:8088/debug/ui.

Зависимости

  • Python >= 3.10
  • blark — парсер IEC 61131-3 ST
  • lark — парсер-генератор (зависимость blark)
  • PyYAML — чтение YAML-конфигов
  • uniset2-iec61131.js — runtime-библиотека стандартных FB

Тестирование

cd extensions/JScript/tools/st2js
pip install -e ".[dev]"
pytest tests/ -v

476 тестов, покрытие ядра > 90%.