Конвертирует программы на языке 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_r1 → GRU_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%.