Программа узла — машина состояний: шаги и переходы между ними по условию. Внутри шага оборудование работает ОДНОВРЕМЕННО, каждое на своей дорожке. Ниже — как это устроено, какие бывают условия и пороги, и три программы целиком, взятые из каталога архетипов дословно.
machine({
initial: 'загрузка',
states: {
'загрузка': {
lanes: { X: [move(120, { v: 0.5 })], зажим: [on()], датчик: [waitFor('↑')] },
entry: [on(5)], exit: [off(5)], // мгновенно на входе и на выходе
on: { 'датчик ↑': 'резка', 'X >= 120': 'резка' },
after: { 8: 'авария' }, // предел времени в этом шаге
},
'резка': { wait: 2 },
},
}) | Вид шага | Что это | Особенность |
|---|---|---|
| обычный | дорожки оборудования, действия на входе и выходе, переходы | длится по самой долгой дорожке |
| составной | шаг со вложенными шагами: сам не исполняется, исполняются дети | его переходы наследуются всеми вложенными — общая авария объявляется один раз |
| параллельный | запускает несколько соседних шагов одновременно как независимые ветки | две ветки, держащие одну ось или один порт, — это конфликт ресурса, и он называется |
| выдержка | фиксированная пауза без оборудования вовсе | длительность объявлена полем, а не выведена из дорожек |
| Дорожка | Что на неё кладётся | Замечание |
|---|---|---|
| дорожка оси | ход в позицию · вращение · прижим моментом · торможение · удержание | движения кладутся только сюда |
| дорожка выхода | включить · выключить · импульс · мигание фоном · удержание | порт известен из имени дорожки |
| дорожка входа | ждать фронт или уровень · удержание | ожидание эластично: дорожка стоит, пока сигнала нет |
Все дорожки шага стартуют одновременно, а сам шаг длится по самой долгой: остальные заканчивают раньше и ждут. Это и есть способ выразить «качаем и зажимаем разом» без второй программы.
Оборудование блока доступно по именам сразу. Код пишется словами — «лента», «зажим», «в позиции», — а с каким приводом и пином они связаны, знает паспорт стенда. Та же программа переносится на соседний участок сменой паспорта, а не переписыванием логики.
Счётчик — именованный объект программы. Один шаг прибавляет, другой сбрасывает; прибавка происходит на входе в шаг, ровно раз за проход. Памяти между пусками у счётчика пока нет: пуск начинает счёт с нуля.
Условия проверяются непрерывно, каждый такт — как скан ПЛК, а не как обработчик одиночного события. Срабатывает первый подошедший переход в порядке объявления.
| Вид | Как пишется | Что значит |
|---|---|---|
| шаг доиграл | done | все дорожки шага закончились, включая эластичное ожидание входа |
| время в шаге | '5s' · after: { 5: … } | секунды с момента входа В ЭТОТ шаг, а не с начала программы |
| вход | 'датчик ↑' · 'датчик ↓' · 'датчик =1' · 'датчик =0' | фронт или уровень; по имени из паспорта блока либо по номеру порта |
| позиция оси | 'X >= 90 град' · 'X >= 2 об' | ось доехала; единица в строке решает, какая это величина |
| скорость оси | 'лента >= 1500 об/мин' | ось разогналась |
| момент оси | 'пресс момент >= 380' | ось нагрузилась. Без единицы число читается в ПРОМИЛЛЕ номинала: 380 — это 38 % |
| счётчик | 'счётчик детали >= 6' | именованный счётчик программы: один шаг считает, другой сбрасывает |
| составное | 'деталь =1 и зажим =1' · 'или' · 'не' | до пяти простых условий на переход; не влезло — это уже состояние, назовите его шагом |
| формула | '|задатчик момент - ведомый момент| >= 120' | арифметика над величинами: до трёх действий, делить можно только на число |
| уставка рецепта | 'пресс момент >= @момент' | порог берётся из рецепта смены — программа одна, порог меняет выбор рецепта на пульте |
| авария | переход по аварии | превышение порога момента или рассогласования на любой оси шага |
| предел ожидания | { to: 'зажим', timeout_s: 5, on_timeout: 'error' } | страховка условия, которое ждёт мира: не наступило за N секунд — исход называет автор (skip — продолжить · stop — остановить · error — отказ). Предела нет — шаг ждёт без конца, и лаборатория предупреждает об этом до пуска |
| Величина | Единица | Замечание |
|---|---|---|
| позиция в ходе оси | обороты | градусы переводятся конвертером: deg(90) |
| скорость вращения | об/с | об/мин переводятся конвертером: rpm(1500) |
| прижим моментом | % номинала | знак задаёт направление |
| момент в условии перехода | ‰ номинала | 40 % прижима соответствует условию «момент >= 400» |
| время | секунды | везде, включая пределы ожидания |
Программу исполняет один из двух слоёв, и это видно по её содержимому. Чистая логика — выходы, ожидание входов, паузы, счётчики, формулы и рецепты — уезжает в логический слой и живёт ТАКТОМ стенда. Программа, которая двигает оси, исполняется плеером движения: генерация траектории — работа интерполятора, и логический слой такую программу принимать отказывается, называя конкретные блоки с движением.
Граница проведена явно в обе стороны. Условие по СЧЁТЧИКУ умеет только логический слой: плееру движения такая программа не отдаётся молча — он отказывает и называет слой, в который её надо доставить. Составное условие, формула и порог-уставка — из того же логического набора.
Готовится. Предел ожидания у ПЕРЕХОДА: автор задаёт предел и его исход ПАРОЙ — продолжить, остановить программу или объявить отказ. Половина пары не принимается: исход без предела никто никогда не исполнит, предел без исхода сработает молча. Там же — предупреждение при пуске: если выйти из шага можно только по внешнему условию и предел не назван, об этом сказано ДО пуска, а не выясняется зависанием. Сделано, на промышленный контур ещё не выкачено.
| Предел | По умолчанию | Замечание |
|---|---|---|
| порог момента | 600 ‰ номинала | превышение считается аварией, только если ДЕРЖИТСЯ дольше выдержки — на разгоне момент штатно уходит за предел |
| порог рассогласования | 0,05 оборота | на виртуальных осях не проверяется вовсе: рассогласование там — штатная работа модели, а не факт привода |
| выдержка аварии | 0,2 с | сколько превышение должно продержаться |
Пределы едут вместе с программой одним объектом: перегруз — свойство механизма, а не отдельного шага. На превышение можно объявить переход; переход не объявлен — останов с названной причиной.
Момент на виртуальных осях считает МОДЕЛЬ, а не привод. Он выводится из рассогласования «велено» и «есть»: свободный ход — момент холостого хода, нагрузка — рост пропорционально ошибке, упор — насыщение на уставке программы. Это годится, чтобы отладить логику поиска упора и затяжки, но замером не является и цитировать его как факт нельзя.
Нет паспортного номинала — в кадре ПРОЧЕРК, и это правильно. Шкала промилле отсчитывается от номинала двигателя; номинал не задан — точки отсчёта нет, и платформа показывает прочерк вместо правдоподобного числа. «Перегруз» при этом не объявляется: сравнивать не с чем. Условие перехода по моменту на такой оси не сработает — гоняйте такую программу на железе, там момент приходит от привода.
Две программы соседних узлов. Питатель выдаёт заготовку из магазина по приходу детали; станция завинчивания подаёт винт, ждёт подтверждения от датчика у биты и затягивает до момента. Обе — шаблоны своих архетипов, без правок.
rig({
axes: { толкатель: 0 },
outputs: { отсекатель: 0 },
inputs: { наличие: 0 },
})
machine({
title: 'Питатель',
initial: 'ждём заготовку',
states: {
'ждём заготовку': {
lanes: { наличие: [waitFor('↑')] },
on: { done: 'выдача' },
},
'выдача': {
entry: [on(0)],
lanes: { толкатель: [move(2, { v: 0.6 })] },
on: { done: 'отсечь' },
},
'отсечь': {
entry: [off(0)],
lanes: { толкатель: [move(0, { v: 1 })] },
on: { done: 'ждём заготовку' },
},
},
}) rig({
axes: { шпиндель: 0, 'подача Z': 1 },
outputs: { 'питатель винта': 0 },
inputs: { 'винт подан': 0 },
})
machine({
title: 'Завинчивание',
initial: 'подать винт',
states: {
'подать винт': {
entry: [pulse(0.3, 0)], // импульс на питатель
lanes: { 'винт подан': [waitFor('↑', { timeout: 5, onTimeout: 'error' })] },
on: { done: 'подвод' },
},
'подвод': {
lanes: { 'подача Z': [move(2, { v: 0.6 })] },
on: { done: 'завинчивание' },
},
'завинчивание': {
lanes: { шпиндель: [push(60, { sec: 2 })] }, // 60 % номинала, держать 2 с
on: { done: 'отвод' },
},
'отвод': {
lanes: { 'подача Z': [move(0, { v: 1 })] },
on: { done: 'подать винт' },
},
},
}) Ожидание входа эластично: дорожка стоит, пока сигнала нет, и время шага плывёт за фактом. Предел ожидания здесь назван явно — пять секунд; молчаливое «доиграл по времени» и «деталь пришла» — разные вещи, и различать их обязан автор.
Затяжка задана моментом, а не координатой: 60 % номинала, удержание две секунды. Резьба кончается там, где кончается, — по координате это не выражается.
Цикл делительного стола: расфиксировать, повернуть на позицию, зафиксировать, выдержать пост. Показывает приём «два перехода в один шаг»: пришли по датчику — уходим сразу, датчика нет — по окончании хода.
rig({
axes: { стол: 0 },
outputs: { фиксатор: 0 },
inputs: { 'в позиции': 0 },
})
machine({
title: 'Поворотный делительный стол',
initial: 'открыть фиксатор',
states: {
'открыть фиксатор': {
entry: [off(0)],
lanes: { стол: [hold(0.1)] },
on: { done: 'поворот' },
},
'поворот': {
lanes: { стол: [move(deg(60), { rel: true, v: 0.3 })] },
// Два перехода в один шаг: пришли по датчику — идём сразу; датчика
// нет — уходим по окончании хода. Первый подошедший и срабатывает.
on: { 'в позиции ↑': 'фиксация', done: 'фиксация' },
},
'фиксация': {
entry: [on(0)],
lanes: { стол: [hold(0.3)] },
on: { done: 'выдержка' },
},
'выдержка': {
wait: 0.5,
on: { done: 'открыть фиксатор' },
},
},
}) Двухосевой узел: ждём деталь, опускаемся, берём, переносим, кладём, возвращаемся. Захват здесь подтверждается выдержкой — у блока есть вход «захват сомкнут», и в своём узле его стоит поставить на место паузы.
rig({
axes: { X: 0, Z: 1 },
outputs: { захват: 0 },
inputs: { 'деталь есть': 0 },
})
machine({
title: 'Pick&Place',
initial: 'ждём деталь',
states: {
'ждём деталь': {
lanes: { 'деталь есть': [waitFor('↑')] },
on: { done: 'опуск забора' },
},
'опуск забора': { lanes: { Z: [move(2, { v: 0.6 })] }, on: { done: 'захват' } },
'захват': { entry: [on(0)], lanes: { Z: [hold(0.3)] }, on: { done: 'подъём' } },
'подъём': { lanes: { Z: [move(0, { v: 1 })] }, on: { done: 'перенос' } },
'перенос': { lanes: { X: [move(6, { v: 0.8 })] }, on: { done: 'опуск укладки' } },
'опуск укладки': { lanes: { Z: [move(2, { v: 0.6 })] }, on: { done: 'отпуск' } },
'отпуск': { entry: [off(0)], lanes: { Z: [hold(0.3)] }, on: { done: 'подъём после укладки' } },
'подъём после укладки': { lanes: { Z: [move(0, { v: 1 })] }, on: { done: 'возврат' } },
'возврат': { lanes: { X: [move(0, { v: 1 })] }, on: { done: 'ждём деталь' } },
},
}) Выдержка вместо подтверждения — это заглушка, а не решение. Пауза на глазок выдаёт «команда подана» за «взял». Роль входа grip_ok заведена ровно затем, чтобы условие перехода звучало как «захват сомкнут», а не «прошло 0,3 секунды».
rig({ axes: { зажим: 0 }, outputs: { готово: 0 } })
machine({
title: 'Зажать до упора',
initial: 'подвод',
states: {
'подвод': {
lanes: { зажим: [move(3, { v: 0.5 })] },
// 300 ‰ = 30 % номинала. Момент читается с самого привода —
// отдельный датчик усилия для этого не нужен.
on: { 'зажим момент >= 300': 'упор', done: 'упор' },
},
'упор': {
lanes: { зажим: [push(40, { sec: 1 })] },
entry: [on(0)],
on: { done: 'отвод' },
},
'отвод': {
entry: [off(0)],
lanes: { зажим: [move(0, { v: 0.8 })] },
on: { done: 'подвод' },
},
},
}) Единицы здесь разные намеренно: прижим говорит в процентах номинала, условие перехода — в промилле. 40 % прижима соответствует условию «момент >= 400».
Состав узлов и словарь ролей каналов — на странице блоков автоматики. Путь навесного органа робота — в глаголах движения.