Добавлены управляющие программы для коллаборативного робота

This commit is contained in:
Даниил Грабарь
2026-05-05 15:43:56 +03:00
parent 748f30b1d1
commit b3f4bc458b
2 changed files with 1130 additions and 0 deletions
+529
View File
@@ -0,0 +1,529 @@
package ros;
// ════════════════════════════════════════════════════════════════════════════
//
// ServerFriRos2.java
// Sunrise OS 1.16 | FRI 1.16 | KUKA iiwa 7
//
// Режимы управления:
// Position — FRI задаёт целевые углы суставов (PositionControlMode)
// Сглаживание команд выполняется на стороне ROS2 (FRIClient EMA)
// Torque — FRI задаёт добавочные моменты (JointImpedanceControlMode)
// Monitor — FRI только читает состояние (NO_COMMAND_MODE)
// Масса инструмента берётся из Sunrise WB (Load Data)
// и валидируется через SmartServo.validateForImpedanceMode()
//
// Сетевые интерфейсы:
// KONI — 192.170.10.10 (рекомендуется)
// KLI — 192.168.21.31
//
// ════════════════════════════════════════════════════════════════════════════
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
import javax.inject.Named;
import com.kuka.connectivity.fastRobotInterface.ClientCommandMode;
import com.kuka.connectivity.fastRobotInterface.FRIChannelInformation;
import com.kuka.connectivity.fastRobotInterface.FRIConfiguration;
import com.kuka.connectivity.fastRobotInterface.FRIJointOverlay;
import com.kuka.connectivity.fastRobotInterface.FRISession;
import com.kuka.connectivity.fastRobotInterface.IFRISessionListener;
import com.kuka.connectivity.motionModel.smartServo.SmartServo;
import com.kuka.roboticsAPI.applicationModel.RoboticsAPIApplication;
import com.kuka.roboticsAPI.controllerModel.Controller;
import com.kuka.roboticsAPI.deviceModel.LBR;
import com.kuka.roboticsAPI.geometricModel.Tool;
import com.kuka.roboticsAPI.motionModel.BasicMotions;
import com.kuka.roboticsAPI.motionModel.PositionHold;
import com.kuka.roboticsAPI.motionModel.controlModeModel.JointImpedanceControlMode;
import com.kuka.roboticsAPI.motionModel.controlModeModel.PositionControlMode;
import com.kuka.roboticsAPI.uiModel.ApplicationDialogType;
public class ServerFriRos2 extends RoboticsAPIApplication {
// ── КОНСТАНТЫ ────────────────────────────────────────────────────────────
private static final double[] ZERO_POSITION = {0, 0, 0, 0, 0, 0, 0};
private static final double[] MONITOR_WORKING_POSITION = {0, 0, 0, -1.57, 0, 1.57, 0};
private static final String KONI_IP = "192.170.10.10";
private static final String KLI_IP = "192.168.21.31";
private static final int FRI_CONNECT_TIMEOUT_SEC = 30;
private static final double APPROACH_VEL = 0.30;
// Monitor режим: нулевая жёсткость = свободное ведение рукой
private static final double MONITOR_JOINT_STIFFNESS = 0.0;
private static final double MONITOR_JOINT_DAMPING = 0.7;
// ── ПЕРЕЧИСЛЕНИЯ ─────────────────────────────────────────────────────────
private enum NetworkInterface {
KONI("KONI (192.170.10.10) - выделенная FRI-сеть", KONI_IP),
KLI ("KLI (192.168.21.31) - основная сеть KRC", KLI_IP);
final String label;
final String ip;
NetworkInterface(String label, String ip) {
this.label = label;
this.ip = ip;
}
}
private enum ControlMode {
POSITION,
TORQUE,
MONITOR
}
// ── ПОЛЯ ─────────────────────────────────────────────────────────────────
private LBR _lbr;
private Controller _lbrController;
/**
* Инструмент, настроенный в Sunrise WB → Object Templates.
* Имя должно совпадать с именем в SWB (здесь: "patron").
* Масса и CoM берутся из Load Data этого объекта.
*/
@Inject
@Named("patron")
private Tool _tool;
private NetworkInterface _selectedNetwork;
private ControlMode _selectedMode;
private int _sendPeriodMs;
private double _jointStiffness;
private FRIConfiguration _friConfig;
private FRISession _friSession;
private FRIJointOverlay _friOverlay;
private IFRISessionListener _friListener;
// ── LIFECYCLE ────────────────────────────────────────────────────────────
@Override
public void initialize() {
_lbrController = (Controller) getContext().getControllers().toArray()[0];
_lbr = (LBR) _lbrController.getDevices().toArray()[0];
// Прикрепляем инструмент к фланцу — обязательно для корректной
// гравкомпенсации и validateForImpedanceMode()
_tool.attachTo(_lbr.getFlange());
getLogger().info("════════════════════════════════════════════");
getLogger().info(" ServerFriRos2 | KUKA iiwa 7 + ROS2 FRI");
getLogger().info(" Sunrise OS 1.16 | FRI 1.16");
getLogger().info(" Робот : " + _lbr.getName());
getLogger().info(" Инструмент : " + _tool.getName());
getLogger().info("════════════════════════════════════════════");
initFriListener();
requestUserConfig();
}
@Override
public void run() throws Exception {
moveToInitialPosition();
switch (_selectedMode) {
case POSITION: runPositionMode(); break;
case TORQUE: runTorqueMode(); break;
case MONITOR: runMonitorMode(); break;
}
getLogger().info("Программа завершена.");
}
@Override
public void dispose() {
if (_friSession != null) {
getLogger().info("dispose(): Закрытие FRI-сессии...");
try {
_friSession.close();
} catch (Exception e) {
getLogger().warn("Ошибка при закрытии FRI: " + e.getMessage());
}
_friSession = null;
}
super.dispose();
}
// ── UI — ЗАПРОС КОНФИГУРАЦИИ ─────────────────────────────────────────────
private void requestUserConfig() {
// Шаг 1: Сетевой интерфейс
int netChoice = getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Шаг 1 / 3 — Сетевой интерфейс FRI\n\n"
+ "KONI: выделенная высокоскоростная сеть (рекомендуется)\n"
+ "KLI : основная сеть KRC",
NetworkInterface.KONI.label,
NetworkInterface.KLI.label
);
_selectedNetwork = (netChoice == 0) ? NetworkInterface.KONI : NetworkInterface.KLI;
getLogger().info("[Шаг 1] Сеть: " + _selectedNetwork.label);
// Шаг 2: Режим управления
int modeChoice = getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Шаг 2 / 3 — Режим управления FRI\n\n"
+ "Position : ROS2 задаёт угловые позиции суставов\n"
+ "Torque : ROS2 задаёт добавочные моменты\n"
+ "Monitor : только данные; ведение рукой",
"Position",
"Torque",
"Monitor"
);
if (modeChoice == 0) {
_selectedMode = ControlMode.POSITION;
_jointStiffness = 0;
int pChoice = getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Шаг 3 / 3 — Период отправки [мс] (Position)\n\n"
+ "Режим: PositionControlMode (жёсткое позиционирование)\n"
+ "Сглаживание команд выполняется на стороне ROS2.\n\n"
+ "10 мс — стабильно\n"
+ " 5 мс — стандарт для 200 Гц ros2_control\n"
+ " 2 мс — быстро, требует низкого джиттера",
"10 мс",
" 5 мс",
" 2 мс"
);
_sendPeriodMs = new int[]{10, 5, 2}[pChoice];
} else if (modeChoice == 1) {
_selectedMode = ControlMode.TORQUE;
int pChoice = getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Шаг 3а / 4 — Период отправки [мс] (Torque)\n\n"
+ "При пропуске пакета Sunrise переходит в PositionHold.\n"
+ "Рекомендуется 1–2 мс при стабильной KONI-сети.",
" 5 мс",
" 2 мс (рекомендуется)",
" 1 мс (максимальная частота)"
);
_sendPeriodMs = new int[]{5, 2, 1}[pChoice];
int sChoice = getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Шаг 3б / 4 — Жёсткость суставов [Нм/рад] (Torque)\n\n"
+ "Высокая: точное следование, меньше отклонение\n"
+ "Низкая : мягкое взаимодействие со средой\n\n"
+ "⚠ 1500 Нм/рад — только без людей в рабочей зоне!",
"1500 (жёсткий / производственный)",
"1000 (стандарт)",
" 800 (средний)",
" 500 (мягкий / взаимодействие)",
" 300 (очень мягкий)"
);
_jointStiffness = new double[]{1500, 1000, 800, 500, 300}[sChoice];
} else {
_selectedMode = ControlMode.MONITOR;
_sendPeriodMs = 2; // 2 мс — запас для non-RT систем без FIFO-планировщика
_jointStiffness = 0;
getLogger().info("[Шаг 3] Monitor: период = " + _sendPeriodMs + " мс");
}
getLogger().info("════════════════════════════════════════════");
getLogger().info(" КОНФИГУРАЦИЯ ЗАПУСКА");
getLogger().info(" Сеть : " + _selectedNetwork.label);
getLogger().info(" IP хоста : " + _selectedNetwork.ip);
getLogger().info(" Режим : " + _selectedMode.name());
getLogger().info(" Период : " + _sendPeriodMs + " мс");
getLogger().info(" Инструмент : " + _tool.getName());
if (_selectedMode == ControlMode.TORQUE) {
getLogger().info(" Жёсткость : " + _jointStiffness + " Нм/рад");
}
getLogger().info("════════════════════════════════════════════");
}
// ── ДВИЖЕНИЕ В СТАРТОВУЮ ПОЗИЦИЮ ─────────────────────────────────────────
private void moveToInitialPosition() {
if (_selectedMode == ControlMode.MONITOR) {
getLogger().info("Движение в рабочую позицию Monitor (через нулевую)...");
_lbr.move(
BasicMotions.batch(
BasicMotions.ptp(ZERO_POSITION).setBlendingRel(0.5),
BasicMotions.ptp(MONITOR_WORKING_POSITION)
).setJointVelocityRel(APPROACH_VEL)
);
getLogger().info("Рабочая позиция Monitor достигнута.");
} else {
getLogger().info("Движение в нулевую позицию...");
_lbr.move(
BasicMotions.ptp(ZERO_POSITION).setJointVelocityRel(APPROACH_VEL)
);
getLogger().info("Нулевая позиция достигнута.");
}
}
// ── РЕЖИМ: POSITION ──────────────────────────────────────────────────────
private void runPositionMode() {
getLogger().info("═══ Position режим (PositionControlMode) ═══");
if (!setupFriSession(ClientCommandMode.POSITION)) {
return;
}
PositionControlMode ctrlMode = new PositionControlMode();
PositionHold posHold = new PositionHold(ctrlMode, -1, TimeUnit.SECONDS);
getLogger().info("Position режим активен. Ожидаю команды от ROS2...");
_lbr.move(posHold.addMotionOverlay(_friOverlay));
_friSession.close();
_friSession = null;
getLogger().info("Position режим завершён. FRI закрыт.");
}
// ── РЕЖИМ: TORQUE ────────────────────────────────────────────────────────
private void runTorqueMode() {
getLogger().info("═══ Torque режим ═══");
getLogger().info("Жёсткость: " + _jointStiffness + " Нм/рад");
if (!setupFriSession(ClientCommandMode.TORQUE)) {
return;
}
JointImpedanceControlMode ctrlMode = new JointImpedanceControlMode(
_jointStiffness, _jointStiffness, _jointStiffness,
_jointStiffness, _jointStiffness, _jointStiffness,
_jointStiffness
);
ctrlMode.setDampingForAllJoints(0.7);
PositionHold posHold = new PositionHold(ctrlMode, -1, TimeUnit.SECONDS);
getLogger().info("Torque режим активен. Ожидаю команды от ROS2...");
_lbr.move(posHold.addMotionOverlay(_friOverlay));
_friSession.close();
_friSession = null;
getLogger().info("Torque режим завершён. FRI закрыт.");
}
// ── РЕЖИМ: MONITOR ───────────────────────────────────────────────────────
/**
* Monitor режим.
*
* Фаза A: Валидация Load Data инструмента из Sunrise WB.
* Масса берётся из Object Templates → patron → Load Data,
* как в TeachKuka.java (SmartServo.validateForImpedanceMode).
*
* Фаза B: Диалог подтверждения — оператор запускает ROS2 FRI узел.
*
* Фаза C: FRI в NO_COMMAND_MODE — данные суставов идут в ROS2.
*
* Фаза D: PositionHold с нулевой жёсткостью — свободное ведение рукой.
*/
private void runMonitorMode() {
getLogger().info("═══ Monitor режим ═══");
// Фаза A: валидация Load Data инструмента из SWB
validateLoadModel();
// Фаза B: ждём подтверждения оператора что ROS2 FRI готов
getApplicationUI().displayModalDialog(
ApplicationDialogType.INFORMATION,
"Запустите ROS2 FRI узел на ПК (" + _selectedNetwork.ip + ").\n\n"
+ "Нажмите OK когда ros2_control_node активен.",
"OK — ROS2 готов"
);
// Фаза C: FRI в режиме только чтения
if (!setupFriSession(ClientCommandMode.NO_COMMAND_MODE)) {
return;
}
// Фаза D: PositionHold с нулевой жёсткостью — свободное ведение рукой
JointImpedanceControlMode guidingMode = new JointImpedanceControlMode(
MONITOR_JOINT_STIFFNESS, MONITOR_JOINT_STIFFNESS, MONITOR_JOINT_STIFFNESS,
MONITOR_JOINT_STIFFNESS, MONITOR_JOINT_STIFFNESS, MONITOR_JOINT_STIFFNESS,
MONITOR_JOINT_STIFFNESS
);
guidingMode.setDampingForAllJoints(MONITOR_JOINT_DAMPING);
PositionHold posHold = new PositionHold(guidingMode, -1, TimeUnit.SECONDS);
getLogger().info("Monitor активен:");
getLogger().info(" • Ведите робота рукой — он не сопротивляется.");
getLogger().info(" • Данные суставов транслируются в ROS2 каждые "
+ _sendPeriodMs + " мс.");
getLogger().info(" • Остановите FRI-клиент для завершения.");
_lbr.move(posHold);
_friSession.close();
_friSession = null;
getLogger().info("Monitor режим завершён. FRI закрыт.");
}
// ── ВАЛИДАЦИЯ НАГРУЗКИ ИНСТРУМЕНТА ──────────────────────────────────────
/**
* Проверяет Load Data (масса / CoM / инерция) инструмента из Sunrise WB.
*
* Аналог validateLoadModel() из TeachKuka.java:
* SmartServo.validateForImpedanceMode(_tool) проверяет, что данные
* нагрузки заданы корректно для работы с JointImpedanceControlMode.
*
* Если валидация не прошла — нужно задать Load Data в:
* Sunrise WB → Object Templates → patron → Load Data
* (Mass, Centre of Mass, Moment of Inertia)
*/
private void validateLoadModel() {
getLogger().info("════════════════════════════════════════════");
getLogger().info(" ВАЛИДАЦИЯ НАГРУЗКИ ИНСТРУМЕНТА");
getLogger().info(" Инструмент: " + _tool.getName());
getLogger().info("════════════════════════════════════════════");
boolean valid = SmartServo.validateForImpedanceMode(_tool);
if (valid) {
getLogger().info(" ✓ Load Data валидны.");
getLogger().info(" Масса и CoM заданы корректно в Sunrise WB.");
getLogger().info(" Гравкомпенсация будет работать точно.");
} else {
getLogger().warn(" ⚠ Валидация Load Data НЕ прошла!");
getLogger().warn(" Задайте данные нагрузки в:");
getLogger().warn(" Sunrise WB → Object Templates → "
+ _tool.getName() + " → Load Data");
getLogger().warn(" (Mass [кг], Centre of Mass [мм], Inertia [кг·м²])");
getLogger().warn(" Гравкомпенсация в Monitor режиме может работать некорректно.");
// Предупреждаем оператора — он решает продолжить или нет
int choice = getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Load Data инструмента '" + _tool.getName() + "' не заданы.\n\n"
+ "Без корректных данных нагрузки гравкомпенсация\n"
+ "будет работать с ошибкой.\n\n"
+ "Задайте данные в Sunrise WB → Object Templates → "
+ _tool.getName() + " → Load Data,\nзатем перезапустите программу.\n\n"
+ "Или продолжите без корректной нагрузки (на свой риск).",
"Продолжить",
"Остановить"
);
if (choice == 1) {
throw new RuntimeException(
"Остановлено оператором: Load Data не заданы для "
+ _tool.getName());
}
}
getLogger().info("════════════════════════════════════════════");
}
// ── FRI — НАСТРОЙКА СЕССИИ ───────────────────────────────────────────────
private boolean setupFriSession(ClientCommandMode commandMode) {
_friConfig = FRIConfiguration.createRemoteConfiguration(_lbr, _selectedNetwork.ip);
_friConfig.setSendPeriodMilliSec(_sendPeriodMs);
_friConfig.setReceiveMultiplier(1);
getLogger().info("Создание FRI-сессии...");
getLogger().info(" Хост : " + _friConfig.getHostName()
+ " порт: " + _friConfig.getPortOnRemote());
getLogger().info(" Режим : " + commandMode.name());
getLogger().info(" Период : " + _friConfig.getSendPeriodMilliSec() + " мс");
_friSession = new FRISession(_friConfig);
_friSession.addFRISessionListener(_friListener);
try {
getLogger().info("Ожидание FRI-клиента на " + _selectedNetwork.ip
+ " (таймаут " + FRI_CONNECT_TIMEOUT_SEC + " с)...");
_friSession.await(FRI_CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS);
} catch (TimeoutException e) {
getLogger().error("Таймаут FRI! Клиент не ответил за "
+ FRI_CONNECT_TIMEOUT_SEC + " с.");
getLogger().error("Убедитесь, что ROS2 FRI-узел запущен на "
+ _selectedNetwork.ip);
_friSession.close();
_friSession = null;
return false;
}
getLogger().info("FRI-соединение установлено!");
logFriChannelInfo();
if (commandMode != ClientCommandMode.NO_COMMAND_MODE) {
_friOverlay = new FRIJointOverlay(_friSession, commandMode);
getLogger().info("FRIJointOverlay создан: " + commandMode.name());
} else {
_friOverlay = null;
getLogger().info("Monitor: FRIJointOverlay не создаётся (только чтение).");
}
return true;
}
// ── FRI — LISTENER ───────────────────────────────────────────────────────
private void initFriListener() {
_friListener = new IFRISessionListener() {
@Override
public void onFRIConnectionQualityChanged(FRIChannelInformation info) {
getLogger().info("[FRI] Качество: " + info.getQuality()
+ " jitter=" + info.getJitter() + " мс"
+ " latency=" + info.getLatency() + " мс");
}
@Override
public void onFRISessionStateChanged(FRIChannelInformation info) {
getLogger().info("[FRI] Состояние: " + info.getFRISessionState()
+ " jitter=" + info.getJitter() + " мс"
+ " latency=" + info.getLatency() + " мс");
}
};
}
private void logFriChannelInfo() {
FRIChannelInformation info = _friSession.getFRIChannelInformation();
getLogger().info("[FRI] Состояние : " + info.getFRISessionState());
getLogger().info("[FRI] Качество : " + info.getQuality());
getLogger().info("[FRI] Jitter : " + info.getJitter() + " мс");
getLogger().info("[FRI] Latency : " + info.getLatency() + " мс");
}
}
+601
View File
@@ -0,0 +1,601 @@
package application;
//══════════════════════════════════════════════════════════════════════
//
//
//TeachKuka.java
//Sunrise OS 1.16 | Servoing 1.16 | ApplicationFramework 1.2
//
//
//Режимы работы:
// Режим 1: Захват позиции
// Режим 2: Запись и воспроизведение траектории
//
//
//══════════════════════════════════════════════════════════════════════
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.inject.Inject;
import javax.inject.Named;
import com.kuka.connectivity.motionModel.smartServo.ISmartServoRuntime;
import com.kuka.connectivity.motionModel.smartServo.SmartServo;
import com.kuka.roboticsAPI.applicationModel.RoboticsAPIApplication;
import com.kuka.roboticsAPI.controllerModel.Controller;
import com.kuka.roboticsAPI.deviceModel.JointPosition;
import com.kuka.roboticsAPI.deviceModel.LBR;
import com.kuka.roboticsAPI.geometricModel.CartDOF;
import com.kuka.roboticsAPI.geometricModel.Frame;
import com.kuka.roboticsAPI.geometricModel.Tool;
import com.kuka.roboticsAPI.motionModel.BasicMotions;
import com.kuka.roboticsAPI.motionModel.IMotionContainer;
import com.kuka.roboticsAPI.motionModel.controlModeModel.CartesianImpedanceControlMode;
import com.kuka.roboticsAPI.motionModel.controlModeModel.PositionControlMode;
import com.kuka.roboticsAPI.uiModel.ApplicationDialogType;
public class TeachKuka extends RoboticsAPIApplication {
@Inject
private LBR _lbr;
@Inject
private Controller _lbrController;
@Inject
@Named("tool1")
private Tool _gripper;
// ==== КОНСТАНТНЫ =====
private static final double[] HOME_POSITION_RAD = {
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
};
private static final double[] WORKING_POSITION_RAD = {
0.0, 0.0, 0.0, -1.57, 0.0, 1.57, 0.0
};
private static final double HOME_BLENDING_REL = 0.5;
private static final double APPROACH_VELOCITY_REL = 0.3;
private static final double REPLAY_VELOCITY_REL = 0.2;
private static final double GRAV_COMP_STIFFNESS_TRANSL = 5.0; // N/m
private static final double GRAV_COMP_STIFFNESS_ROT = 1.0; // Nm/рад
private static final double GRAV_COMP_DAMPING = 0.7;
private static final long GRAV_COMP_UPDATE_INTERVAL_MS = 10L;
// Интервал записи точек (мс). 100 мс = 10 точек/сек
private static final long RECORD_INTERVAL_MS = 100L;
// Лимит точек: 3000 * 100 мс = 5 минут записи
private static final int MAX_TRAJECTORY_POINTS = 3000;
// Задержка перед стартом воспроизведения — чтобы успеть отойти
private static final long PRE_REPLAY_DELAY_MS = 2000L;
// Порог детекции столкновения по внешнему суставному моменту (Нм)
// 4–6 Нм - высокая чувствительность
// 7–10 Нм - средняя (производственная среда)
private static final double COLLISION_TORQUE_THRESHOLD_NM = 6.0;
// Период опроса пока путь заблокирован (мс)
private static final long OBSTACLE_POLL_INTERVAL_MS = 100L;
// ==== СОСТОЯНИЕ ПРИЛОЖЕНИЯ
private final List<JointPosition> recordedTrajectory =
new ArrayList<JointPosition>();
private final AtomicBoolean stopRecordingFlag =
new AtomicBoolean(false);
private final AtomicBoolean stopGravCompFlag =
new AtomicBoolean(false);
private volatile IMotionContainer activeMotionContainer = null;
private volatile ISmartServoRuntime gravCompRuntime = null;
private Thread recordingThread = null;
private Thread gravCompThread = null;
@Override
public void initialize() {
_gripper.attachTo(_lbr.getFlange());
getLogger().info(" Программа : TeachKuka.java");
getLogger().info(" Sunrise OS 1.16 | Servoing 1.16");
getLogger().info(" Робот : " + _lbr.getName());
getLogger().info(" Суставов : " + _lbr.getJointCount());
getLogger().info(" Контроллер: " + _lbrController.getName());
getLogger().info(" Захват: " + _gripper.getName() + " -> прикреплён к фланцу");
}
@Override
public void run() throws Exception {
moveToHomePosition();
boolean running = true;
while (running) {
int choice = showMainMenu();
switch (choice) {
case 0:
positionSnapshot();
break;
case 1:
trajectoryRecord();
break;
case 2:
default:
running = false;
break;
}
}
getLogger().info("Выход. Возвращаюсь в Home...");
moveToHomePosition();
getLogger().info("Программа завершена.");
}
@Override
public void dispose() {
stopRecordingFlag.set(true);
stopGravCompFlag.set(true);
stopGravCompThread();
stopRecordingThread();
cancelActiveMotion();
super.dispose();
}
private int showMainMenu() {
return getApplicationUI().displayModalDialog(
ApplicationDialogType.QUESTION,
"Выберите режим работы",
"Режим 1: Позиция",
"Режим 2: Траектория",
"Выход"
);
}
// Режим 1
private void positionSnapshot() throws Exception {
getLogger().info("Вход в Режим 1: Захват позиции");
moveToWorkingPosition();
validateLoadModel();
startGravCompOnTool();
getLogger().info("Ведите робота рукой. Он останется там где вы его поставите.");
boolean inMode = true;
while (inMode) {
int choice = getApplicationUI().displayModalDialog(
ApplicationDialogType.INFORMATION,
"Выбери действие",
"Получить позицию",
"Назад"
);
if (choice == 0) {
logCurrentPosition();
} else {
inMode = false;
}
}
stopGravComp();
getLogger().info("Режим 1 завершён. Тормоз активирован.");
}
// Режим 2
private void trajectoryRecord() throws Exception {
getLogger().info("Вход в Режим 2: Запись траектории");
moveToWorkingPosition();
validateLoadModel();
synchronized (recordedTrajectory) {
recordedTrajectory.clear();
}
startGravCompOnTool();
startRecordingThread();
boolean inMode = true;
while (inMode) {
int choise = getApplicationUI().displayModalDialog(
ApplicationDialogType.INFORMATION,
"Выберите действие",
"Повторить траекторию",
"Рестарт",
"Назад"
);
if (choise == 0) {
stopRecordingThread();
stopGravComp();
List<JointPosition> snapshot;
synchronized (recordedTrajectory) {
snapshot = new ArrayList<JointPosition>(recordedTrajectory);
}
if (snapshot.size() < 2) {
getLogger().warn("Траектория слишком короткая (< 2 точек)."
+ " Переместите робота и попробуйте снова.");
} else {
replayTrajectory(snapshot);
}
getLogger().info("Воспроизведение закончено. Начинаю новую запись...");
synchronized (recordedTrajectory) {
recordedTrajectory.clear();
}
validateLoadModel();
startGravCompOnTool();
startRecordingThread();
} else if (choise == 1) {
stopRecordingThread();
stopGravComp();
synchronized (recordedTrajectory) {
recordedTrajectory.clear();
}
getLogger().info("Запись сброшена. Начинаю заново...");
validateLoadModel();
startGravCompOnTool();
startRecordingThread();
} else {
stopRecordingThread();
stopGravComp();
inMode = false;
}
}
getLogger().info("Режим 2 завершён. Тормоз активирован.");
}
// ==== Вспомогательные методоы ====
// Запись данных
private void startRecordingThread() {
stopRecordingFlag.set(false);
recordingThread = new Thread(new Runnable() {
@Override
public void run() {
recordingLoop();
}
}, "TrajectoryRecorder");
recordingThread.setDaemon(true);
recordingThread.start();
getLogger().info("[Запись] Запущена. Интервал: "
+ RECORD_INTERVAL_MS + " мс.");
}
private void recordingLoop() {
getLogger().info("[Запись] Поток запущен.");
while (!stopRecordingFlag.get()) {
try {
JointPosition pose = _lbr.getCurrentJointPosition();
synchronized (recordedTrajectory) {
if (recordedTrajectory.size() >= MAX_TRAJECTORY_POINTS) {
getLogger().warn("[Запись] Лимит "
+ MAX_TRAJECTORY_POINTS + " точек достигнут."
+ " Запись остановлена.");
break;
}
recordedTrajectory.add(pose);
}
Thread.sleep(RECORD_INTERVAL_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
getLogger().error("[Запись] Ошибка: " + e.getMessage());
break;
}
}
getLogger().info("[Запись] Поток остановлен. Точек: "
+ recordedTrajectory.size());
}
private void stopRecordingThread() {
stopRecordingFlag.set(true);
if (recordingThread != null && recordingThread.isAlive()) {
recordingThread.interrupt();
try {
recordingThread.join(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
recordingThread = null;
}
}
// Воспроизведение траекторий
private void replayTrajectory(List<JointPosition> trajectory) throws Exception {
int total = trajectory.size();
getLogger().info("===========================================");
getLogger().info(" ВОСПРОИЗВЕДЕНИЕ | Точек: " + total
+ " | Скорость: " + (int)(REPLAY_VELOCITY_REL * 100) + "%");
getLogger().info(" Старт через " + (PRE_REPLAY_DELAY_MS / 1000)
+ " сек. Отойдите от робота!");
getLogger().info("===========================================");
Thread.sleep(PRE_REPLAY_DELAY_MS);
getLogger().info("Перемещаюсь к начальной точке...");
_lbr.move(
BasicMotions.ptp(trajectory.get(0))
.setJointVelocityRel(REPLAY_VELOCITY_REL)
);
getLogger().info("Начальная точка достигнута.");
SmartServo replayServo = new SmartServo(trajectory.get(0));
replayServo.setMinimumTrajectoryExecutionTime(8e-3);
replayServo.setTimeoutAfterGoalReach(3600);
replayServo.setJointVelocityRel(REPLAY_VELOCITY_REL);
_gripper.getDefaultMotionFrame()
.moveAsync(replayServo.setMode(new PositionControlMode()));
ISmartServoRuntime replayRuntime = replayServo.getRuntime();
getLogger().info("SmartServo активирован. Воспроизведение...");
int pauseCount = 0;
long totalWaitMs = 0L;
for (int i = 1; i < total; i++) {
if (isObstacleDetected(replayRuntime)) {
pauseCount++;
int waitCount = 0;
getLogger().warn("ПРЕПЯТСТВИЕ на точке " + i + "/" + total
+ ". Удерживаю позицию...");
while (isObstacleDetected(replayRuntime)) {
replayRuntime.setDestination(_lbr.getCurrentJointPosition());
Thread.sleep(OBSTACLE_POLL_INTERVAL_MS);
waitCount++;
totalWaitMs += OBSTACLE_POLL_INTERVAL_MS;
if (waitCount % 30 == 0) {
getLogger().warn(" Ожидание: "
+ (waitCount * OBSTACLE_POLL_INTERVAL_MS / 1000)
+ " сек. Освободите путь.");
}
}
getLogger().info("Путь свободен. Продолжаю с точки "
+ i + "/" + total);
}
replayRuntime.setDestination(trajectory.get(i));
Thread.sleep(RECORD_INTERVAL_MS);
}
// stopMotion ПОСЛЕ цикла, не внутри него
replayRuntime.stopMotion();
getLogger().info("===========================================");
getLogger().info(" ГОТОВО | Точек: " + total
+ " | Пауз: " + pauseCount
+ (pauseCount > 0
? " | Ожидание: " + (totalWaitMs / 1000.0) + " сек"
: ""));
getLogger().info("===========================================");
}
// Проверка наличия контакта с препятсвием
private boolean isObstacleDetected(ISmartServoRuntime runtime) {
try {
double[] tauExt = runtime.getAxisTauExtMsr();
if (tauExt == null) {
return false;
}
for (int j = 0; j < tauExt.length; j++) {
if (Math.abs(tauExt[j]) > COLLISION_TORQUE_THRESHOLD_NM) {
getLogger().info(String.format(
" [Детекция] J%d: tau_ext=%.2f Нм (порог %.1f Нм)",
j + 1, tauExt[j], COLLISION_TORQUE_THRESHOLD_NM));
return true;
}
}
} catch (Exception e) {
getLogger().warn("[Детекция] Ошибка: " + e.getMessage());
}
return false;
}
// Компенсация массы робота
private void startGravCompOnTool() throws InterruptedException {
CartesianImpedanceControlMode gravComp = createGravityCompMode();
SmartServo smartServo = new SmartServo(_lbr.getCurrentJointPosition());
smartServo.setMinimumTrajectoryExecutionTime(8e-3);
smartServo.setTimeoutAfterGoalReach(3600);
smartServo.setJointVelocityRel(0.5);
activeMotionContainer = _gripper
.getDefaultMotionFrame()
.moveAsync(smartServo.setMode(gravComp));
gravCompRuntime = smartServo.getRuntime();
stopGravCompFlag.set(false);
gravCompThread = new Thread(new Runnable() {
@Override
public void run() {
gravCompAnchorLoop();
}
}, "GravCompAnchorUpdater");
gravCompThread.setDaemon(true);
gravCompThread.start();
getLogger().info("Гравкомпенсация активна. Якорь следует за роботом.");
}
private void gravCompAnchorLoop() {
getLogger().info("[GravComp] Поток обновления якоря запущен.");
while (!stopGravCompFlag.get()) {
try {
ISmartServoRuntime rt = gravCompRuntime;
if (rt != null) {
rt.setDestination(_lbr.getCurrentJointPosition());
}
Thread.sleep(GRAV_COMP_UPDATE_INTERVAL_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
getLogger().warn("[GravComp] Ошибка обновления: " + e.getMessage());
break;
}
}
getLogger().info("[GravComp] Поток обновления остановлен.");
}
private void stopGravComp() {
stopGravCompThread();
ISmartServoRuntime rt = gravCompRuntime;
if (rt != null) {
try {
rt.stopMotion();
getLogger().info("SmartServo остановлен. Тормоз активирован.");
} catch (Exception e) {
getLogger().warn("Ошибка stopMotion(): " + e.getMessage());
cancelActiveMotion();
} finally {
gravCompRuntime = null;
activeMotionContainer = null;
}
}
}
private void stopGravCompThread() {
stopGravCompFlag.set(true);
if (gravCompThread != null && gravCompThread.isAlive()) {
gravCompThread.interrupt();
try {
gravCompThread.join(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
gravCompThread = null;
}
}
// Утилиты
private void logCurrentPosition() {
getLogger().info("===========================================");
Frame flangeInWorld = _lbr.getCurrentCartesianPosition(_lbr.getFlange());
double x_mm = flangeInWorld.getX();
double y_mm = flangeInWorld.getY();
double z_mm = flangeInWorld.getZ();
double a_deg = Math.toDegrees(flangeInWorld.getAlphaRad());
double b_deg = Math.toDegrees(flangeInWorld.getBetaRad());
double c_deg = Math.toDegrees(flangeInWorld.getGammaRad());
getLogger().info(String.format(" XYZ [мм]: X=%9.3f Y=%9.3f Z=%9.3f", x_mm, y_mm, z_mm));
getLogger().info(String.format(" ABC [мм]: [ ° ]: A=%9.3f B=%9.3f C=%9.3f", a_deg, b_deg, c_deg));
JointPosition joints = _lbr.getCurrentJointPosition();
int n = _lbr.getJointCount();
StringBuilder rowDeg = new StringBuilder(" Суставы [ ° ]: ");
StringBuilder rowRad = new StringBuilder(" Суставы [рад]: ");
for (int i=0; i<n; i++) {
double rad = joints.get(i);
rowDeg.append(String.format("J%d=%7.2f ", i + 1, Math.toDegrees(rad)));
rowRad.append(String.format("J%d=%7.4f ", i + 1, rad));
}
getLogger().info(rowDeg.toString());
getLogger().info(rowRad.toString());
getLogger().info("===========================================");
}
private CartesianImpedanceControlMode createGravityCompMode() {
CartesianImpedanceControlMode mode = new CartesianImpedanceControlMode();
mode.parametrize(CartDOF.TRANSL)
.setStiffness(GRAV_COMP_STIFFNESS_TRANSL)
.setDamping(GRAV_COMP_DAMPING);
mode.parametrize(CartDOF.ROT)
.setStiffness(GRAV_COMP_STIFFNESS_ROT)
.setDamping(GRAV_COMP_DAMPING);
return mode;
}
private void validateLoadModel() {
getLogger().info("Валидация динамической модели нагрузки...");
boolean valid = SmartServo.validateForImpedanceMode(_gripper);
if (valid) {
getLogger().info("Динамическая модель нагрузки валидна. "
+ "Гравкомпенсация будет корректной.");
} else {
getLogger().warn("ВНИМАНИЕ: Валидация динамической модели "
+ "не прошла! Проверьте load data захвата в "
+ "Sunrise Workbench -> Object Templates -> "
+ _gripper.getName() + " -> Load data.");
getLogger().warn("Гравкомпенсация может работать некорректно.");
}
}
/**
* Перемещает в Home (все оси = 0
*/
private void moveToHomePosition() {
getLogger().info("Перемещение в Home...");
_lbr.move(
BasicMotions.ptp(HOME_POSITION_RAD)
.setJointVelocityRel(APPROACH_VELOCITY_REL)
);
getLogger().info("Home достигнута.");
}
// Перемещает в рабочую позицию через Home
private void moveToWorkingPosition() throws Exception {
getLogger().info("Перемещение в рабочую позицию ...");
_lbr.move(
BasicMotions.batch(
BasicMotions.ptp(HOME_POSITION_RAD)
.setBlendingRel(HOME_BLENDING_REL),
BasicMotions.ptp(WORKING_POSITION_RAD)
).setJointVelocityRel(APPROACH_VELOCITY_REL)
);
getLogger().info("Рабочая позиция достигнута");
}
private void cancelActiveMotion() {
if (activeMotionContainer != null) {
try {
activeMotionContainer.cancel();
getLogger().info("Движение отменено. Тормоз активирован.");
} catch (Exception e) {
getLogger().warn("Ошибка при cancel(): " + e.getMessage());
} finally {
activeMotionContainer = null;
}
}
}
}