15 Commits

Author SHA1 Message Date
Rune Harlyk cbfd7aa354 Adds skill system 2025-12-25 14:03:39 +01:00
Rune Harlyk bc27e5000a 🎨 Update connection url 2025-12-25 14:02:21 +01:00
Rune Harlyk a67d4643b0 🎨 Handle static config files 2025-12-25 13:39:15 +01:00
Rune Harlyk 4e24d87e4b Make read imu and mag be timing based 2025-12-25 13:38:50 +01:00
Rune Harlyk 630bab7678 ♻️ Remove duplicate ping pong handling 2025-12-25 13:37:05 +01:00
Rune Harlyk f54c957be8 🐛 Secure sub and unsub with mutex 2025-12-25 13:36:49 +01:00
Rune Harlyk ed88e47944 🐛 Map rotation bound correct in rad 2025-12-25 13:36:24 +01:00
Rune Harlyk ba36bcc5a5 ♻️ Adds filesystems endpoints back 2025-12-25 13:36:01 +01:00
Rune Harlyk 5e2e29d2a4 ♻️ Change kinematics units to SI 2025-12-24 13:44:45 +01:00
Rune Harlyk 3be08a31ed Adds imu calibration 2025-12-24 13:44:43 +01:00
Rune Harlyk e22ac69e9b 🚨 Fix warnings 2025-12-24 12:34:36 +01:00
Rune Harlyk 0e54f0430f Adds rest of missing api endpoints 2025-12-23 22:28:23 +01:00
Rune Harlyk 0556f86473 🎨 Adds endpoints for wifi and ap 2025-12-20 21:07:32 +01:00
Rune Harlyk 097cc0e33e 🐛 Update base step height in visualizer 2025-12-08 22:30:56 +01:00
Rune Harlyk fe76f2d7dd Adds system metrics endpoints 2025-11-27 21:15:47 +01:00
36 changed files with 863 additions and 411 deletions
+156
View File
@@ -0,0 +1,156 @@
<script lang="ts">
import { skill } from '$lib/stores'
import { onMount, onDestroy } from 'svelte'
let targetX = $state(0.5)
let targetZ = $state(0)
let targetYaw = $state(0)
let speed = $state(0.5)
const status = skill.status
const isActive = skill.isActive
const progress = skill.progress
const presets = [
{ name: 'Forward 0.5m', x: 0.5, z: 0, yaw: 0 },
{ name: 'Forward 1m', x: 1, z: 0, yaw: 0 },
{ name: 'Back 0.5m', x: -0.5, z: 0, yaw: 0 },
{ name: 'Left 0.5m', x: 0, z: 0.5, yaw: 0 },
{ name: 'Right 0.5m', x: 0, z: -0.5, yaw: 0 },
{ name: 'Turn Left 90°', x: 0, z: 0, yaw: 1.57 },
{ name: 'Turn Right 90°', x: 0, z: 0, yaw: -1.57 },
{ name: 'Turn 180°', x: 0, z: 0, yaw: 3.14 }
]
onMount(() => skill.init())
onDestroy(() => skill.destroy())
function executeSkill() {
skill.walk(targetX, targetZ, targetYaw, speed)
}
function runPreset(preset: (typeof presets)[0]) {
skill.walk(preset.x, preset.z, preset.yaw, speed)
}
function formatMeters(val: number): string {
return val.toFixed(3) + 'm'
}
function formatDegrees(rad: number): string {
return ((rad * 180) / Math.PI).toFixed(1) + '°'
}
</script>
<div class="card bg-base-200 shadow-xl">
<div class="card-body p-4">
<h2 class="card-title text-sm flex justify-between">
Skill Control
<span class="badge" class:badge-success={$isActive} class:badge-ghost={!$isActive}>
{$isActive ? 'Active' : 'Idle'}
</span>
</h2>
<div class="grid grid-cols-2 gap-2 text-xs mb-2">
<div class="stat bg-base-300 rounded-lg p-2">
<div class="stat-title text-xs">Position</div>
<div class="stat-value text-sm">
{formatMeters($status.x)}, {formatMeters($status.z)}
</div>
<div class="stat-desc">Yaw: {formatDegrees($status.yaw)}</div>
</div>
<div class="stat bg-base-300 rounded-lg p-2">
<div class="stat-title text-xs">Distance</div>
<div class="stat-value text-sm">{formatMeters($status.distance)}</div>
<div class="stat-desc">Total traveled</div>
</div>
</div>
{#if $isActive}
<div class="mb-2">
<div class="flex justify-between text-xs mb-1">
<span>Progress</span>
<span>{($progress * 100).toFixed(0)}%</span>
</div>
<progress class="progress progress-primary w-full" value={$progress} max="1"></progress>
<div class="text-xs text-base-content/60 mt-1">
Target: ({$status.skill_target_x.toFixed(2)}, {$status.skill_target_z.toFixed(2)}, {formatDegrees(
$status.skill_target_yaw
)})
</div>
</div>
{/if}
<div class="divider my-1 text-xs">Presets</div>
<div class="grid grid-cols-4 gap-1">
{#each presets as preset}
<button class="btn btn-xs btn-outline" onclick={() => runPreset(preset)}>
{preset.name}
</button>
{/each}
</div>
<div class="divider my-1 text-xs">Custom</div>
<div class="grid grid-cols-3 gap-2">
<div class="form-control">
<label class="label py-0" for="skill-x">
<span class="label-text text-xs">X (m)</span>
</label>
<input
id="skill-x"
type="number"
step="0.1"
bind:value={targetX}
class="input input-bordered input-xs w-full"
/>
</div>
<div class="form-control">
<label class="label py-0" for="skill-z">
<span class="label-text text-xs">Z (m)</span>
</label>
<input
id="skill-z"
type="number"
step="0.1"
bind:value={targetZ}
class="input input-bordered input-xs w-full"
/>
</div>
<div class="form-control">
<label class="label py-0" for="skill-yaw">
<span class="label-text text-xs">Yaw (rad)</span>
</label>
<input
id="skill-yaw"
type="number"
step="0.1"
bind:value={targetYaw}
class="input input-bordered input-xs w-full"
/>
</div>
</div>
<div class="form-control mt-2">
<label class="label py-0" for="skill-speed">
<span class="label-text text-xs">Speed: {speed.toFixed(2)}</span>
</label>
<input id="skill-speed" type="range" min="0.1" max="1" step="0.05" bind:value={speed} class="range range-xs range-primary" />
</div>
<div class="card-actions justify-between mt-2">
<div class="flex gap-1">
<button class="btn btn-xs btn-ghost" onclick={() => skill.resetPosition()}>Reset Pos</button>
</div>
<div class="flex gap-1">
<button class="btn btn-xs btn-error" onclick={() => skill.stop()} disabled={!$isActive}>
Stop
</button>
<button class="btn btn-xs btn-primary" onclick={executeSkill} disabled={$isActive}>
Execute
</button>
</div>
</div>
</div>
</div>
+15 -7
View File
@@ -74,13 +74,14 @@
let lastTick = performance.now() let lastTick = performance.now()
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1] const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
const THREEJS_SCALE = 10
let body_state = { let body_state = {
omega: 0, omega: 0,
phi: 0, phi: 0,
psi: 0, psi: 0,
xm: 0, xm: 0,
ym: 0.5, ym: 0.15,
zm: 0, zm: 0,
feet: kinematic.getDefaultFeetPos(), feet: kinematic.getDefaultFeetPos(),
cumulative_x: 0, cumulative_x: 0,
@@ -104,7 +105,7 @@
phi: 0, phi: 0,
psi: 0, psi: 0,
xm: 0, xm: 0,
ym: 0.7, ym: 0.15,
zm: 0, zm: 0,
Background: defaultColor Background: defaultColor
} }
@@ -239,8 +240,16 @@
const rotatedXm = settings.xm * cosYaw - settings.zm * sinYaw const rotatedXm = settings.xm * cosYaw - settings.zm * sinYaw
const rotatedZm = settings.xm * sinYaw + settings.zm * cosYaw const rotatedZm = settings.xm * sinYaw + settings.zm * cosYaw
robot.position.x = smooth(robot.position.x, -rotatedZm - body_state.cumulative_z * 1.2, 0.1) robot.position.x = smooth(
robot.position.z = smooth(robot.position.z, -rotatedXm - body_state.cumulative_x * 1.2, 0.1) robot.position.x,
(-rotatedZm - body_state.cumulative_z) * THREEJS_SCALE,
0.1
)
robot.position.z = smooth(
robot.position.z,
(-rotatedXm - body_state.cumulative_x) * THREEJS_SCALE,
0.1
)
const pitch = degToRad(settings.psi - 90) + body_state.cumulative_pitch const pitch = degToRad(settings.psi - 90) + body_state.cumulative_pitch
const roll = degToRad(settings.omega) + body_state.cumulative_roll const roll = degToRad(settings.omega) + body_state.cumulative_roll
@@ -275,7 +284,6 @@
s: controlData[5], s: controlData[5],
s1: controlData[6] s1: controlData[6]
} }
body_state.ym = data.h
let planner = planners[get(mode)] let planner = planners[get(mode)]
const delta = performance.now() - lastTick const delta = performance.now() - lastTick
@@ -296,8 +304,8 @@
settings.omega = radToDeg(robot.rotation.y) settings.omega = radToDeg(robot.rotation.y)
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90 settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
settings.psi = radToDeg(robot.rotation.x) + 90 settings.psi = radToDeg(robot.rotation.x) + 90
settings.xm = robot.position.z * 100 settings.xm = robot.position.z / THREEJS_SCALE
settings.zm = -robot.position.x * 100 settings.zm = -robot.position.x / THREEJS_SCALE
} }
const updateTargetPosition = () => { const updateTargetPosition = () => {
+35 -20
View File
@@ -26,21 +26,34 @@ export abstract class GaitState {
protected dt = 0.02 protected dt = 0.02
protected body_state!: body_state_t protected body_state!: body_state_t
protected get kinematic() {
return get(currentKinematic)
}
protected gait_state: gait_state_t = { protected gait_state: gait_state_t = {
step_height: 0.4, step_height: 0,
step_x: 0, step_x: 0,
step_z: 0, step_z: 0,
step_angle: 0, step_angle: 0,
step_velocity: 1, step_velocity: 1,
step_depth: 0.002 step_depth: 0
} }
public get default_feet_pos() { public get default_feet_pos() {
return get(currentKinematic).getDefaultFeetPos() return this.kinematic.getDefaultFeetPos()
} }
protected get default_height() { protected get default_height() {
return 0.5 return this.kinematic.default_body_height
}
protected get default_step_depth() {
return this.kinematic.default_step_depth
}
protected get default_step_height() {
return this.kinematic.default_step_height
} }
begin() { begin() {
@@ -67,16 +80,15 @@ export abstract class GaitState {
} }
map_command(command: ControllerCommand) { map_command(command: ControllerCommand) {
const newCommand = { const kin = this.kinematic
step_height: 0.4 + (command.s1 + 1) / 2, this.gait_state = {
step_x: command.ly, step_height: command.s1 * kin.max_step_height,
step_z: -command.lx, step_x: command.ly * kin.max_step_length,
step_z: -command.lx * kin.max_step_length,
step_velocity: command.s, step_velocity: command.s,
step_angle: command.rx, step_angle: command.rx,
step_depth: 0.002 step_depth: kin.default_step_depth
} }
this.gait_state = newCommand
} }
} }
@@ -92,14 +104,13 @@ export class IdleState extends GaitState {
export class CalibrationState extends GaitState { export class CalibrationState extends GaitState {
protected name = 'Calibration' protected name = 'Calibration'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) { step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command) super.step(body_state, _command)
body_state.omega = 0 body_state.omega = 0
body_state.phi = 0 body_state.phi = 0
body_state.psi = 0 body_state.psi = 0
body_state.xm = 0 body_state.xm = 0
body_state.ym = this.default_height * 10 body_state.ym = this.kinematic.max_body_height
body_state.zm = 0 body_state.zm = 0
body_state.feet = this.default_feet_pos body_state.feet = this.default_feet_pos
return body_state return body_state
@@ -109,14 +120,13 @@ export class CalibrationState extends GaitState {
export class RestState extends GaitState { export class RestState extends GaitState {
protected name = 'Rest' protected name = 'Rest'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) { step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command) super.step(body_state, _command)
body_state.omega = 0 body_state.omega = 0
body_state.phi = 0 body_state.phi = 0
body_state.psi = 0 body_state.psi = 0
body_state.xm = 0 body_state.xm = 0
body_state.ym = this.default_height / 2 body_state.ym = this.kinematic.min_body_height
body_state.zm = 0 body_state.zm = 0
body_state.feet = this.default_feet_pos body_state.feet = this.default_feet_pos
return body_state return body_state
@@ -128,11 +138,13 @@ export class StandState extends GaitState {
step(body_state: body_state_t, command: ControllerCommand) { step(body_state: body_state_t, command: ControllerCommand) {
super.step(body_state, command) super.step(body_state, command)
const kin = this.kinematic
body_state.omega = 0 body_state.omega = 0
body_state.phi = command.rx * 10 * (Math.PI / 2) body_state.ym = kin.min_body_height + command.h * kin.body_height_range
body_state.psi = command.ry * 10 * (Math.PI / 2) body_state.psi = command.ry * kin.max_pitch
body_state.xm = command.ly / 4 body_state.phi = command.rx * kin.max_roll
body_state.zm = command.lx / 4 body_state.xm = command.ly * kin.max_body_shift_x
body_state.zm = command.lx * kin.max_body_shift_z
body_state.feet = this.default_feet_pos body_state.feet = this.default_feet_pos
return body_state return body_state
} }
@@ -191,6 +203,8 @@ export class BezierState extends GaitState {
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
super.step(body_state, command, dt) super.step(body_state, command, dt)
const kin = this.kinematic
this.body_state.ym = kin.min_body_height + command.h * kin.body_height_range
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2) this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2)
if (this.gait_state.step_x < 0) this.step_length = -this.step_length if (this.gait_state.step_x < 0) this.step_length = -this.step_length
this.update_phase() this.update_phase()
@@ -339,7 +353,8 @@ export class BezierState extends GaitState {
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2 let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2
const delta_pos = controller(length, angle, ...args, phase) const delta_pos = controller(length, angle, ...args, phase)
length = this.gait_state.step_angle * 2 const kin = this.kinematic
length = this.gait_state.step_angle * kin.max_step_length
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index]) angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index])
const delta_rot = controller(length, angle, ...args, phase) const delta_rot = controller(length, angle, ...args, phase)
+34 -3
View File
@@ -50,7 +50,22 @@ export default class Kinematic {
DEG2RAD = DEG2RAD DEG2RAD = DEG2RAD
max_roll: number
max_pitch: number
max_body_shift_x: number
max_body_shift_z: number
max_leg_reach: number
min_body_height: number
max_body_height: number
body_height_range: number
max_step_length: number
max_step_height: number
default_step_depth: number
default_body_height: number
default_step_height: number
mountOffsets: number[][] mountOffsets: number[][]
default_feet_positions: number[][]
invMountRot = [ invMountRot = [
[0, 0, -1], [0, 0, -1],
@@ -66,18 +81,34 @@ export default class Kinematic {
this.L = params.L this.L = params.L
this.W = params.W this.W = params.W
this.max_roll = 15 * (Math.PI / 2)
this.max_pitch = 15 * (Math.PI / 2)
this.max_body_shift_x = this.W / 3
this.max_body_shift_z = this.W / 3
this.max_leg_reach = this.femur + this.tibia - this.coxa_offset
this.min_body_height = this.max_leg_reach * 0.45
this.max_body_height = this.max_leg_reach * 1
this.body_height_range = this.max_body_height - this.min_body_height
this.max_step_length = this.max_leg_reach * 0.8
this.max_step_height = this.max_leg_reach / 2
this.default_step_depth = 0.002
this.default_body_height = this.min_body_height + this.body_height_range / 2
this.default_step_height = this.default_body_height / 2
this.mountOffsets = [ this.mountOffsets = [
[this.L / 2, 0, this.W / 2], [this.L / 2, 0, this.W / 2],
[this.L / 2, 0, -this.W / 2], [this.L / 2, 0, -this.W / 2],
[-this.L / 2, 0, this.W / 2], [-this.L / 2, 0, this.W / 2],
[-this.L / 2, 0, -this.W / 2] [-this.L / 2, 0, -this.W / 2]
] ]
this.default_feet_positions = this.mountOffsets.map((offset, i) => {
return [offset[0], 0, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
})
} }
getDefaultFeetPos(): number[][] { getDefaultFeetPos(): number[][] {
return this.mountOffsets.map((offset, i) => { return this.default_feet_positions.map(pos => [...pos])
return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
})
} }
calcIK(p: body_state_t): number[] { calcIK(p: body_state_t): number[] {
+1 -1
View File
@@ -20,7 +20,7 @@ import {
Group, Group,
MeshBasicMaterial, MeshBasicMaterial,
RepeatWrapping, RepeatWrapping,
Object3D type Object3D
} from 'three' } from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls' import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
+13 -1
View File
@@ -4,6 +4,7 @@ import { get, type Writable } from 'svelte/store'
import Visualization from '$lib/components/Visualization.svelte' import Visualization from '$lib/components/Visualization.svelte'
import Stream from '$lib/components/Stream.svelte' import Stream from '$lib/components/Stream.svelte'
import ChartWidget from '$lib/components/widget/ChartWidget.svelte' import ChartWidget from '$lib/components/widget/ChartWidget.svelte'
import SkillPanel from '$lib/components/SkillPanel.svelte'
export interface WidgetConfig { export interface WidgetConfig {
id: string | number id: string | number
@@ -25,7 +26,8 @@ export const isWidgetConfig = (
export const WidgetComponents = { export const WidgetComponents = {
Visualization, Visualization,
Stream, Stream,
ChartWidget ChartWidget,
SkillPanel
} }
interface View { interface View {
@@ -59,6 +61,16 @@ const defaultViews: View[] = [
{ id: 2, component: 'Visualization', props: { debug: true } } { id: 2, component: 'Visualization', props: { debug: true } }
] ]
} }
},
{
name: 'Skills',
content: {
id: 'root',
widgets: [
{ id: 1, component: 'Visualization', props: { debug: true } },
{ id: 2, component: 'SkillPanel' }
]
}
} }
] ]
+12 -12
View File
@@ -29,24 +29,24 @@ export const variants = {
model: `${base}spot_micro.urdf.xacro`, model: `${base}spot_micro.urdf.xacro`,
stl: `${base}stl.zip`, stl: `${base}stl.zip`,
kinematics: { kinematics: {
coxa: 60.5 / 100, coxa: 0.0605,
coxa_offset: 10 / 100, coxa_offset: 0.01,
femur: 111.7 / 100, femur: 0.1112,
tibia: 118.5 / 100, tibia: 0.1185,
L: 207.5 / 100, L: 0.2075,
W: 78 / 100 W: 0.078
} }
}, },
SPOTMICRO_YERTLE: { SPOTMICRO_YERTLE: {
model: `${base}yertle.URDF`, model: `${base}yertle.URDF`,
stl: `${base}URDF.zip`, stl: `${base}URDF.zip`,
kinematics: { kinematics: {
coxa: 35 / 100, coxa: 0.035,
coxa_offset: 0 / 100, coxa_offset: 0.0,
femur: 130 / 100, femur: 0.13,
tibia: 130 / 100, tibia: 0.13,
L: 240 / 100, L: 0.24,
W: 78 / 100 W: 0.078
} }
} }
} }
+1
View File
@@ -7,3 +7,4 @@ export * from './telemetry'
export * from './analytics' export * from './analytics'
export * from './featureFlags' export * from './featureFlags'
export * from './location-store' export * from './location-store'
export * from './skill'
+1 -1
View File
@@ -50,5 +50,5 @@ export const input: Writable<ControllerInput> = writable({
right: { x: 0, y: 0 }, right: { x: 0, y: 0 },
height: 0.5, height: 0.5,
speed: 0.5, speed: 0.5,
s1: 0.05 s1: 0.5
}) })
+85
View File
@@ -0,0 +1,85 @@
import { writable, derived } from 'svelte/store'
import { socket } from './socket'
import { MessageTopic, type SkillStatus, type SkillCommand } from '$lib/types/models'
const defaultStatus: SkillStatus = {
x: 0,
y: 0,
z: 0,
yaw: 0,
distance: 0,
skill_active: false,
skill_target_x: 0,
skill_target_z: 0,
skill_target_yaw: 0,
skill_traveled_x: 0,
skill_traveled_z: 0,
skill_rotated: 0,
skill_progress: 0,
skill_complete: false
}
function createSkillStore() {
const status = writable<SkillStatus>(defaultStatus)
const history = writable<SkillCommand[]>([])
let unsubscribe: (() => void) | null = null
function init() {
if (unsubscribe) return
unsubscribe = socket.on<SkillStatus>(MessageTopic.skillStatus, data => {
status.set(data)
if (data.event === 'complete') {
history.update(h => [...h.slice(-9), getCurrentTarget(data)])
}
})
}
function getCurrentTarget(s: SkillStatus): SkillCommand {
return { x: s.skill_target_x, z: s.skill_target_z, yaw: s.skill_target_yaw }
}
function execute(cmd: SkillCommand) {
socket.sendEvent(MessageTopic.skill, cmd)
}
function walk(x: number, z: number = 0, yaw: number = 0, speed: number = 0.5) {
execute({ x, z, yaw, speed })
}
function turn(yaw: number, speed: number = 0.5) {
execute({ x: 0, z: 0, yaw, speed })
}
function stop() {
socket.sendEvent(MessageTopic.displacement, { action: 'clear' })
}
function resetPosition() {
socket.sendEvent(MessageTopic.displacement, { action: 'reset' })
}
function destroy() {
if (unsubscribe) {
unsubscribe()
unsubscribe = null
}
}
return {
status,
history,
init,
destroy,
execute,
walk,
turn,
stop,
resetPosition,
isActive: derived(status, $s => $s.skill_active),
progress: derived(status, $s => $s.skill_progress),
position: derived(status, $s => ({ x: $s.x, y: $s.y, z: $s.z, yaw: $s.yaw }))
}
}
export const skill = createSkillStore()
+34 -1
View File
@@ -1,5 +1,6 @@
export enum MessageTopic { export enum MessageTopic {
imu = 'imu', imu = 'imu',
imuCalibrate = 'imuCalibrate',
mode = 'mode', mode = 'mode',
input = 'input', input = 'input',
analytics = 'analytics', analytics = 'analytics',
@@ -13,7 +14,10 @@ export enum MessageTopic {
servoPWM = 'servoPWM', servoPWM = 'servoPWM',
WiFiSettings = 'WiFiSettings', WiFiSettings = 'WiFiSettings',
sonar = 'sonar', sonar = 'sonar',
rssi = 'rssi' rssi = 'rssi',
skill = 'skill',
skillStatus = 'skill_status',
displacement = 'displacement'
} }
export type vector = { x: number; y: number } export type vector = { x: number; y: number }
@@ -160,6 +164,10 @@ export type IMUMsg = {
bmp: [number, number, number, boolean] bmp: [number, number, number, boolean]
} }
export type IMUCalibrationResult = {
success: boolean
}
export interface I2CDevice { export interface I2CDevice {
address: number address: number
part_number: string part_number: string
@@ -243,3 +251,28 @@ export interface MDNSStatus {
services: MDNSService[] services: MDNSService[]
global_txt_records: MDNSTxtRecord[] global_txt_records: MDNSTxtRecord[]
} }
export interface SkillCommand {
x: number
z: number
yaw: number
speed?: number
}
export interface SkillStatus {
x: number
y: number
z: number
yaw: number
distance: number
skill_active: boolean
skill_target_x: number
skill_target_z: number
skill_target_yaw: number
skill_traveled_x: number
skill_traveled_z: number
skill_rotated: number
skill_progress: number
skill_complete: boolean
event?: string
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { Color, LoaderUtils, Vector3 } from 'three' import { Color, Vector3 } from 'three'
import URDFLoader, { type URDFRobot } from 'urdf-loader' import URDFLoader, { type URDFRobot } from 'urdf-loader'
import { XacroLoader } from 'xacro-parser' import { XacroLoader } from 'xacro-parser'
import { Result } from '$lib/utilities' import { Result } from '$lib/utilities'
+1 -1
View File
@@ -5,7 +5,7 @@
const update = () => { const update = () => {
const ws = $apiLocation ? $apiLocation : window.location.host const ws = $apiLocation ? $apiLocation : window.location.host
socket.init(`ws://${ws}/api/ws/events`) socket.init(`ws://${ws}/api/ws`)
} }
</script> </script>
@@ -16,11 +16,6 @@
part_number: 'MPU6050', part_number: 'MPU6050',
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices' name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
}, },
{
address: 105,
part_number: 'ICM20948',
name: 'Nine-Axis (Gyro + Accelerometer + Magnetometer) MEMS MotionTracking™ Device'
},
{ address: 115, part_number: 'PAJ7620U2', name: 'Gesture sensor' }, { address: 115, part_number: 'PAJ7620U2', name: 'Gesture sensor' },
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' } { address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
] ]
+35 -49
View File
@@ -6,7 +6,7 @@
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition'
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte'
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
import { MessageTopic, type IMUMsg } from '$lib/types/models' import { MessageTopic, type IMUMsg, type IMUCalibrationResult } from '$lib/types/models'
import { useFeatureFlags } from '$lib/stores/featureFlags' import { useFeatureFlags } from '$lib/stores/featureFlags'
import { Rotate3d } from '$lib/components/icons' import { Rotate3d } from '$lib/components/icons'
@@ -14,16 +14,16 @@
const features = useFeatureFlags() const features = useFeatureFlags()
let intervalId: ReturnType<typeof setInterval> | number let intervalId: ReturnType<typeof setInterval> | number
let isCalibrating = $state(false)
let calibrationResult = $state<IMUCalibrationResult | null>(null)
let angleChartElement: HTMLCanvasElement let angleChartElement: HTMLCanvasElement
let tempChartElement: HTMLCanvasElement let tempChartElement: HTMLCanvasElement
let altitudeChartElement: HTMLCanvasElement let altitudeChartElement: HTMLCanvasElement
let magnetometerChartElement: HTMLCanvasElement
let angleChart: Chart let angleChart: Chart
let tempChart: Chart let tempChart: Chart
let altitudeChart: Chart let altitudeChart: Chart
let magnetometerChart: Chart
const getChartColors = () => { const getChartColors = () => {
const style = getComputedStyle(document.body) const style = getComputedStyle(document.body)
@@ -173,37 +173,6 @@
} }
} }
}) })
magnetometerChart = new Chart(magnetometerChartElement, {
type: 'line',
data: {
datasets: [
{
label: 'Heading',
borderColor: colors.primary,
backgroundColor: colors.primary,
borderWidth: 2,
data: $imu.heading,
yAxisID: 'y'
}
]
},
options: {
...baseConfig,
scales: {
...baseConfig.scales,
y: {
...baseConfig.scales.y,
title: {
display: true,
text: 'Heading [°]',
color: colors.background,
font: { size: 16, weight: 'bold' }
}
}
}
}
})
} }
const updateChartData = (chart: Chart, data: number[]) => { const updateChartData = (chart: Chart, data: number[]) => {
@@ -227,10 +196,6 @@
angleChart.update('none') angleChart.update('none')
} }
if ($features.mag) {
updateChartData(magnetometerChart, $imu.heading)
}
if ($features.bmp) { if ($features.bmp) {
updateChartData(tempChart, $imu.bmp_temp) updateChartData(tempChart, $imu.bmp_temp)
updateChartData(altitudeChart, $imu.altitude) updateChartData(altitudeChart, $imu.altitude)
@@ -243,14 +208,26 @@
imu.addData(data) imu.addData(data)
}) })
socket.on(MessageTopic.imuCalibrate, (data: IMUCalibrationResult) => {
isCalibrating = false
calibrationResult = data
})
initializeCharts() initializeCharts()
intervalId = setInterval(updateData, 200) intervalId = setInterval(updateData, 200)
}) })
onDestroy(() => { onDestroy(() => {
socket.off(MessageTopic.imu) socket.off(MessageTopic.imu)
socket.off(MessageTopic.imuCalibrate)
clearInterval(intervalId) clearInterval(intervalId)
}) })
function startCalibration() {
isCalibrating = true
calibrationResult = null
socket.sendEvent(MessageTopic.imuCalibrate, {})
}
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -261,6 +238,26 @@
<span>IMU</span> <span>IMU</span>
{/snippet} {/snippet}
<div class="flex items-center gap-2 mb-4">
<button
class="btn btn-sm btn-primary"
onclick={startCalibration}
disabled={isCalibrating || !$features.imu}
>
{#if isCalibrating}
<span class="loading loading-spinner loading-xs"></span>
Calibrating...
{:else}
Calibrate IMU
{/if}
</button>
{#if calibrationResult}
<span class="badge" class:badge-success={calibrationResult.success} class:badge-error={!calibrationResult.success}>
{calibrationResult.success ? 'Calibrated' : 'Failed'}
</span>
{/if}
</div>
{#if $features.imu} {#if $features.imu}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
@@ -272,17 +269,6 @@
</div> </div>
{/if} {/if}
{#if $features.mag}
<div class="w-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={magnetometerChartElement}></canvas>
</div>
</div>
{/if}
{#if $features.bmp} {#if $features.bmp}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
-2
View File
@@ -13,8 +13,6 @@ build_flags =
-D USE_HMC5883=0 -D USE_HMC5883=0
-D USE_BMP180=0 -D USE_BMP180=0
-D USE_MPU6050=0 -D USE_MPU6050=0
-D USE_ICM20948=1
-D USE_ICM20948_SPIMODE=1
-D USE_WS2812=1 -D USE_WS2812=1
-D USE_BNO055=0 -D USE_BNO055=0
-D USE_USS=0 -D USE_USS=0
+15 -21
View File
@@ -55,8 +55,16 @@ class CommAdapterBase {
void send(const char *data, int cid = -1) { send(reinterpret_cast<const uint8_t *>(data), strlen(data), cid); } void send(const char *data, int cid = -1) { send(reinterpret_cast<const uint8_t *>(data), strlen(data), cid); }
virtual void send(const uint8_t *data, size_t len, int cid = -1) = 0; virtual void send(const uint8_t *data, size_t len, int cid = -1) = 0;
void subscribe(const char *event, int cid = 0) { client_subscriptions[event].push_back(cid); } void subscribe(const char *event, int cid = 0) {
void unsubscribe(const char *event, int cid = 0) { client_subscriptions[event].push_back(cid); } xSemaphoreTake(mutex_, portMAX_DELAY);
client_subscriptions[event].push_back(cid);
xSemaphoreGive(mutex_);
}
void unsubscribe(const char *event, int cid = 0) {
xSemaphoreTake(mutex_, portMAX_DELAY);
client_subscriptions[event].remove(cid);
xSemaphoreGive(mutex_);
}
void handleEventCallbacks(std::string event, JsonVariant &jsonObject, int originId) { void handleEventCallbacks(std::string event, JsonVariant &jsonObject, int originId) {
for (auto &callback : event_callbacks[event]) { for (auto &callback : event_callbacks[event]) {
@@ -102,35 +110,21 @@ class CommAdapterBase {
} }
case message_type_t::PING: { case message_type_t::PING: {
ESP_LOGI("Comm Base", "PING (cid=%d)", cid); ESP_LOGI("Comm Base", "PING (cid=%d)", cid);
#if USE_MSGPACK ping(cid);
static const uint8_t pong[] = {0x91, 0x04};
send(pong, sizeof(pong), cid);
#else
send("[4]", cid);
#endif
break; break;
} }
case message_type_t::PONG: ESP_LOGI("Comm Base", "PONG (cid=%d)", cid); break; case message_type_t::PONG: ESP_LOGI("Comm Base", "PONG (cid=%d)", cid); break;
default: ESP_LOGW("Comm Base", "Unknown message type: %d", static_cast<int>(type)); break; default: ESP_LOGW("Comm Base", "Unknown message type: %d", static_cast<int>(type)); break;
} }
if (type == PONG) {
ESP_LOGV("EventSocket", "Pong");
return;
} else if (type == PING) {
ESP_LOGV("EventSocket", "Ping");
ping(cid);
return;
}
} }
void ping(int cid) { void ping(int cid) {
#if USE_MSGPACK #if USE_MSGPACK
const uint8_t out[] = {0x91, 0x04}; static const uint8_t pong[] = {0x91, 0x04};
send(out, sizeof(out), cid); send(pong, sizeof(pong), cid);
#else #else
const char *out = "[4]"; send("[4]", cid);
send(out, strlen(out), cid);
#endif #endif
} }
+6 -9
View File
@@ -12,7 +12,7 @@
#define USE_CAMERA 0 #define USE_CAMERA 0
#endif #endif
// ESP32 IMU off by default // ESP32 IMU on by default
#ifndef USE_MPU6050 #ifndef USE_MPU6050
#define USE_MPU6050 0 #define USE_MPU6050 0
#endif #endif
@@ -22,14 +22,6 @@
#define USE_BNO055 1 #define USE_BNO055 1
#endif #endif
// ESP32 IMU off by default
#ifndef USE_ICM20948
#define USE_ICM20948 0
#endif
#ifndef USE_ICM20948_SPIMODE // I2C on by default
#define USE_ICM20948_SPIMODE 0
#endif
// ESP32 magnetometer on by default // ESP32 magnetometer on by default
#ifndef USE_HMC5883 #ifndef USE_HMC5883
#define USE_HMC5883 0 #define USE_HMC5883 0
@@ -50,6 +42,11 @@
#define USE_PCA9685 1 #define USE_PCA9685 1
#endif #endif
// WS2812 LED strip off by default
#ifndef USE_WS2812
#define USE_WS2812 0
#endif
// ESP32 MDNS on by default // ESP32 MDNS on by default
#ifndef USE_MDNS #ifndef USE_MDNS
#define USE_MDNS 1 #define USE_MDNS 1
+1
View File
@@ -25,6 +25,7 @@ esp_err_t uploadFile(PsychicRequest *request, const std::string &filename, uint6
bool last); bool last);
esp_err_t getFiles(PsychicRequest *request); esp_err_t getFiles(PsychicRequest *request);
esp_err_t getConfigFile(PsychicRequest *request);
esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json); esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json);
esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json); esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json);
+93 -21
View File
@@ -6,26 +6,26 @@
class KinConfig { class KinConfig {
public: public:
#if defined(SPOTMICRO_ESP32) #if defined(SPOTMICRO_ESP32)
static constexpr float coxa = 60.5f / 100.0f; static constexpr float coxa = 0.0605f;
static constexpr float coxa_offset = 10.0f / 100.0f; static constexpr float coxa_offset = 0.010f;
static constexpr float femur = 111.2f / 100.0f; static constexpr float femur = 0.1112f;
static constexpr float tibia = 118.5f / 100.0f; static constexpr float tibia = 0.1185f;
static constexpr float L = 207.5f / 100.0f; static constexpr float L = 0.2075f;
static constexpr float W = 78.0f / 100.0f; static constexpr float W = 0.078f;
#elif defined(SPOTMICRO_ESP32_MINI) #elif defined(SPOTMICRO_ESP32_MINI)
static constexpr float coxa = 35.0f / 100.0f; static constexpr float coxa = 0.035f;
static constexpr float coxa_offset = 0.0f / 100.0f;
static constexpr float femur = 60.0f / 100.0f;
static constexpr float tibia = 60.0f / 100.0f;
static constexpr float L = 160.0f / 100.0f;
static constexpr float W = 80.0f / 100.0f;
#elif defined(SPOTMICRO_YERTLE)
static constexpr float coxa = 35.0f / 100.0f;
static constexpr float coxa_offset = 0.0f; static constexpr float coxa_offset = 0.0f;
static constexpr float femur = 130.0f / 100.0f; static constexpr float femur = 0.060f;
static constexpr float tibia = 130.0f / 100.0f; static constexpr float tibia = 0.060f;
static constexpr float L = 240.0f / 100.0f; static constexpr float L = 0.160f;
static constexpr float W = 78.0f / 100.0f; static constexpr float W = 0.080f;
#elif defined(SPOTMICRO_YERTLE)
static constexpr float coxa = 0.035f;
static constexpr float coxa_offset = 0.0f;
static constexpr float femur = 0.130f;
static constexpr float tibia = 0.130f;
static constexpr float L = 0.240f;
static constexpr float W = 0.078f;
#endif #endif
static constexpr float mountOffsets[4][3] = { static constexpr float mountOffsets[4][3] = {
@@ -38,9 +38,8 @@ class KinConfig {
{mountOffsets[3][0], 0, mountOffsets[3][2] - coxa, 1}, {mountOffsets[3][0], 0, mountOffsets[3][2] - coxa, 1},
}; };
// Max constants static constexpr float max_roll = 15 * DEG2RAD_F;
static constexpr float max_roll = 15 * (float)M_PI_2; static constexpr float max_pitch = 15 * DEG2RAD_F;
static constexpr float max_pitch = 15 * (float)M_PI_2;
static constexpr float max_body_shift_x = W / 3; static constexpr float max_body_shift_x = W / 3;
static constexpr float max_body_shift_z = W / 3; static constexpr float max_body_shift_z = W / 3;
@@ -60,12 +59,85 @@ class KinConfig {
static constexpr float default_step_height = default_body_height / 2; static constexpr float default_step_height = default_body_height / 2;
}; };
struct displacement_state_t {
float x {0};
float y {0};
float z {0};
float roll {0};
float pitch {0};
float yaw {0};
void reset() { x = y = z = roll = pitch = yaw = 0; }
float distance() const { return std::sqrt(x * x + z * z); }
};
struct skill_target_t {
float target_x {0};
float target_z {0};
float target_yaw {0};
float traveled_x {0};
float traveled_z {0};
float rotated {0};
bool active {false};
void set(float x, float z, float yaw) {
target_x = x;
target_z = z;
target_yaw = yaw;
traveled_x = traveled_z = rotated = 0;
active = true;
}
void reset() {
target_x = target_z = target_yaw = 0;
traveled_x = traveled_z = rotated = 0;
active = false;
}
void accumulate(float dx, float dz, float dyaw) {
traveled_x += dx;
traveled_z += dz;
rotated += dyaw;
}
bool isComplete() const {
if (!active) return false;
bool x_ok = (target_x == 0) || (target_x > 0 ? traveled_x >= target_x : traveled_x <= target_x);
bool z_ok = (target_z == 0) || (target_z > 0 ? traveled_z >= target_z : traveled_z <= target_z);
bool yaw_ok = (target_yaw == 0) || (target_yaw > 0 ? rotated >= target_yaw : rotated <= target_yaw);
return x_ok && z_ok && yaw_ok;
}
float progress() const {
if (!active) return 0;
float total_target = std::fabs(target_x) + std::fabs(target_z) + std::fabs(target_yaw);
if (total_target == 0) return 1;
auto clampProgress = [](float traveled, float target) -> float {
if (target == 0) return 0;
float p = traveled / target;
return std::clamp(p, 0.0f, 1.0f) * std::fabs(target);
};
float total_progress = clampProgress(traveled_x, target_x) + clampProgress(traveled_z, target_z) +
clampProgress(rotated, target_yaw);
return total_progress / total_target;
}
};
struct alignas(16) body_state_t { struct alignas(16) body_state_t {
float omega {0}, phi {0}, psi {0}, xm {0}, ym {KinConfig::default_body_height}, zm {0}; float omega {0}, phi {0}, psi {0}, xm {0}, ym {KinConfig::default_body_height}, zm {0};
float feet[4][4]; float feet[4][4];
displacement_state_t cumulative;
skill_target_t skill;
void updateFeet(const float newFeet[4][4]) { COPY_2D_ARRAY_4x4(feet, newFeet); } void updateFeet(const float newFeet[4][4]) { COPY_2D_ARRAY_4x4(feet, newFeet); }
void resetDisplacement() { cumulative.reset(); }
bool operator==(const body_state_t &other) const { bool operator==(const body_state_t &other) const {
if (!IS_ALMOST_EQUAL(omega, other.omega) || !IS_ALMOST_EQUAL(phi, other.phi) || if (!IS_ALMOST_EQUAL(omega, other.omega) || !IS_ALMOST_EQUAL(phi, other.phi) ||
!IS_ALMOST_EQUAL(psi, other.psi) || !IS_ALMOST_EQUAL(xm, other.xm) || !IS_ALMOST_EQUAL(ym, other.ym) || !IS_ALMOST_EQUAL(psi, other.psi) || !IS_ALMOST_EQUAL(xm, other.xm) || !IS_ALMOST_EQUAL(ym, other.ym) ||
+45
View File
@@ -3,6 +3,7 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "esp_timer.h" #include "esp_timer.h"
#include <functional>
#include <kinematics.h> #include <kinematics.h>
#include <peripherals/gesture.h> #include <peripherals/gesture.h>
@@ -18,6 +19,8 @@
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK }; enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK };
using SkillCompleteCallback = std::function<void()>;
class MotionService { class MotionService {
public: public:
void begin(); void begin();
@@ -30,6 +33,10 @@ class MotionService {
void handleMode(JsonVariant &root, int originId); void handleMode(JsonVariant &root, int originId);
void handleDisplacement(JsonVariant &root, int originId);
void handleSkill(JsonVariant &root, int originId);
void setState(MotionState *newState); void setState(MotionState *newState);
void handleGestures(const gesture_t ges); void handleGestures(const gesture_t ges);
@@ -42,6 +49,39 @@ class MotionService {
inline bool isActive() { return state != nullptr; } inline bool isActive() { return state != nullptr; }
void resetDisplacement() { body_state.resetDisplacement(); }
void setSkillTarget(float x, float z, float yaw) { body_state.skill.set(x, z, yaw); }
void clearSkill() { body_state.skill.reset(); }
bool isSkillActive() const { return body_state.skill.active; }
bool isSkillComplete() const { return body_state.skill.isComplete(); }
const displacement_state_t &getDisplacement() const { return body_state.cumulative; }
const skill_target_t &getSkill() const { return body_state.skill; }
void getDisplacementResult(JsonVariant &root) const {
root["x"] = body_state.cumulative.x;
root["y"] = body_state.cumulative.y;
root["z"] = body_state.cumulative.z;
root["yaw"] = body_state.cumulative.yaw;
root["distance"] = body_state.cumulative.distance();
root["skill_active"] = body_state.skill.active;
root["skill_target_x"] = body_state.skill.target_x;
root["skill_target_z"] = body_state.skill.target_z;
root["skill_target_yaw"] = body_state.skill.target_yaw;
root["skill_traveled_x"] = body_state.skill.traveled_x;
root["skill_traveled_z"] = body_state.skill.traveled_z;
root["skill_rotated"] = body_state.skill.rotated;
root["skill_progress"] = body_state.skill.progress();
root["skill_complete"] = body_state.skill.isComplete();
}
void setSkillCompleteCallback(SkillCompleteCallback callback) { skillCompleteCallback = callback; }
private: private:
Kinematics kinematics; Kinematics kinematics;
@@ -63,6 +103,11 @@ class MotionService {
float dir[12] = {1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1}; float dir[12] = {1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1};
int64_t lastUpdate = esp_timer_get_time(); int64_t lastUpdate = esp_timer_get_time();
SkillCompleteCallback skillCompleteCallback = nullptr;
bool skillWasComplete = false;
void checkSkillComplete();
}; };
#endif #endif
+22
View File
@@ -98,11 +98,33 @@ class WalkState : public MotionState {
step_length = std::hypot(gait_state.step_x, gait_state.step_z); step_length = std::hypot(gait_state.step_x, gait_state.step_z);
if (gait_state.step_x < 0.0f) step_length = -step_length; if (gait_state.step_x < 0.0f) step_length = -step_length;
const bool moving = !isZero(gait_state.step_x) || !isZero(gait_state.step_z) || !isZero(gait_state.step_angle);
updateDisplacement(body_state, dt, moving);
updatePhase(dt); updatePhase(dt);
updateBodyPosition(body_state, dt); updateBodyPosition(body_state, dt);
updateFeetPositions(body_state); updateFeetPositions(body_state);
} }
void updateDisplacement(body_state_t &body_state, float dt, bool moving) {
if (!moving) return;
float dx_local = gait_state.step_x * gait_state.step_velocity * dt * speed_factor;
float dz_local = gait_state.step_z * gait_state.step_velocity * dt * speed_factor;
float dyaw = gait_state.step_angle * gait_state.step_velocity * dt * speed_factor;
if (body_state.skill.active) {
body_state.skill.accumulate(dx_local, dz_local, dyaw);
}
float cos_yaw = std::cos(body_state.cumulative.yaw);
float sin_yaw = std::sin(body_state.cumulative.yaw);
body_state.cumulative.x += dx_local * cos_yaw - dz_local * sin_yaw;
body_state.cumulative.z += dx_local * sin_yaw + dz_local * cos_yaw;
body_state.cumulative.yaw += dyaw;
}
protected: protected:
void handleCommand(const CommandMsg &cmd) override { void handleCommand(const CommandMsg &cmd) override {
target_body_state.ym = KinConfig::min_body_height + cmd.h * KinConfig::body_height_range; target_body_state.ym = KinConfig::min_body_height + cmd.h * KinConfig::body_height_range;
+1 -1
View File
@@ -38,7 +38,7 @@ struct BarometerMsg : public SensorMessageBase {
class Barometer : public SensorBase<BarometerMsg> { class Barometer : public SensorBase<BarometerMsg> {
public: public:
bool initialize(void* _) override { bool initialize() override {
_msg.success = _bmp.begin(); _msg.success = _bmp.begin();
return _msg.success; return _msg.success;
} }
+22 -59
View File
@@ -6,10 +6,6 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <utils/math_utils.h> #include <utils/math_utils.h>
#if FT_ENABLED(USE_ICM20948)
#include "ICM_20948.h"
#endif
#if FT_ENABLED(USE_MPU6050) #if FT_ENABLED(USE_MPU6050)
#include <MPU6050_6Axis_MotionApps612.h> #include <MPU6050_6Axis_MotionApps612.h>
#endif #endif
@@ -48,7 +44,7 @@ struct IMUAnglesMsg : public SensorMessageBase {
class IMU : public SensorBase<IMUAnglesMsg> { class IMU : public SensorBase<IMUAnglesMsg> {
public: public:
bool initialize(void* _arg = nullptr) override { bool initialize() override {
#if FT_ENABLED(USE_MPU6050) #if FT_ENABLED(USE_MPU6050)
_imu.initialize(); _imu.initialize();
_msg.success = _imu.testConnection(); _msg.success = _imu.testConnection();
@@ -85,42 +81,12 @@ class IMU : public SensorBase<IMUAnglesMsg> {
return false; return false;
} }
_imu.setExtCrystalUse(true); _imu.setExtCrystalUse(true);
#endif
#if FT_ENABLED(USE_ICM20948)
#if FT_ENABLED(USE_ICM20948_SPIMODE) > 0
#define ICM_20948_USE_DMP // TODO: Move to features.ini
SPI_PORT.begin(SPI_SCK, SPI_MISO, SPI_MOSI, -1); // TODO: Move to global spi start
_imu = (ICM_20948_SPI*)_arg;
#ifndef ICM20948_ALIVE
#define ICM20948_ALIVE
_imu->begin(ICM20948_SPI_CS, SPI_PORT); ESP_LOGI("IMU", "Beginning ICM20948 in SPI mode");
_imu->initializeDMP();
#endif
#else
_imu = (ICM_20948_I2C*)_arg;
#ifndef ICM20948_ALIVE
#define ICM20948_ALIVE
_imu->begin(Wire, 1, 0xFF); ESP_LOGI("IMU", "Beginning ICM20948 in I2C mode");
#endif
#endif
if (_imu->status != ICM_20948_Stat_Ok){ ESP_LOGW("IMU", "Failed to start ICM20948: begin failed"); return false; }
_imu->setSampleMode((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), ICM_20948_Sample_Mode_Continuous);
if (_imu->status != ICM_20948_Stat_Ok){ ESP_LOGW("IMU", "Failed to start ICM20948: set sample failed"); return false; }
ICM_20948_fss_t myFSS;
myFSS.a = gpm2;
myFSS.g = dps250;
_imu->setFullScale((ICM_20948_Internal_Acc | ICM_20948_Internal_Gyr), myFSS);
if (_imu->status != ICM_20948_Stat_Ok){ ESP_LOGW("IMU", "Failed to start ICM20948: set full scale failed"); return false; }
// TODO: Setup low pass filter config
_msg.success = true;
#endif #endif
return _msg.success; return _msg.success;
} }
bool update() override { bool update() override {
if (!_msg.success) return false;
#if FT_ENABLED(USE_MPU6050) #if FT_ENABLED(USE_MPU6050)
uint16_t fifoCount = _imu.getFIFOCount(); uint16_t fifoCount = _imu.getFIFOCount();
uint8_t intStatus = _imu.getIntStatus(); uint8_t intStatus = _imu.getIntStatus();
@@ -139,21 +105,6 @@ class IMU : public SensorBase<IMUAnglesMsg> {
} }
return false; return false;
#endif #endif
#if FT_ENABLED(USE_ICM20948)
#ifndef ICM20948_GET_AGMT_UPDATED_ONCE_PER_LOOP
#define ICM20948_GET_AGMT_UPDATED_ONCE_PER_LOOP
if (_imu->dataReady())
{
_imu->getAGMT();
} else {
return false;
}
#endif
_msg.rpy[0] = _imu->accX();
_msg.rpy[1] = _imu->accY();
_msg.rpy[2] = _imu->accZ();
#endif
#if FT_ENABLED(USE_BNO055) #if FT_ENABLED(USE_BNO055)
sensors_event_t event; sensors_event_t event;
_imu.getEvent(&event); _imu.getEvent(&event);
@@ -172,6 +123,26 @@ class IMU : public SensorBase<IMUAnglesMsg> {
float getAngleZ() { return _msg.rpy[0]; } float getAngleZ() { return _msg.rpy[0]; }
bool calibrate() {
#if FT_ENABLED(USE_MPU6050)
if (!_msg.success) return false;
ESP_LOGI("IMU", "Starting calibration...");
_imu.CalibrateGyro(6);
_imu.CalibrateAccel(6);
ESP_LOGI("IMU", "Calibration complete");
return true;
#elif FT_ENABLED(USE_BNO055)
if (!_msg.success) return false;
ESP_LOGI("IMU", "Starting calibration...");
adafruit_bno055_offsets_t offsets;
bool result = _imu.getSensorOffsets(offsets);
ESP_LOGI("IMU", "Calibration complete");
return result;
#else
return false;
#endif
}
private: private:
#if FT_ENABLED(USE_MPU6050) #if FT_ENABLED(USE_MPU6050)
MPU6050 _imu; MPU6050 _imu;
@@ -183,12 +154,4 @@ class IMU : public SensorBase<IMUAnglesMsg> {
#if FT_ENABLED(USE_BNO055) #if FT_ENABLED(USE_BNO055)
Adafruit_BNO055 _imu {55, 0x29}; Adafruit_BNO055 _imu {55, 0x29};
#endif #endif
#if FT_ENABLED(USE_ICM20948)
#if FT_ENABLED(USE_ICM20948_SPIMODE) > 0
ICM_20948_SPI* _imu;
#else
//#define WIRE_PORT Wire
ICM_20948_I2C* _imu;
#endif
#endif
}; };
+10 -62
View File
@@ -11,7 +11,6 @@
#include <peripherals/sensor.hpp> #include <peripherals/sensor.hpp>
struct MagnetometerMsg : public SensorMessageBase { struct MagnetometerMsg : public SensorMessageBase {
float rpy[3] {0, 0, 0}; float rpy[3] {0, 0, 0};
float heading {-1}; float heading {-1};
@@ -39,59 +38,20 @@ struct MagnetometerMsg : public SensorMessageBase {
class Magnetometer : public SensorBase<MagnetometerMsg> { class Magnetometer : public SensorBase<MagnetometerMsg> {
public: public:
bool initialize(void* _arg) override { bool initialize() override {
#if FT_ENABLED(USE_ICM20948) _msg.success = _mag.begin();
#if FT_ENABLED(USE_ICM20948_SPIMODE) > 0
SPI_PORT.begin(SPI_SCK, SPI_MISO, SPI_MOSI, -1);
_mag = (ICM_20948_SPI*)_arg;
#ifndef ICM20948_ALIVE
#define ICM20948_ALIVE
_imu->begin(ICM20948_SPI_CS, SPI_PORT); ESP_LOGI("Magnetometer", "Beginning ICM20948 in SPI mode");
#endif
#else
_mag = (ICM_20948_I2C*)_arg;
#ifndef ICM20948_ALIVE
#define ICM20948_ALIVE
if (!_mag->isConnected()) { _mag->begin(Wire, 1, 0xFF); ESP_LOGI("Magnetometer", "Beginning ICM20948 in I2C mode"); }
#endif
#endif
if (_mag->status != ICM_20948_Stat_Ok){ return false; }
_mag->startupMagnetometer();
if (_mag->status != ICM_20948_Stat_Ok){ return false; }
_msg.success = true;
#elif FT_ENABLED(USE_HMC5883)
_msg.success = _mag.begin();
#endif
return _msg.success; return _msg.success;
} }
bool update() override { bool update() override {
if (!_msg.success) return false; if (!_msg.success) return false;
#if FT_ENABLED(USE_ICM20948) sensors_event_t event;
#ifndef ICM20948_GET_AGMT_UPDATED_ONCE_PER_LOOP bool updated = _mag.getEvent(&event);
#define ICM20948_GET_AGMT_UPDATED_ONCE_PER_LOOP if (!updated) return false;
if (_imu->dataReady()) _msg.rpy[0] = event.magnetic.x;
{ _msg.rpy[1] = event.magnetic.y;
_imu->getAGMT(); _msg.rpy[2] = event.magnetic.z;
} else { _msg.heading = atan2(event.magnetic.y, event.magnetic.x);
return false;
}
#endif
_msg.rpy[0] = _mag->magX();
_msg.rpy[1] = _mag->magY();
_msg.rpy[2] = _mag->magZ();
#elif FT_ENABLED(USE_HMC5883)
sensors_event_t event;
bool updated = _mag.getEvent(&event);
if (!updated) return false;
_msg.rpy[0] = event.magnetic.x;
_msg.rpy[1] = event.magnetic.y;
_msg.rpy[2] = event.magnetic.z;
#endif
_msg.heading = atan2(_msg.rpy[1], _msg.rpy[0]); // atan2(y, x)
_msg.heading += declinationAngle; _msg.heading += declinationAngle;
if (_msg.heading < 0) _msg.heading += 2 * PI; if (_msg.heading < 0) _msg.heading += 2 * PI;
if (_msg.heading > 2 * PI) _msg.heading -= 2 * PI; if (_msg.heading > 2 * PI) _msg.heading -= 2 * PI;
@@ -108,18 +68,6 @@ class Magnetometer : public SensorBase<MagnetometerMsg> {
float getHeading() { return _msg.heading; } float getHeading() { return _msg.heading; }
private: private:
Adafruit_HMC5883_Unified _mag {12345};
#if FT_ENABLED(USE_ICM20948)
#if FT_ENABLED(USE_ICM20948_SPIMODE) > 0
#define SPI_PORT SPI // TODO in periphearals_seetings.h
#define CS_PIN 2
ICM_20948_SPI* _mag;
#else
//#define WIRE_PORT Wire
ICM_20948_I2C* _mag;
#endif
#elif FT_ENABLED(USE_HMC5883)
Adafruit_HMC5883_Unified _mag {12345};
#endif
const float declinationAngle = 0.22; const float declinationAngle = 0.22;
}; };
+4 -2
View File
@@ -74,6 +74,8 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
float leftDistance(); float leftDistance();
float rightDistance(); float rightDistance();
bool calibrateIMU();
StatefulHttpEndpoint<PeripheralsConfiguration> endpoint; StatefulHttpEndpoint<PeripheralsConfiguration> endpoint;
private: private:
@@ -87,10 +89,10 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
JsonDocument doc; JsonDocument doc;
char message[MAX_ESP_IMU_SIZE]; char message[MAX_ESP_IMU_SIZE];
#if FT_ENABLED(USE_MPU6050 || USE_BNO055 || USE_ICM20948) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
IMU _imu; IMU _imu;
#endif #endif
#if FT_ENABLED(USE_HMC5883) || FT_ENABLED(USE_ICM20948) #if FT_ENABLED(USE_HMC5883)
Magnetometer _mag; Magnetometer _mag;
#endif #endif
#if FT_ENABLED(USE_BMP180) #if FT_ENABLED(USE_BMP180)
+1 -1
View File
@@ -17,7 +17,7 @@ class SensorBase {
public: public:
SensorBase() {} SensorBase() {}
virtual bool initialize(void* _arg) = 0; virtual bool initialize() = 0;
virtual bool update() = 0; virtual bool update() = 0;
@@ -15,7 +15,7 @@
#define SCL_PIN SCL #define SCL_PIN SCL
#endif #endif
#ifndef I2C_FREQUENCY #ifndef I2C_FREQUENCY
#define I2C_FREQUENCY 400000UL #define I2C_FREQUENCY 1000000UL
#endif #endif
class PinConfig { class PinConfig {
-46
View File
@@ -39,49 +39,3 @@
name##_count = 0; \ name##_count = 0; \
last_time = esp_timer_get_time() / 1000; \ last_time = esp_timer_get_time() / 1000; \
} }
#define CALLS_PER_SECOND_TIMED_START_TICK(name, function) \
static uint64_t name##_##function##_start = 0; \
static uint64_t name##_##function##_total_time = 0; \
static uint64_t name##_##function##_call_count = 0; \
name##_##function##_start = esp_timer_get_time();
#define CALLS_PER_SECOND_TIMED_END_TICK(name, function) \
name##_##function##_total_time += (esp_timer_get_time() - name##_##function##_start); \
name##_##function##_call_count++;
#define CALLS_PER_SECOND_TIMED_CALL(name, function, call) \
static uint64_t name##_##function##_total_time = 0; \
static uint64_t name##_##function##_call_count = 0; \
do { \
uint64_t name##_##function##_start = esp_timer_get_time(); \
call; \
name##_##function##_total_time += (esp_timer_get_time() - name##_##function##_start); \
name##_##function##_call_count++; \
} while (0)
#define CALLS_PER_SECOND_TIMED_FUNC_PRINT(name, function) \
if (name##_##function##_call_count > 0) { \
uint64_t avg = name##_##function##_total_time / name##_##function##_call_count; \
if (avg < 1000) { \
ESP_LOGI("Timing", " %s: %llu us (avg over %llu calls)", \
#function, avg, name##_##function##_call_count); \
} else { \
ESP_LOGI("Timing", " %s: %llu ms (avg over %llu calls)", \
#function, avg / 1000, name##_##function##_call_count); \
} \
name##_##function##_total_time = 0; \
name##_##function##_call_count = 0; \
}
#define CALLS_PER_SECOND_TIMED(name, ...) \
do { \
static uint64_t name##_last_print = 0; \
uint64_t name##_current_time = esp_timer_get_time() / 1000; \
if (name##_current_time - name##_last_print >= 1000) { \
ESP_LOGI("Timing", "=== %s Average Timings ===", #name); \
__VA_ARGS__ \
name##_last_print = name##_current_time; \
} \
} while (0)
+2 -5
View File
@@ -12,14 +12,11 @@ void printFeatureConfiguration() {
ESP_LOGI("Features", "USE_MOTION: %s", USE_MOTION ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_MOTION: %s", USE_MOTION ? "enabled" : "disabled");
// Sensors // Sensors
ESP_LOGI("Features", "USE_ICM20948: %s", USE_ICM20948 ? "enabled" : "disabled");
ESP_LOGI("Features", "USE_BNO055: %s", USE_BNO055 ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_BNO055: %s", USE_BNO055 ? "enabled" : "disabled");
ESP_LOGI("Features", "USE_MPU6050: %s", USE_MPU6050 ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_MPU6050: %s", USE_MPU6050 ? "enabled" : "disabled");
ESP_LOGI("Features", "USE_HMC5883: %s", USE_HMC5883 ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_HMC5883: %s", USE_HMC5883 ? "enabled" : "disabled");
ESP_LOGI("Features", "USE_BMP180: %s", USE_BMP180 ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_BMP180: %s", USE_BMP180 ? "enabled" : "disabled");
ESP_LOGI("Features", "USE_USS: %s", USE_USS ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_USS: %s", USE_USS ? "enabled" : "disabled");
ESP_LOGI("Features", "USE_PAJ7620U2: %s", USE_PAJ7620U2 ? "enabled" : "disabled");
// Peripherals // Peripherals
ESP_LOGI("Features", "USE_PCA9685: %s", USE_PCA9685 ? "enabled" : "disabled"); ESP_LOGI("Features", "USE_PCA9685: %s", USE_PCA9685 ? "enabled" : "disabled");
@@ -34,8 +31,8 @@ void printFeatureConfiguration() {
void features(JsonObject &root) { void features(JsonObject &root) {
root["camera"] = USE_CAMERA ? true : false; root["camera"] = USE_CAMERA ? true : false;
root["imu"] = (USE_MPU6050 || USE_BNO055 || USE_ICM20948) ? true : false; root["imu"] = (USE_MPU6050 || USE_BNO055) ? true : false;
root["mag"] = (USE_HMC5883 || USE_BNO055 || USE_ICM20948) ? true : false; root["mag"] = (USE_HMC5883 || USE_BNO055) ? true : false;
root["bmp"] = USE_BMP180 ? true : false; root["bmp"] = USE_BMP180 ? true : false;
root["sonar"] = USE_USS ? true : false; root["sonar"] = USE_USS ? true : false;
root["servo"] = USE_PCA9685 ? true : false; root["servo"] = USE_PCA9685 ? true : false;
+14
View File
@@ -22,6 +22,20 @@ static Initializer initializer;
esp_err_t getFiles(PsychicRequest *request) { return request->reply(200, "application/json", listFiles("/").c_str()); } esp_err_t getFiles(PsychicRequest *request) { return request->reply(200, "application/json", listFiles("/").c_str()); }
esp_err_t getConfigFile(PsychicRequest *request) {
String path = "/config" + request->uri().substring(11);
if (!ESP_FS.exists(path)) {
return request->reply(404, "text/plain", "File not found");
}
File file = ESP_FS.open(path, "r");
if (!file) {
return request->reply(500, "text/plain", "Failed to open file");
}
String content = file.readString();
file.close();
return request->reply(200, "application/json", content.c_str());
}
esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json) { esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json) {
if (json.is<JsonObject>()) { if (json.is<JsonObject>()) {
const char *filename = json["file"].as<const char *>(); const char *filename = json["file"].as<const char *>();
+111 -13
View File
@@ -33,17 +33,29 @@ LEDService ledService;
#if FT_ENABLED(USE_CAMERA) #if FT_ENABLED(USE_CAMERA)
Camera::CameraService cameraService; Camera::CameraService cameraService;
#endif #endif
#if FT_ENABLED(USE_MDNS)
MDNSService mdnsService;
#endif
// Service // Service
WiFiService wifiService; WiFiService wifiService;
APService apService; APService apService;
void setupServer() { void setupServer() {
server.config.max_uri_handlers = 10 + WWW_ASSETS_COUNT; server.config.max_uri_handlers = 32 + WWW_ASSETS_COUNT;
server.maxUploadSize = 1000000; // 1 MB; server.maxUploadSize = 1000000; // 1 MB;
server.listen(80); server.listen(80);
server.serveStatic("/api/config/", ESP_FS, "/config/");
server.on("/api/features", feature_service::getFeatures); server.on("/api/features", feature_service::getFeatures);
server.on("/api/system/status", HTTP_GET,
[&](PsychicRequest *request) { return system_service::getStatus(request); });
server.on("/api/system/reset", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return system_service::handleReset(request); });
server.on("/api/system/restart", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return system_service::handleRestart(request); });
server.on("/api/system/sleep", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return system_service::handleSleep(request); });
server.on("/api/system/metrics", HTTP_GET,
[&](PsychicRequest *request) { return system_service::getMetrics(request); });
#if USE_CAMERA #if USE_CAMERA
server.on("/api/camera/still", HTTP_GET, server.on("/api/camera/still", HTTP_GET,
[&](PsychicRequest *request) { return cameraService.cameraStill(request); }); [&](PsychicRequest *request) { return cameraService.cameraStill(request); });
@@ -60,6 +72,56 @@ void setupServer() {
server.on("/api/servo/config", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) { server.on("/api/servo/config", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) {
return servoController.endpoint.handleStateUpdate(request, json); return servoController.endpoint.handleStateUpdate(request, json);
}); });
// WiFi
server.on("/api/wifi/sta/settings", HTTP_GET,
[&](PsychicRequest *request) { return wifiService.endpoint.getState(request); });
server.on("/api/wifi/sta/settings", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) {
return wifiService.endpoint.handleStateUpdate(request, json);
});
server.on("/api/wifi/scan", HTTP_GET, [&](PsychicRequest *request) { return wifiService.handleScan(request); });
server.on("/api/wifi/networks", HTTP_GET,
[&](PsychicRequest *request) { return wifiService.getNetworks(request); });
server.on("/api/wifi/sta/status", HTTP_GET,
[&](PsychicRequest *request) { return wifiService.getNetworkStatus(request); });
// AP
server.on("/api/ap/status", HTTP_GET, [&](PsychicRequest *request) { return apService.getStatus(request); });
server.on("/api/ap/settings", HTTP_GET,
[&](PsychicRequest *request) { return apService.endpoint.getState(request); });
server.on("/api/ap/settings", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) {
return apService.endpoint.handleStateUpdate(request, json);
});
// Peripherals
server.on("/api/peripherals", HTTP_GET,
[&](PsychicRequest *request) { return peripherals.endpoint.getState(request); });
server.on("/api/peripherals", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) {
return peripherals.endpoint.handleStateUpdate(request, json);
});
// MDNS
#if FT_ENABLED(USE_MDNS)
server.on("/api/mdns", HTTP_GET, [&](PsychicRequest *request) { return mdnsService.endpoint.getState(request); });
server.on("/api/mdns", HTTP_POST, [&](PsychicRequest *request, JsonVariant &json) {
return mdnsService.endpoint.handleStateUpdate(request, json);
});
server.on("/api/mdns/status", HTTP_GET, [&](PsychicRequest *request) { return mdnsService.getStatus(request); });
server.on("/api/mdns/query", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return mdnsService.queryServices(request, json); });
#endif
// Filesystem
server.on("/api/config/*", HTTP_GET, [](PsychicRequest *request) { return FileSystem::getConfigFile(request); });
server.on("/api/files", HTTP_GET, [&](PsychicRequest *request) { return FileSystem::getFiles(request); });
server.on("/api/files", HTTP_POST, FileSystem::uploadHandler);
server.on("/api/files/delete", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return FileSystem::handleDelete(request, json); });
server.on("/api/files/edit", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return FileSystem::handleEdit(request, json); });
server.on("/api/files/mkdir", HTTP_POST,
[&](PsychicRequest *request, JsonVariant &json) { return FileSystem::mkdir(request, json); });
#if EMBED_WEBAPP #if EMBED_WEBAPP
mountStaticAssets(server); mountStaticAssets(server);
#endif #endif
@@ -82,6 +144,10 @@ void setupServer() {
#define EVENT_I2C_SCAN "i2cScan" #define EVENT_I2C_SCAN "i2cScan"
#define EVENT_SERVO_CONFIGURATION_SETTINGS "servoPWM" #define EVENT_SERVO_CONFIGURATION_SETTINGS "servoPWM"
#define EVENT_SERVO_STATE "servoState" #define EVENT_SERVO_STATE "servoState"
#define EVENT_DISPLACEMENT "displacement"
#define EVENT_SKILL "skill"
#define EVENT_SKILL_STATUS "skill_status"
#define EVENT_IMU_CALIBRATE "imuCalibrate"
void setupEventSocket() { void setupEventSocket() {
// Motion events // Motion events
@@ -107,11 +173,39 @@ void setupEventSocket() {
socket.emit(EVENT_I2C_SCAN, results); socket.emit(EVENT_I2C_SCAN, results);
}); });
socket.onEvent(EVENT_IMU_CALIBRATE, [&](JsonVariant &root, int originId) {
JsonDocument doc;
JsonVariant results = doc.to<JsonVariant>();
results["success"] = peripherals.calibrateIMU();
socket.emit(EVENT_IMU_CALIBRATE, results);
});
// Servo controller events // Servo controller events
socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS, socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
[&](JsonVariant &root, int originId) { servoController.servoEvent(root, originId); }); [&](JsonVariant &root, int originId) { servoController.servoEvent(root, originId); });
socket.onEvent(EVENT_SERVO_STATE, socket.onEvent(EVENT_SERVO_STATE,
[&](JsonVariant &root, int originId) { servoController.stateUpdate(root, originId); }); [&](JsonVariant &root, int originId) { servoController.stateUpdate(root, originId); });
// Skill events
socket.onEvent(EVENT_DISPLACEMENT,
[&](JsonVariant &root, int originId) { motionService.handleDisplacement(root, originId); });
socket.onEvent(EVENT_SKILL, [&](JsonVariant &root, int originId) { motionService.handleSkill(root, originId); });
socket.onEvent(EVENT_SKILL_STATUS, [&](JsonVariant &root, int originId) {
JsonDocument doc;
JsonVariant results = doc.to<JsonVariant>();
motionService.getDisplacementResult(results);
socket.emit(EVENT_SKILL_STATUS, results);
});
motionService.setSkillCompleteCallback([&]() {
JsonDocument doc;
JsonVariant results = doc.to<JsonVariant>();
motionService.getDisplacementResult(results);
results["event"] = "complete";
socket.emit(EVENT_SKILL_STATUS, results);
});
} }
void IRAM_ATTR SpotControlLoopEntry(void *) { void IRAM_ATTR SpotControlLoopEntry(void *) {
@@ -122,22 +216,18 @@ void IRAM_ATTR SpotControlLoopEntry(void *) {
peripherals.begin(); peripherals.begin();
servoController.begin(); servoController.begin();
motionService.begin(); motionService.begin();
peripherals.calibrateIMU();
for (;;) { for (;;) {
CALLS_PER_SECOND_TIMED_CALL(SpotControlLoopEntry, peripherals_update, peripherals.update()); CALLS_PER_SECOND(SpotControlLoopEntry);
CALLS_PER_SECOND_TIMED_CALL(SpotControlLoopEntry, motionService_update, motionService.update(&peripherals)); peripherals.update();
CALLS_PER_SECOND_TIMED_CALL(SpotControlLoopEntry, servoController_setAngles, servoController.setAngles(motionService.getAngles())); motionService.update(&peripherals);
CALLS_PER_SECOND_TIMED_CALL(SpotControlLoopEntry, servoController_update, servoController.update()); servoController.setAngles(motionService.getAngles());
servoController.update();
#if FT_ENABLED(USE_WS2812) #if FT_ENABLED(USE_WS2812)
ledService.loop(); ledService.loop();
#endif #endif
// CALLS_PER_SECOND_TIMED(SpotControlLoopEntry, vTaskDelayUntil(&xLastWakeTime, xFrequency);
// CALLS_PER_SECOND_TIMED_FUNC_PRINT(SpotControlLoopEntry, peripherals_update)
// CALLS_PER_SECOND_TIMED_FUNC_PRINT(SpotControlLoopEntry, motionService_update)
// CALLS_PER_SECOND_TIMED_FUNC_PRINT(SpotControlLoopEntry, servoController_setAngles)
// CALLS_PER_SECOND_TIMED_FUNC_PRINT(SpotControlLoopEntry, servoController_update)
// );
// vTaskDelayUntil(&xLastWakeTime, xFrequency);
} }
} }
@@ -169,6 +259,14 @@ void IRAM_ATTR serviceLoopEntry(void *) {
peripherals.getIMUResult(results); peripherals.getIMUResult(results);
socket.emit(EVENT_IMU, results); socket.emit(EVENT_IMU, results);
}); });
EXECUTE_EVERY_N_MS(200, {
if (motionService.isSkillActive()) {
JsonDocument doc;
JsonVariant results = doc.to<JsonVariant>();
motionService.getDisplacementResult(results);
socket.emit(EVENT_SKILL_STATUS, results);
}
});
vTaskDelay(100 / portTICK_PERIOD_MS); vTaskDelay(100 / portTICK_PERIOD_MS);
} }
+62 -1
View File
@@ -46,6 +46,65 @@ void MotionService::handleMode(JsonVariant &root, int originId) {
} }
} }
void MotionService::handleDisplacement(JsonVariant &root, int originId) {
std::string action = root["action"] | "";
if (action == "reset") {
resetDisplacement();
ESP_LOGI("MotionService", "Displacement reset");
} else if (action == "clear") {
clearSkill();
skillWasComplete = false;
ESP_LOGI("MotionService", "Skill cleared");
}
}
void MotionService::handleSkill(JsonVariant &root, int originId) {
float x = root["x"] | 0.0f;
float z = root["z"] | 0.0f;
float yaw = root["yaw"] | 0.0f;
float speed = root["speed"] | 0.5f;
setSkillTarget(x, z, yaw);
skillWasComplete = false;
float linear_mag = std::hypot(x, z);
bool has_linear = linear_mag > 0.001f;
bool has_yaw = std::fabs(yaw) > 0.001f;
if (has_linear || has_yaw) {
if (has_linear) {
float norm_x = x / linear_mag;
float norm_z = z / linear_mag;
command.ly = norm_x;
command.lx = -norm_z;
} else {
command.ly = 0;
command.lx = 0;
}
command.rx = has_yaw ? (yaw > 0 ? 1.0f : -1.0f) : 0;
command.s = speed;
if (state) state->handleCommand(command);
}
ESP_LOGI("MotionService", "Skill set: Walk(%.3f, %.3f, %.3f) speed=%.2f", x, z, yaw, speed);
}
void MotionService::checkSkillComplete() {
if (!body_state.skill.active) return;
if (skillWasComplete) return;
if (body_state.skill.isComplete()) {
skillWasComplete = true;
command = {0, 0, 0, 0, command.h, 0, command.s1};
if (state) state->handleCommand(command);
ESP_LOGI("MotionService", "Skill complete! Traveled: (%.3f, %.3f), Rotated: %.3f", body_state.skill.traveled_x,
body_state.skill.traveled_z, body_state.skill.rotated);
if (skillCompleteCallback) {
skillCompleteCallback();
}
}
}
void MotionService::handleGestures(const gesture_t ges) { void MotionService::handleGestures(const gesture_t ges) {
if (ges != gesture_t::eGestureNone) { if (ges != gesture_t::eGestureNone) {
ESP_LOGI("Motion", "Gesture: %d", ges); ESP_LOGI("Motion", "Gesture: %d", ges);
@@ -64,10 +123,12 @@ bool MotionService::update(Peripherals *peripherals) {
handleGestures(peripherals->takeGesture()); handleGestures(peripherals->takeGesture());
if (!state) return false; if (!state) return false;
int64_t now = esp_timer_get_time(); int64_t now = esp_timer_get_time();
float dt = (now - lastUpdate) / 1000000.0f; // Convert microseconds to seconds float dt = (now - lastUpdate) / 1000000.0f;
lastUpdate = now; lastUpdate = now;
state->updateImuOffsets(peripherals->angleY(), peripherals->angleX()); state->updateImuOffsets(peripherals->angleY(), peripherals->angleX());
ESP_LOGI("MotionService", "IMU Offsets: %.3f, %.3f", peripherals->angleY(), peripherals->angleX());
state->step(body_state, dt); state->step(body_state, dt);
checkSkillComplete();
kinematics.calculate_inverse_kinematics(body_state, new_angles); kinematics.calculate_inverse_kinematics(body_state, new_angles);
return update_angles(new_angles, angles); return update_angles(new_angles, angles);
+27 -53
View File
@@ -15,40 +15,18 @@ void Peripherals::begin() {
updatePins(); updatePins();
#if FT_ENABLED(USE_ICM20948)
#if FT_ENABLED(USE_ICM20948_SPIMODE) > 0
ICM_20948_SPI* icm20948 = new ICM_20948_SPI;
#else
ICM_20948_I2C* icm20948 = new ICM_20948_I2C;
#endif
#endif
// --- IMU ---
#if FT_ENABLED(USE_MPU6050 || USE_BNO055) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
if (!_imu.initialize(nullptr)) ESP_LOGE("Peripherals", "IMU initialize failed"); if (!_imu.initialize()) ESP_LOGE("IMUService", "IMU initialize failed");
#elif FT_ENABLED(USE_ICM20948)
if (!_imu.initialize(icm20948)) ESP_LOGE("Peripherals", "IMU initialize failed (ICM20948)");
#endif #endif
// --- MAGNETOMETER ---
#if FT_ENABLED(USE_HMC5883) #if FT_ENABLED(USE_HMC5883)
if (!_mag.initialize(nullptr)) ESP_LOGE("Peripherals", "MAG initialize failed"); if (!_mag.initialize()) ESP_LOGE("IMUService", "MAG initialize failed");
#elif FT_ENABLED(USE_ICM20948)
if (!_mag.initialize(icm20948)) ESP_LOGE("Peripherals", "MAG initialize failed (ICM20948)");
#endif #endif
// --- BMP ---
#if FT_ENABLED(USE_BMP180) #if FT_ENABLED(USE_BMP180)
if (!_bmp.initialize(nullptr)) ESP_LOGE("Peripherals", "BMP initialize failed"); if (!_bmp.initialize()) ESP_LOGE("IMUService", "BMP initialize failed");
#endif #endif
// --- GESTURE ---
#if FT_ENABLED(USE_PAJ7620U2) #if FT_ENABLED(USE_PAJ7620U2)
if (!_gesture.initialize(nullptr)) ESP_LOGE("Peripherals", "Gesture initialize failed"); if (!_gesture.initialize()) ESP_LOGE("IMUService", "Gesture initialize failed");
#endif #endif
// --- SONAR ---
#if FT_ENABLED(USE_USS) #if FT_ENABLED(USE_USS)
_left_sonar = std::make_unique<NewPing>(USS_LEFT_PIN, USS_LEFT_PIN, MAX_DISTANCE); _left_sonar = std::make_unique<NewPing>(USS_LEFT_PIN, USS_LEFT_PIN, MAX_DISTANCE);
_right_sonar = std::make_unique<NewPing>(USS_RIGHT_PIN, USS_RIGHT_PIN, MAX_DISTANCE); _right_sonar = std::make_unique<NewPing>(USS_RIGHT_PIN, USS_RIGHT_PIN, MAX_DISTANCE);
@@ -56,25 +34,11 @@ void Peripherals::begin() {
}; };
void Peripherals::update() { void Peripherals::update() {
bool res = true; EXECUTE_EVERY_N_MS(20, { readImu(); });
CALLS_PER_SECOND_TIMED_CALL(Peripherals_update, read_imu, res = readImu()); EXECUTE_EVERY_N_MS(100, { readMag(); });
#ifdef FT_ENABLED(USE_ICM20948) EXECUTE_EVERY_N_MS(100, { readGesture(); });
// IF ICM_20948 fails to get IMU, it means that mag also does not have new data EXECUTE_EVERY_N_MS(500, { readBMP(); });
CALLS_PER_SECOND_TIMED_CALL(Peripherals_update, read_mag, if (res) { res = readMag(); } ); EXECUTE_EVERY_N_MS(500, { readSonar(); });
#else
CALLS_PER_SECOND_TIMED_CALL(Peripherals_update, read_mag, res = readMag());
#endif
CALLS_PER_SECOND_TIMED_CALL(Peripherals_update, read_gesture, EXECUTE_EVERY_N_MS(100, { readGesture(); }) );
CALLS_PER_SECOND_TIMED_CALL(Peripherals_update, read_bmp, EXECUTE_EVERY_N_MS(500, { readBMP(); }) );
CALLS_PER_SECOND_TIMED_CALL(Peripherals_update, read_sonar, EXECUTE_EVERY_N_MS(500, { readSonar(); }) );
CALLS_PER_SECOND_TIMED(Peripherals_update,
CALLS_PER_SECOND_TIMED_FUNC_PRINT(Peripherals_update, read_imu)
CALLS_PER_SECOND_TIMED_FUNC_PRINT(Peripherals_update, read_mag)
CALLS_PER_SECOND_TIMED_FUNC_PRINT(Peripherals_update, read_gesture)
CALLS_PER_SECOND_TIMED_FUNC_PRINT(Peripherals_update, read_bmp)
CALLS_PER_SECOND_TIMED_FUNC_PRINT(Peripherals_update, read_sonar)
);
} }
void Peripherals::updatePins() { void Peripherals::updatePins() {
@@ -84,7 +48,6 @@ void Peripherals::updatePins() {
if (state().sda != -1 && state().scl != -1) { if (state().sda != -1 && state().scl != -1) {
Wire.begin(state().sda, state().scl, state().frequency); Wire.begin(state().sda, state().scl, state().frequency);
ESP_LOGI("Peripherals", "Starting Wire with SDA=%d, SCL=%d, FREQ=%d", state().sda, state().scl, state().frequency);
i2c_active = true; i2c_active = true;
} }
} }
@@ -116,7 +79,7 @@ void Peripherals::scanI2C(uint8_t lower, uint8_t higher) {
/* IMU FUNCTIONS */ /* IMU FUNCTIONS */
bool Peripherals::readImu() { bool Peripherals::readImu() {
bool updated = false; bool updated = false;
#if FT_ENABLED(USE_MPU6050 || USE_BNO055 || USE_ICM20948) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
beginTransaction(); beginTransaction();
updated = _imu.update(); updated = _imu.update();
endTransaction(); endTransaction();
@@ -126,7 +89,7 @@ bool Peripherals::readImu() {
bool Peripherals::readMag() { bool Peripherals::readMag() {
bool updated = false; bool updated = false;
#if FT_ENABLED(USE_HMC5883 || USE_ICM20948) #if FT_ENABLED(USE_HMC5883)
beginTransaction(); beginTransaction();
updated = _mag.update(); updated = _mag.update();
endTransaction(); endTransaction();
@@ -164,7 +127,7 @@ void Peripherals::readSonar() {
float Peripherals::angleX() { float Peripherals::angleX() {
return return
#if FT_ENABLED(USE_MPU6050 || USE_BNO055 || USE_ICM20948) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
_imu.getAngleX(); _imu.getAngleX();
#else #else
0; 0;
@@ -173,7 +136,7 @@ float Peripherals::angleX() {
float Peripherals::angleY() { float Peripherals::angleY() {
return return
#if FT_ENABLED(USE_MPU6050 || USE_BNO055 || USE_ICM20948) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
_imu.getAngleY(); _imu.getAngleY();
#else #else
0; 0;
@@ -182,7 +145,7 @@ float Peripherals::angleY() {
float Peripherals::angleZ() { float Peripherals::angleZ() {
return return
#if FT_ENABLED(USE_MPU6050 || USE_BNO055 || USE_ICM20948) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
_imu.getAngleZ(); _imu.getAngleZ();
#else #else
0; 0;
@@ -202,11 +165,11 @@ float Peripherals::leftDistance() { return _left_distance; }
float Peripherals::rightDistance() { return _right_distance; } float Peripherals::rightDistance() { return _right_distance; }
void Peripherals::getIMUResult(JsonVariant &root) { void Peripherals::getIMUResult(JsonVariant &root) {
#if FT_ENABLED(USE_MPU6050 || USE_BNO055 || USE_ICM20948) #if FT_ENABLED(USE_MPU6050 || USE_BNO055)
JsonVariant imu = root["imu"].to<JsonVariant>(); JsonVariant imu = root["imu"].to<JsonVariant>();
_imu.getResults(imu); _imu.getResults(imu);
#endif #endif
#if FT_ENABLED(USE_HMC5883 || USE_ICM20948) // TODO: #if FT_ENABLED(USE_HMC5883)
JsonVariant mag = root["mag"].to<JsonVariant>(); JsonVariant mag = root["mag"].to<JsonVariant>();
_mag.getResults(mag); _mag.getResults(mag);
#endif #endif
@@ -222,4 +185,15 @@ void Peripherals::getSonarResult(JsonVariant &root) {
array[0] = _left_distance; array[0] = _left_distance;
array[1] = _right_distance; array[1] = _right_distance;
#endif #endif
}
bool Peripherals::calibrateIMU() {
#if FT_ENABLED(USE_MPU6050 || USE_BNO055)
beginTransaction();
bool result = _imu.calibrate();
endTransaction();
return result;
#else
return false;
#endif
} }
+2 -6
View File
@@ -23,12 +23,8 @@ void mountStaticAssets(PsychicHttpServer& server) {
auto* handle = new (&buf[i * sizeof(PsychicWebHandler)]) PsychicWebHandler(); auto* handle = new (&buf[i * sizeof(PsychicWebHandler)]) PsychicWebHandler();
handle->onRequest([a](PsychicRequest* req) { return web_send(req, *a); }); handle->onRequest([a](PsychicRequest* req) { return web_send(req, *a); });
server.on(a->uri, HTTP_GET, handle); server.on(a->uri, HTTP_GET, handle);
} if (strcmp(a->uri, WWW_OPT.default_uri) == 0) {
for (size_t i = 0; i < WWW_ASSETS_COUNT; i++) { server.defaultEndpoint->setHandler(handle);
if (strcmp(WWW_ASSETS[i].uri, WWW_OPT.default_uri) == 0) {
server.defaultEndpoint->setHandler(
reinterpret_cast<PsychicWebHandler*>(&buf[i * sizeof(PsychicWebHandler)]));
break;
} }
} }
} }
-6
View File
@@ -55,10 +55,6 @@ build_flags =
-D USS_RIGHT_PIN=14 -D USS_RIGHT_PIN=14
-D SDA_PIN=47 -D SDA_PIN=47
-D SCL_PIN=21 -D SCL_PIN=21
-D SPI_SCK=41
-D SPI_MISO=19
-D SPI_MOSI=20
-D ICM20948_SPI_CS=2 # Only needed if ICM20948 is in SPI mode
[env:seeed-xiao-esp32s3] [env:seeed-xiao-esp32s3]
platform = espressif32 platform = espressif32
@@ -94,7 +90,6 @@ build_flags =
${factory_settings.build_flags} ${factory_settings.build_flags}
${features.build_flags} ${features.build_flags}
${build_settings.build_flags} ${build_settings.build_flags}
-D SPI_PORT=SPI # Define which spi port to use for external components
-D CORE_DEBUG_LEVEL=4 -D CORE_DEBUG_LEVEL=4
-D register= -D register=
-std=gnu++2a -std=gnu++2a
@@ -121,7 +116,6 @@ lib_deps =
adafruit/Adafruit Unified Sensor@^1.1.14 adafruit/Adafruit Unified Sensor@^1.1.14
adafruit/Adafruit BNO055@^1.6.4 adafruit/Adafruit BNO055@^1.6.4
FastLED@3.5.0 FastLED@3.5.0
sparkfun/SparkFun 9DoF IMU Breakout - ICM 20948 - Arduino Library@^1.3.2
extra_scripts = extra_scripts =
pre:esp32/scripts/pre_build.py pre:esp32/scripts/pre_build.py
pre:esp32/scripts/build_app.py pre:esp32/scripts/build_app.py