Skip to content

Commit bdd8523

Browse files
committed
Merge branch 'develop' into 363-support-of-uv
2 parents e2616ea + 0f8bc9a commit bdd8523

31 files changed

+1181
-153
lines changed

.gitattributes

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
*.stl filter=lfs diff=lfs merge=lfs -text
1212
*.npz filter=lfs diff=lfs merge=lfs -text
1313
*.onnx filter=lfs diff=lfs merge=lfs -text
14-
*.svg filter=lfs diff=lfs merge=lfs -text
14+
*.bin filter=lfs diff=lfs merge=lfs -text
15+
*.svg filter=lfs diff=lfs merge=lfs -text

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
Reachy Mini's hardware comes in two flavors:
1212
- **Reachy Mini lite**: where the robot is directly connected to your computer via USB. And the code that controls the robot (the daemon) runs on your computer.
13-
- **Reachy Mini wireless**: where an Raspberry Pi is embedded in the robot, and the code that controls the robot (the daemon) runs on the Raspberry Pi. You can connect to it via Wi-Fi from your computer. (TODO: add link to section on how to set it up)
13+
- **Reachy Mini wireless**: where an Raspberry Pi is embedded in the robot, and the code that controls the robot (the daemon) runs on the Raspberry Pi. You can connect to it via Wi-Fi from your computer (see [Wireless Setup](./docs/wireless-version.md)).
1414

