Fmrrehab is a comprehensive rehabilitation robotics platform built on ROS 2 with advanced motion control, force-torque sensing, and safety-critical PLC integration. The system coordinates a 3-DOF rehabilitation arm through a real-time control architecture with emergency stop chains and sensor-based impedance control.
Dual Controller Manager Setup The system uses two separate controller-manager instances for safety isolation:
The safety PLC module installed in the FMRREHAB platform is managed by the following ros2 nodes at a 500 Hz control loop: PLC Controller Manager └─ PLC Controller - Reads/writes 8 GPIO command outputs - Reads 8 GPIO state inputs - Publishes PlcStates for subscribers
The following controllers are automathically loaded and will run at 250 Hz:
- Joint Trajectory Controller
- Forward Velocity Controller
- Impedance Controller
- Force/Torque Sensor Broadcaster
(PyQt5 Interface with SSH Remote Access) High-Level user interface with SSH connection for remote launching and UDP client for status monitoring
Master configuration package containing all URDF definitions, controller YAML configs, and launch files.
ROS 2 node managing system lifecycle through PLC state transitions.
C++ controller plugin implementing GPIO bridge between ROS 2 and Safety PLC.
This package provides the primary motion control and sensor management modules needed by the motion control stack.
Multi-Platform user interface developed with QT managing robot high-level control by the user and rehabilitation exercise configuration.
launch_ros2_env.sh Launches ros2 control framework and default controllers, managing two cases:
- first launch: homing process performed
- launch after an emergency stop by the user: no homing needed
kill_ros_apps.sh
- Gracefully unspawn all active controllers
- Unload inactive controllers
- Coordinate with PLC manager for clean shutdown
fit4med_to_robot_sync.sh / fit4med_from_robot_sync.sh
- To robot: make each folder of the workspace
srccontaining your checkout identical on the robot (robot 192.168.1.1,--dry-runto preview). Robot folders you don't have locally are left untouched. It first offers a backup, and asks before deleting files that exist only on the robot. - From robot: pull the robot's changes, keeping local files that are newer.
fit4med_backup.sh / fit4med_restore.sh
- Back up the robot sources to
/home/fit4med/bkp/YYYYMMDD/HHMM/, or list the backups and restore one. - Run them on the robot, or from your PC (they work through ssh;
--host <ip>/--localto force).
- fmrr_gui.ps1 - launch GUI
- fmrr_bringup.ps1 - SSH into Linux, start ROS2 stack via PLC launcher
- fmrr_cleanup.ps1 - kill the local GUI (and whatever holds UDP 5005), then SSH into Linux and shut down all nodes (
-Target gui|remoteto do only one side) - fmrr_scp.ps1 - Transfer files remotely between Windows & Linux
- fmrr_to_robot_sync.ps1 / fmrr_backup.ps1 / fmrr_restore.ps1 - Windows versions of the robot sync, backup and restore scripts (see bash_scripts/README.md)
- PlcController.msg - Command messages to PLC
- PlcStates.msg - State feedback from PLC
- SetExercise.srv - Exercise execution request
- SetTrajectory.srv - Tajectory execution request
Important
Let fit4med read the system journal. Run once on the robot PC:
sudo usermod -aG systemd-journal fit4medIt takes effect for processes started after the next login (for ssh: the
next connection; for the bring-up service: after a reboot, see the end of
bash_scripts/README.md). Without it, the log archive of each bring-up
misses the EtherCAT side (the timeline file only starts with a warning):
ethercat.service and the EtherCAT master kernel messages are system logs,
readable only by root or members of systemd-journal. The [ETHERCAT] and
[KERNEL] lines of the merged timeline (journal/fit4med_ethercat_timeline.log)
would then be empty, and it would no longer show whether a failure starts on
the EtherCAT network or in the controller.
Check: id fit4med must list systemd-journal.
The GUI is a plain Python/PyQt5 application: it never uses rclpy, it talks to
the platform over rosbridge (TCP 9090) and receives status over UDP 5005
(sent by plc_manager from UDP 5006, where the GUI replies with its ROS state).
No ROS 2 installation is required on the Windows PC.
Two addresses matter and only one of them ever changes:
| Setting | Value | Where |
|---|---|---|
--remote-ip |
192.168.1.1 (ROS 2 PC) — never changes |
ps_scripts/fmrr_gui.ps1 |
gui_ip |
the static IP of this Windows PC | -GuiIp argument of ps_scripts/fmrr_bringup.ps1 |
Give the PC a static address on the platform subnet 192.168.1.0/24.
192.168.1.1 is the ROS 2 PC and 192.168.1.2 is the platform's own GUI PC, so
a second machine needs a free address (e.g. 192.168.1.57). That address is the
gui_ip: plc_manager sends the UDP status stream to it, and nothing else
tells it where the GUI lives.
Allow the status stream in, from an Administrator PowerShell:
New-NetFirewallRule -DisplayName "FMRREHAB GUI status" -Direction Inbound -Protocol UDP -LocalPort 5005 -Action AllowThe bringup/cleanup scripts use the Windows OpenSSH client (included in Windows
10/11) to log into fit4med@192.168.1.1, so make sure ssh 192.168.1.1 works
before going further.
winget install --id=astral-sh.uv -eor, without winget:
irm https://astral.sh/uv/install.ps1 | iexClose and reopen PowerShell, then check uv --version. Python itself does not
need to be installed: uv downloads the interpreter this project asks for
(CPython >= 3.10, < 3.13).
git clone https://github.com/CNR-STIIMA-IRAS/Fit4Med.git
cd Fit4MedAny folder works — the PowerShell scripts resolve their paths relative to their
own location, so the repository no longer has to sit in C:\Fit4Med.
uv syncThis creates .venv in the repository root with PyQt5, roslibpy, NumPy, SciPy,
Matplotlib, psutil, PyYAML and rich, as pinned by pyproject.toml / uv.lock.
Movements and protocols are read from rehab_gui/Movements and
rehab_gui/Protocols inside the clone.
Set-ExecutionPolicy -Scope CurrentUser RemoteSignedAlternatively, launch each script as
powershell -ExecutionPolicy Bypass -File ps_scripts\fmrr_gui.ps1.
.\ps_scripts\fmrr_bringup.ps1 -GuiIp 192.168.1.57This SSHes into the ROS 2 PC and runs
ros2 launch tecnobody_workbench run_sickPLC.launch.py gui_ip:=192.168.1.57.
Without -GuiIp it falls back to the platform PC (192.168.1.2), which is the
behaviour on the machine in the lab. Leave the window open: it holds the ROS 2
session.
.\ps_scripts\fmrr_gui.ps1The script uses .venv\Scripts\python.exe when the repository has an
environment and the system python otherwise, and always connects to
--remote-ip 192.168.1.1. The equivalent manual command is:
uv run python rehab_gui\rehab_gui\FMRRMainProgram.py --remote-ip 192.168.1.1 --maximise-window.\ps_scripts\fmrr_cleanup.ps1Every bring-up is archived on the robot by itself, nothing has to be run:
when run_sickPLC.launch.py exits (cleanup script, systemctl stop, Ctrl-C,
plc_manager exit), it writes
/home/fit4med/.ros/fit4med_log/run_NNNN_YYYYMMDD-HHMMSS.zip. NNNN grows by
one at each bring-up; the robot clock may be wrong, the number is not. The zip
contains:
| Path in the zip | Content |
|---|---|
session.txt |
start/end time, systemd unit, GUI IP, why the launch stopped |
sickPLC/ |
ROS logs of run_sickPLC.launch.py and its nodes (plc_manager, ...) |
starts/NNN_<label>_HHMMSS/ |
one folder per start of launch_ros2_env.sh (platform), launch_ros2_env_z_recovery.sh (z_recovery) and launch_ros2_bridge.sh (rosbridge), numbered in order, so each restart after an e-stop is separate: console.log (whole output of the script, EtherCAT checks included), run.txt (start/end, exit code) and the ROS logs of that start |
journal/fit4med_bringup.log |
journalctl --user -u fit4med-bringup@<ip>.service, this bring-up only |
journal/ethercat_service.log |
ethercat.service status and journal, EtherCAT kernel messages |
journal/fit4med_ethercat_timeline.log |
the two above merged in one timeline |
ethercat_start.txt, ethercat_end.txt |
ethercat master and ethercat slaves -v at start and end |
ros_home_log/ |
what was left in ~/.ros/log (what log.sh used to archive) |
If the launch is killed without closing (SIGKILL, power loss), its folder stays
open and is zipped at the next bring-up, with the journal up to its last log
write. Disable the archive with FIT4MED_SESSION_LOG=0; the script is
bash_scripts/fit4med_session_log.sh.
To copy the archives to the GUI PC:
.\ps_scripts\fmrr_retrieve_logs.ps1 # the last 3 bring-ups
.\ps_scripts\fmrr_retrieve_logs.ps1 -Last 10 # the last 10
.\ps_scripts\fmrr_retrieve_logs.ps1 -All # all of themThe archives land in Desktop\fit4med_logs\; those already there are not
copied again. The script lists what it copied, with start time and how each
bring-up ended. The last archive is often not the one with the problem: after a
failure the platform is usually restarted, so look at the one before. A
bring-up still running is copied as run_NNNN_..._partial.zip (its logs and
journals up to now), replaced by the full archive at a later retrieve. Also
copied: the robot and PC clocks (clocks.txt) and the GUI logs (gui\). The
EtherCAT part of the journals needs the one-time step in
One-time setup on the robot PC.
Nothing deletes the archives on the robot. To empty the log folders, see
"Svuotare le cartelle dei log del robot" at the end of bash_scripts/README.md.
The window and the rosbridge connection work over TCP 9090, while the status
fields are fed by the UDP stream. An empty status panel therefore almost always
means the gui_ip given at step 6 is not this PC's address, or the firewall
rule of step 1 is missing.
Common Issues
-
EtherCAT PLC Not Operational
- Check EtherCAT slaves: ethercat slaves # Expected: "FLX0-GETC100 OP" | ethercat master # If not OP, check ethercat master
-
Homing Timeout
- Check motor connection (ESTOP signal must be active)
- Verify safe movement zone (no mechanical obstructions)
- Increase timeout in boot_hw.py if needed
-
GUI Not Receiving Status
- Verify UDP port 5005 is open
- Check network connectivity (SSH must work)
- Verify GUI IP in plc_manager launch: gui_ip:=<windows_ip>
-
Trajectory Execution Hangs
- Check joint_trajectory_controller state: ros2 control list_controllers
- Verify all joints in "active" state
- Check for controller timeouts in /tmp/launch logs
This project is licensed under the Apache-2.0 License. See LICENSE file for details.
For questions about this rehabilitation platform, contact Nicola Pedrocchi and Adriano Scibilia