-
Notifications
You must be signed in to change notification settings - Fork 0
Gabos111/fish-control
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
# AUTHOR : GABRIEL VEIGAS MARQUES
# MASTER PROJECT : BIOMIMETIC FISH OSCILLATORY GAIT
# 🐟 Fish Robot Controller
This repository contains two pieces:
1. **C++ Core** (`fish_control`):
- Reads `cfg.yaml`, drives Dynamixel tail & RC-servo fin, logs data.
2. **Python GUI** (`fish_control_client.py`):
- Remote dashboard over SSH to edit `cfg.yaml` and toggle logging.
---
## 📁 Repository Layout
fish-control/
├── cfg.yaml # Main config for both core & GUI
├── cpp/ # C++ core source
│ ├── CMakeLists.txt
│ ├── src/
│ └── include/
├── fish_control/ # Python GUI
│ ├── fish_control_client.py
│ └── ui.py # (optional alternative GUI)
├── logs/ # CSV logs written by C++ core
└── systemd/
├── fish.service
└── fish-gui.service
---
## ⚙️ Prerequisites
### On Raspberry Pi
- **C++ core**:
- `g++`, `cmake`, `make`
- `yaml-cpp` development headers (`libyaml-cpp-dev`)
- Dynamixel SDK (installed via CMake below)
- `pigpio` library & daemon (`sudo apt install pigpio`)
- **Python GUI**:
- Python 3 (3.7+)
- `paramiko`, `pyyaml`
- `tkinter` (e.g. `sudo apt install python3-tk`)
---
## 🏗️ Build & Install C++ Core
```bash
# 1. Install DynamixelSDK:
cd ~/DynamixelSDK
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=ON \
-DCMAKE_INSTALL_PREFIX=/usr/local \
..
make -j4
sudo make install
sudo ldconfig
# 2. Build this project's C++ core:
cd ~/projects/fish-control/cpp/build
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make -j4
sudo make install
This installs the fish_control binary into /usr/local/bin/.
⸻
🐍 Set Up Python GUI
# Create & activate your venv
cd ~/projects/fish-control
python3 -m venv venv-fish
source venv-fish/bin/activate
# Install Python deps
pip install paramiko pyyaml
# Ensure tkinter is available
sudo apt install python3-tk
To run manually:
source venv-fish/bin/activate
python fish_control_client.py
⸻
🛠️ Systemd Services
Copy unit files and enable at boot:
sudo cp systemd/fish.service /etc/systemd/system/
sudo cp systemd/fish-gui.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable fish.service fish-gui.service
sudo systemctl start fish.service fish-gui.service
• fish.service runs the C++ core on boot.
• fish-gui.service runs the Python dashboard.
⸻
🔧 Configuration (cfg.yaml)
Located at /home/fish_pizero/projects/fish-control/cfg.yaml.
Fields:
mode: "symmetric_sin" # "standby", "test", or "symmetric_sin"
amplitude_tail: 20.0
amplitude_fin: 20.0
frequency: 0.3
phase: 0.0
phi_tail: 0.0 # only in "test" mode
phi_fin: 0.0 # only in "test" mode
logging: false # true to start logging
⸻
🚀 Running & Using
0. Should start running the fish.service on Pi boot
1. Start pigpio:
sudo systemctl enable pigpiod
sudo systemctl start pigpiod
2. Launch services:
sudo systemctl start fish.service
sudo systemctl start fish-gui.service
3. Open GUI (locally or via SSH-X):
source ~/projects/fish-control/venv-fish/bin/activate
python fish_control_client.py
4. In the GUI:
• Select Host → Connect.
• Load reads the current cfg.yaml.
• Modify fields and Save.
• Start/Stop Logging toggles log streaming.
5. Logs appear in projects/fish-control/logs/log_YYYYMMDD_HHMMSS.csv.
⸻
🔄 After Code Changes
• C++ changes:
cd ~/projects/fish-control/cpp/build
cmake ..
make -j4
sudo make install
sudo systemctl restart fish.service
• Python GUI changes:
sudo systemctl restart fish-gui.service
# or re-run python fish_control_client.py
below the fish-gui.service code (run on personnal computer):
```python
#!/usr/bin/env python3
import yaml
import paramiko
import tkinter as tk
from tkinter import messagebox, font, ttk
import traceback # for full error dumps
# remote user + where the Pi's cfg.yaml lives
PI_USER = "fish_pizero"
REMOTE_CFG = "/home/fish_pizero/projects/fish-control/cfg.yaml"
# name -> host-address
HOST_OPTIONS = {
"EPFL": "128.179.200.41",
"Local": "fishpizero.local",
"EPFL2": "128.179.204.90",
"Unknown": "172.20.10.2",
}
class MacDashboard(tk.Tk):
def __init__(self):
super().__init__()
self.title("🐟 Fish Remote Dashboard")
self.resizable(False, False)
# ── make a legible default font ──
default_font = font.Font(family="Helvetica", size=11)
self.option_add("*Font", default_font)
# ── Host selector + Connect button + status ──
ttk.Label(self, text="Host:")\
.grid(row=0, column=0, sticky="w", padx=5, pady=5)
self.host_combo = ttk.Combobox(
self, values=list(HOST_OPTIONS.keys()), state="readonly", width=12
)
self.host_combo.set("EPFL")
self.host_combo.grid(row=0, column=1, sticky="ew", padx=5)
ttk.Button(self, text="Connect", command=self.on_connect)\
.grid(row=0, column=2, padx=5)
self.status_lbl = ttk.Label(self, text="Not connected", foreground="red")
self.status_lbl.grid(row=0, column=3, padx=5)
# ── Parameter fields ──
self.fields = {}
params = [
("mode", ["standby","test","symmetric_sin"]),
("amplitude_tail", None),
("amplitude_fin", None),
("frequency", None),
("phase", None),
("phi_tail", None),
("phi_fin", None),
]
for i,(name,choices) in enumerate(params, start=1):
label = name.replace("_"," ").title() + ":"
ttk.Label(self, text=label)\
.grid(row=i, column=0, sticky="e", padx=5, pady=2)
var = tk.StringVar()
self.fields[name] = var
if choices:
w = ttk.Combobox(self, textvariable=var, values=choices, state="readonly")
else:
w = ttk.Entry(self, textvariable=var)
w.grid(row=i, column=1, columnspan=3, sticky="ew", padx=5, pady=2)
# ── Load / Save buttons ──
ttk.Button(self, text="Load from Pi", command=self.load_from_pi)\
.grid(row=8, column=0, columnspan=2, pady=(15,5), padx=5, sticky="ew")
ttk.Button(self, text="Save to Pi", command=self.save_to_pi)\
.grid(row=8, column=2, columnspan=2, pady=(15,5), padx=5, sticky="ew")
# ── Single Start/Stop Logging toggle button ──
self.log_btn = ttk.Button(self,
text="Start Logging",
command=self.toggle_logging
)
self.log_btn.grid(row=9, column=0, columnspan=4,
pady=(5,10), padx=5, sticky="ew")
# ── Let columns stretch ──
for c in range(4):
self.columnconfigure(c, weight=1)
def _ssh_sftp(self):
"""Return an (ssh, sftp) pair connected to the selected host."""
host_key = self.host_combo.get()
if host_key not in HOST_OPTIONS:
raise RuntimeError(f"Unknown host “{host_key}”")
ssh = paramiko.SSHClient()
ssh.load_system_host_keys()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(HOST_OPTIONS[host_key], username=PI_USER, timeout=5)
return ssh, ssh.open_sftp()
def on_connect(self):
try:
ssh, sftp = self._ssh_sftp()
sftp.close(); ssh.close()
self.status_lbl.config(text="Connected ✔️", foreground="green")
except Exception as e:
self.status_lbl.config(text=f"Error: {e}", foreground="red")
def load_from_pi(self):
try:
ssh, sftp = self._ssh_sftp()
with sftp.open(REMOTE_CFG, 'r') as f:
cfg = yaml.safe_load(f)
sftp.close(); ssh.close()
# fill in all fields
for name,var in self.fields.items():
if name in cfg:
var.set(str(cfg[name]))
# sync logging button
current = cfg.get("logging", False)
self.log_btn.config(text="Stop Logging" if current else "Start Logging")
self.status_lbl.config(text="Loaded ✔️", foreground="green")
except Exception as e:
traceback.print_exc()
self.status_lbl.config(text=f"Error: {e}", foreground="red")
def save_to_pi(self):
try:
# first read existing config to preserve logging flag
ssh, sftp = self._ssh_sftp()
with sftp.open(REMOTE_CFG, 'r') as f:
old_cfg = yaml.safe_load(f)
# build new config from UI fields
new_cfg = {
name: (var.get() if name=="mode" else float(var.get()))
for name,var in self.fields.items()
}
# preserve logging state
new_cfg["logging"] = old_cfg.get("logging", False)
# write directly to the config file
with sftp.open(REMOTE_CFG, 'w') as f:
f.write(yaml.safe_dump(new_cfg))
sftp.close(); ssh.close()
messagebox.showinfo("Saved", "Configuration written to Pi.")
self.status_lbl.config(text="Saved ✔️", foreground="green")
except Exception as e:
traceback.print_exc()
self.status_lbl.config(text=f"Error: {e}", foreground="red")
def toggle_logging(self):
"""Toggle remote logging on/off and update button text."""
try:
ssh, sftp = self._ssh_sftp()
# read current
with sftp.open(REMOTE_CFG, 'r') as f:
cfg = yaml.safe_load(f)
new_state = not cfg.get("logging", False)
cfg["logging"] = new_state
# write logging flag directly
with sftp.open(REMOTE_CFG, 'w') as f:
f.write(yaml.safe_dump(cfg))
sftp.close(); ssh.close()
# update UI
self.log_btn.config(text="Stop Logging" if new_state else "Start Logging")
self.status_lbl.config(
text=("Logging On ✔️" if new_state else "Logging Off ✔️"),
foreground="green"
)
except Exception as e:
traceback.print_exc()
self.status_lbl.config(text=f"Error: {e}", foreground="red")
if __name__ == "__main__":
app = MacDashboard()
app.mainloop()
```
⸻
🐞 Troubleshooting
• Port errors: check /dev/ttyUSB0, permissions (sudo usermod -aG dialout $USER).
• Dynamixel ping fail: ensure U2D2 green LED & proper wiring.
• GUI blank: confirm tkinter in venv:
python -c "import tkinter; tkinter._test()"
• Logging not toggling: verify logging: field in cfg.yaml.
⸻
Happy fish-driving! 🐟
About
MasterThesis
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published