feat: gyro recalibration button in web UI (#32)

Add 'G' CDC command that disarms and re-runs gyro bias calibration.
safety_refresh() added to calibration loop (every 40ms) so IWDG
does not trip during the 1s blocking re-cal when watchdog is running.
GYRO CAL button in ui/index.html sends 'G' and shows status feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sl-firmware 2026-02-28 21:50:28 -05:00
parent f867956b43
commit bd30e2b40d
5 changed files with 25 additions and 1 deletions

View File

@ -6,6 +6,7 @@ volatile uint8_t cdc_streaming = 1; /* auto-stream */
static volatile uint8_t cdc_port_open = 0; /* set when host asserts DTR */ static volatile uint8_t cdc_port_open = 0; /* set when host asserts DTR */
volatile uint8_t cdc_arm_request = 0; /* set by A command */ volatile uint8_t cdc_arm_request = 0; /* set by A command */
volatile uint8_t cdc_disarm_request = 0; /* set by D command */ volatile uint8_t cdc_disarm_request = 0; /* set by D command */
volatile uint8_t cdc_recal_request = 0; /* set by G command — gyro recalibration */
/* /*
* PID tuning command buffer. * PID tuning command buffer.
@ -138,6 +139,7 @@ static int8_t CDC_Receive(uint8_t *buf, uint32_t *len) {
case 'S': cdc_streaming = !cdc_streaming; break; case 'S': cdc_streaming = !cdc_streaming; break;
case 'A': cdc_arm_request = 1; break; case 'A': cdc_arm_request = 1; break;
case 'D': cdc_disarm_request = 1; break; case 'D': cdc_disarm_request = 1; break;
case 'G': cdc_recal_request = 1; break; /* gyro recalibration */
case 'R': request_bootloader(); break; /* never returns */ case 'R': request_bootloader(); break; /* never returns */
/* /*

View File

@ -23,6 +23,7 @@
extern volatile uint8_t cdc_streaming; /* set by S command in CDC RX */ extern volatile uint8_t cdc_streaming; /* set by S command in CDC RX */
extern volatile uint8_t cdc_arm_request; /* set by A command */ extern volatile uint8_t cdc_arm_request; /* set by A command */
extern volatile uint8_t cdc_disarm_request; /* set by D command */ extern volatile uint8_t cdc_disarm_request; /* set by D command */
extern volatile uint8_t cdc_recal_request; /* set by G command */
/* /*
* Apply a PID tuning command string from the USB terminal. * Apply a PID tuning command string from the USB terminal.
@ -207,6 +208,14 @@ int main(void) {
balance_disarm(&bal); balance_disarm(&bal);
} }
/* Gyro recalibration — disarm first, then re-sample bias (~1s blocked) */
if (cdc_recal_request) {
cdc_recal_request = 0;
safety_arm_cancel();
balance_disarm(&bal);
if (imu_ret == 0) mpu6000_calibrate();
}
/* Handle PID tuning commands from USB (P/I/D/T/M/?) */ /* Handle PID tuning commands from USB (P/I/D/T/M/?) */
if (cdc_cmd_ready) { if (cdc_cmd_ready) {
cdc_cmd_ready = 0; cdc_cmd_ready = 0;

View File

@ -12,6 +12,7 @@
#include "mpu6000.h" #include "mpu6000.h"
#include "icm42688.h" #include "icm42688.h"
#include "config.h" #include "config.h"
#include "safety.h"
#include "stm32f7xx_hal.h" #include "stm32f7xx_hal.h"
#include <math.h> #include <math.h>
@ -67,6 +68,8 @@ void mpu6000_calibrate(void) {
sum_gy += raw.gy; sum_gy += raw.gy;
sum_gz += raw.gz; sum_gz += raw.gz;
HAL_Delay(1); HAL_Delay(1);
/* Refresh IWDG every 40ms — safe during re-cal with watchdog running */
if (i % 40 == 39) safety_refresh();
} }
s_bias_gx = (float)sum_gx / GYRO_CAL_SAMPLES; s_bias_gx = (float)sum_gx / GYRO_CAL_SAMPLES;

View File

@ -42,7 +42,7 @@ void safety_init(void) {
} }
void safety_refresh(void) { void safety_refresh(void) {
HAL_IWDG_Refresh(&hiwdg); if (hiwdg.Instance) HAL_IWDG_Refresh(&hiwdg);
} }
bool safety_rc_alive(uint32_t now) { bool safety_rc_alive(uint32_t now) {

View File

@ -26,6 +26,8 @@
#arm-btn.armed { background: #ff2222; } #arm-btn.armed { background: #ff2222; }
#dfu-btn { background: #555; display: none; } #dfu-btn { background: #555; display: none; }
#dfu-btn:hover { background: #777; } #dfu-btn:hover { background: #777; }
#gyrocal-btn { background: #1a4a6a; display: none; }
#gyrocal-btn:hover { background: #2a6a9a; }
#status { margin-top: 8px; font-size: 12px; color: #666; } #status { margin-top: 8px; font-size: 12px; color: #666; }
#state-badge { #state-badge {
display: inline-block; padding: 2px 8px; border-radius: 3px; display: inline-block; padding: 2px 8px; border-radius: 3px;
@ -62,6 +64,7 @@
<button class="btn" id="arm-btn" onclick="toggleArm()">ARM</button> <button class="btn" id="arm-btn" onclick="toggleArm()">ARM</button>
<button class="btn" id="dfu-btn" onclick="enterDFU()">DFU</button> <button class="btn" id="dfu-btn" onclick="enterDFU()">DFU</button>
<button class="btn" id="yaw-btn" onclick="resetYaw()" style="background:#335533;display:none">YAW RESET</button> <button class="btn" id="yaw-btn" onclick="resetYaw()" style="background:#335533;display:none">YAW RESET</button>
<button class="btn" id="gyrocal-btn" onclick="gyroRecal()">GYRO CAL</button>
</div> </div>
<div id="status">WebSerial ready</div> <div id="status">WebSerial ready</div>
</div> </div>
@ -315,6 +318,7 @@ window.toggleSerial = async function() {
document.getElementById('arm-btn').style.display = 'none'; document.getElementById('arm-btn').style.display = 'none';
document.getElementById('dfu-btn').style.display = 'none'; document.getElementById('dfu-btn').style.display = 'none';
document.getElementById('yaw-btn').style.display = 'none'; document.getElementById('yaw-btn').style.display = 'none';
document.getElementById('gyrocal-btn').style.display = 'none';
document.getElementById('status').textContent = 'Disconnected'; document.getElementById('status').textContent = 'Disconnected';
return; return;
} }
@ -329,6 +333,7 @@ window.toggleSerial = async function() {
document.getElementById('arm-btn').style.display = 'inline-block'; document.getElementById('arm-btn').style.display = 'inline-block';
document.getElementById('dfu-btn').style.display = 'inline-block'; document.getElementById('dfu-btn').style.display = 'inline-block';
document.getElementById('yaw-btn').style.display = 'inline-block'; document.getElementById('yaw-btn').style.display = 'inline-block';
document.getElementById('gyrocal-btn').style.display = 'inline-block';
document.getElementById('status').textContent = 'Connected — streaming'; document.getElementById('status').textContent = 'Connected — streaming';
readLoop(); readLoop();
@ -353,6 +358,11 @@ window.enterDFU = async function() {
document.getElementById('status').textContent = 'Rebooting to DFU...'; document.getElementById('status').textContent = 'Rebooting to DFU...';
}; };
window.gyroRecal = async function() {
await sendCmd('G');
document.getElementById('status').textContent = 'Calibrating gyro — hold robot still for 1s...';
};
async function readLoop() { async function readLoop() {
const decoder = new TextDecoderStream(); const decoder = new TextDecoderStream();
port.readable.pipeTo(decoder.writable); port.readable.pipeTo(decoder.writable);