1515
There is also a simulated version of Reachy Mini in [MuJoCo](https://mujoco.org) that you can use to prototype your applications before deploying them on the real robot. It behaves like the lite version where the daemon runs on your computer.
1616

docs/assets/qrcode-ap.png

Lines changed: 3 additions & 0 deletions
Loading

docs/wireless-version.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Set up your Reachy Mini wireless
2+
3+
## Connect to a Wi-Fi network
4+
5+
1. Power on your Reachy Mini.
6+
2. Reachy Mini will create its own access point: "reachy-mini-ap". It should appear in the list of available Wi-Fi networks on your computer or smartphone after a few moments.
7+
3. Connect your computer to the `reachy-mini-ap` Wi-Fi network (password: `reachy-mini`). Or you can directly scan the QR-code below to join the network:
8+
9+
![QR-Code reachy-mini-ap](./assets/qrcode-ap.png)
10+
11+
4. Open a web browser and go to [http://reachy-mini.local:8000/settings](http://reachy-mini.local:8000/settings) to access the configuration page.
12+
5. Enter your Wi-Fi network credentials (SSID and password) and click "Connect".
13+
6. Wait a few moments for Reachy Mini to connect to your Wi-Fi network. The access point will disappear once connected. If the connection fails, Reachy Mini will restart the access point, and you can try again.

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "reachy_mini"
7-
version = "1.0.0"
7+
version = "1.1.0rc2"
88
authors = [{ name = "Pollen Robotics", email = "[email protected]" }]
99
description = ""
1010
readme = "README.md"
@@ -57,6 +57,11 @@ rerun = [
5757
# See https://github.com/rerun-io/rerun-loader-python-example-urdf/issues/12
5858
# "rerun-loader-urdf @ git+https://github.com/rerun-io/rerun-loader-python-example-urdf.git",
5959
]
60+
wireless-version = [
61+
"semver>=3,<4",
62+
"nmcli>=1.5",
63+
"pollen_BMI088_imu_library"
64+
]
6065

6166
[project.scripts]
6267
reachy-mini-daemon = "reachy_mini.daemon.app.main:main"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:71b6b59a2a7382fc11d5f8953b9614348b7cf8f276c96cc212b393237d32c677
3+
size 933888
Lines changed: 99 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,106 @@
1-
async function checkUpdates() {
2-
const statusElem = document.getElementById('status');
3-
const updateBtn = document.getElementById('update-btn');
4-
statusElem.textContent = 'Checking for updates...';
5-
updateBtn.style.display = 'none';
6-
7-
try {
8-
const response = await fetch('/update/available');
9-
const data = await response.json();
10-
if (data.update.reachy_mini) {
11-
statusElem.textContent = 'An update is available!';
12-
updateBtn.style.display = 'inline-block';
13-
} else {
14-
statusElem.textContent = 'Your system is up to date.';
15-
}
16-
} catch (e) {
17-
statusElem.textContent = 'Error checking for updates.';
18-
}
19-
}
20-
21-
async function triggerUpdate() {
22-
const statusElem = document.getElementById('status');
23-
statusElem.textContent = 'Updating...';
24-
try {
25-
const response = await fetch('/update/start', { method: 'POST' });
26-
if (response.ok) {
27-
await response.json().then(data => {
28-
connectLogsWebSocket(data.job_id);
1+
const updateManager = {
2+
busy: false,
3+
preRelease: false,
4+
5+
isUpdateAvailable: async () => {
6+
fetch('/update/available?pre_release=' + updateManager.preRelease)
7+
.then(response => response.json())
8+
.then(data => {
9+
return data.update.reachy_mini;
10+
}).catch(error => {
11+
console.error('Error checking for updates:', error);
2912
});
30-
statusElem.textContent = 'Update started!';
13+
},
3114

32-
} else {
33-
await response.json().then(data => {
34-
if (data.detail) {
35-
statusElem.textContent = 'Error: ' + data.detail;
15+
startUpdate: async () => {
16+
if (updateManager.busy) {
17+
console.warn('An update is already in progress.');
18+
return;
19+
}
20+
updateManager.busy = true;
21+
22+
fetch('/update/start?pre_release=' + updateManager.preRelease, { method: 'POST' })
23+
.then(response => {
24+
if (response.ok) {
25+
return response.json();
26+
} else {
27+
return response.json().then(data => {
28+
throw new Error(data.detail || 'Error starting update');
29+
});
3630
}
31+
})
32+
.then(data => {
33+
const jobId = data.job_id;
34+
updateManager.connectLogsWebSocket(jobId);
35+
})
36+
.catch(error => {
37+
console.error('Error triggering update:', error);
38+
updateManager.busy = false;
3739
});
40+
},
41+
42+
connectLogsWebSocket: (jobId) => {
43+
const updateModal = document.getElementById('update-modal');
44+
const updateModalTitle = updateModal.queryElementById('update-modal-title');
45+
const logsDiv = document.getElementById('update-logs');
46+
const closeButton = document.getElementById('update-modal-close-button');
47+
48+
updateModalTitle.textContent = 'Updating...';
49+
50+
closeButton.onclick = () => {
51+
installModal.classList.add('hidden');
52+
};
53+
closeButton.classList = "hidden";
54+
closeButton.textContent = '';
55+
56+
updateModal.classList.removeAttribute('hidden');
57+
58+
const ws = new WebSocket(`ws://${location.host}/api/update/ws/logs?job_id=${jobId}`);
59+
60+
ws.onmessage = (event) => {
61+
};
62+
ws.onclose = async () => {
63+
updateManager.busy = false;
64+
updateManager.updateUI();
65+
};
66+
},
67+
68+
updateUI: async () => {
69+
const isUpdateAvailable = await updateManager.isUpdateAvailable();
70+
71+
updateManager.updateMainPage(isUpdateAvailable);
72+
updateManager.updateUpdatePage(isUpdateAvailable);
73+
},
74+
75+
updateMainPage: async (isUpdateAvailable) => {
76+
const daemonUpdateBtn = document.getElementById('daemon-update-btn');
77+
if (!daemonUpdateBtn) return;
78+
79+
if (isUpdateAvailable) {
80+
daemonUpdateBtn.innerHTML = 'Update <span class="rounded-full bg-blue-700 text-white text-xs font-semibold px-2 py-1 ml-2">1</span>';
81+
} else {
82+
daemonUpdateBtn.innerHTML = 'Update';
3883
}
39-
} catch (e) {
40-
statusElem.textContent = 'Error triggering update.';
41-
}
42-
}
43-
44-
45-
function connectLogsWebSocket(jobId) {
46-
const logsElem = document.getElementById('logs');
47-
if (!logsElem) return;
48-
49-
let wsProto = window.location.protocol === 'https:' ? 'wss' : 'ws';
50-
let wsUrl = wsProto + '://' + window.location.host + '/update/ws/logs?job_id=' + jobId;
51-
const ws = new WebSocket(wsUrl);
52-
53-
ws.onmessage = function (event) {
54-
// Append new log line
55-
logsElem.textContent += event.data + '\n';
56-
logsElem.scrollTop = logsElem.scrollHeight;
57-
};
58-
ws.onerror = function () {
59-
logsElem.textContent += '[WebSocket error connecting to logs]\n';
60-
};
61-
ws.onclose = function () {
62-
logsElem.textContent += '[WebSocket closed]\n';
63-
64-
const statusElem = document.getElementById('status');
65-
statusElem.textContent = 'Update completed. Reloading in a 5 seconds...';
66-
67-
for (let i = 5; i > 0; i--) {
68-
setTimeout(() => {
69-
statusElem.textContent = 'Update completed. Reloading in ' + i + ' seconds...';
70-
}, (5 - i) * 1000);
84+
},
85+
updateUpdatePage: async (isUpdateAvailable) => {
86+
const statusElem = document.getElementById('update-status');
87+
if (!statusElem) return;
88+
89+
const checkAgainBtn = document.getElementById('check-again-btn');
90+
const startUpdateBtn = document.getElementById('start-update-btn');
91+
92+
if (isUpdateAvailable) {
93+
statusElem.innerHTML = 'An update is available!';
94+
checkAgainBtn.classList.add('hidden');
95+
startUpdateBtn.classList.remove('hidden');
96+
} else {
97+
statusElem.innerHTML = 'Your system is up to date.';
98+
checkAgainBtn.classList.remove('hidden');
99+
startUpdateBtn.classList.add('hidden');
71100
}
72-
setTimeout(() => {
73-
window.location.reload();
74-
}, 5000);
75-
};
76-
}
77-
78-
window.onload = function () {
79-
checkUpdates();
101+
}
80102
};
103+
104+
window.addEventListener('load', async () => {
105+
updateManager.updateUI();
106+
});

src/reachy_mini/daemon/app/dashboard/static/js/setup_wifi.js renamed to src/reachy_mini/daemon/app/dashboard/static/js/wifi.js

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
11

2-
window.onload = () => {
3-
refreshStatus();
4-
setInterval(refreshStatus, 1000);
5-
};
6-
7-
82
const refreshStatus = () => {
9-
const statusDiv = document.getElementById('status');
10-
113
fetch('/wifi/status')
124
.then(response => response.json())
135
.then(data => {
14-
statusDiv.innerText = `Status: ${data}`;
15-
166
handleStatus(data);
177
})
188
.catch(error => {
199
console.error('Error fetching WiFi status:', error);
20-
statusDiv.innerText = 'Make sure you are connected on the right WiFi.\n Attempt to reconnect...';
10+
handleStatus('error');
2111
});
2212

2313
fetch('/wifi/error')
@@ -32,7 +22,7 @@ const refreshStatus = () => {
3222
.catch(error => {
3323
console.error('Error fetching WiFi error:', error);
3424
});
35-
}
25+
};
3626

3727
const connectToWifi = (_) => {
3828
const ssid = document.getElementById('ssid').value;
@@ -66,29 +56,35 @@ const connectToWifi = (_) => {
6656
alert(`Error connecting to WiFi: ${error.message}`);
6757
});
6858
return false; // Prevent form submission
69-
}
59+
};
7060

7161
const handleStatus = (status) => {
62+
const statusDiv = document.getElementById('wifi-status');
7263
const addWifiDiv = document.getElementById('add-wifi');
73-
addWifiDiv.hidden = true;
74-
7564
const busyDiv = document.getElementById('busy');
76-
busyDiv.hidden = true;
7765

78-
const connectedDiv = document.getElementById('connected');
79-
connectedDiv.hidden = true;
66+
addWifiDiv.classList = 'hidden';
8067

81-
if (status === 'hotspot') {
82-
addWifiDiv.hidden = false;
83-
} else if (status === 'wlan') {
84-
connectedDiv.hidden = false;
85-
}
86-
else if (status === 'busy') {
68+
if (status == 'hotspot') {
69+
statusDiv.innerText = 'Hotspot mode active. 🔌';
70+
addWifiDiv.classList.remove('hidden');
71+
} else if (status == 'wlan') {
72+
statusDiv.innerText = 'Connected to WiFi. 📶';
73+
} else if (status == 'disconnected') {
74+
statusDiv.innerText = 'WiFi disconnected. ❌';
75+
} else if (status == 'busy') {
76+
statusDiv.innerText = 'Changing your WiFi configuration... Please wait ⏳';
8777
busyDiv.hidden = false;
88-
}
89-
else {
78+
} else if (status == 'error') {
79+
statusDiv.innerText = 'Error connecting to WiFi. ⚠️';
80+
} else {
9081
console.warn(`Unknown status: ${status}`);
9182
}
9283

9384
currentStatus = status;
94-
};
85+
};
86+
87+
window.addEventListener('load', () => {
88+
refreshStatus();
89+
setInterval(refreshStatus, 1000);
90+
});

src/reachy_mini/daemon/app/dashboard/static/style.css

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,17 @@ body {
8080
display: flex;
8181
align-items: center;
8282
justify-content: center;
83-
}
83+
}
84+
85+
.wifi-status {
86+
font-family: 'Asap', sans-serif;
87+
font-weight: 500;
88+
color: rgba(0, 0, 0, 0.9);
89+
90+
}
91+
92+
.wifi-status-info {
93+
font-family: 'Asap', sans-serif;
94+
font-weight: 400;
95+
color: rgba(0, 0, 0, 0.7);
96+
}

src/reachy_mini/daemon/app/dashboard/templates/sections/daemon.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,13 @@
3333
<div class="mt-4">
3434
{% include 'sections/move_player.html' %}
3535
</div>
36+
{% if args.wireless_version %}
37+
<div class="w-full flex pt-4 gap-4 justify-center">
38+
<a id="daemon-settings-btn"
39+
class="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-300 rounded-lg shadow border border-gray-300"
40+
href="/settings">
41+
Settings
42+
</a>
43+
</div>
44+
{% endif %}
3645
</section>

0 commit comments

Comments
 (0)