-
Notifications
You must be signed in to change notification settings - Fork 2
/
SimulatedTwoSidedHardware.scala
191 lines (157 loc) · 7.09 KB
/
SimulatedTwoSidedHardware.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package com.lynbrookrobotics.potassium.model.simulations
import com.lynbrookrobotics.potassium.{Component, Signal}
import com.lynbrookrobotics.potassium.clock.Clock
import com.lynbrookrobotics.potassium.commons.cartesianPosition.XYPosition
import com.lynbrookrobotics.potassium.commons.drivetrain._
import com.lynbrookrobotics.potassium.commons.drivetrain.TwoSidedDrive
import com.lynbrookrobotics.potassium.streams.Stream
import com.lynbrookrobotics.potassium.units.Point
import squants.mass.MomentOfInertia
import squants.{Acceleration, Angle, Dimensionless, Length, Mass, Percent, Velocity}
import squants.motion.{AngularVelocity, Force, _}
import squants.space.{Degrees, Meters, Radians}
import squants.time.{Seconds, Time}
import scala.collection.mutable
case class MomentInHistory(time: Time,
forwardPosition: Length,
angle: Angle,
forwardVelocity: Velocity,
turnSpeed: AngularVelocity,
position: Point)
case class RobotVelocities(left: Velocity,
right: Velocity,
angular: AngularVelocity)
case class TwoSidedDriveForce(left: Force, right: Force)
class SimulatedTwoSidedHardware(constantFriction: Force,
override val track: Length,
mass: Mass,
momentOfInertia: MomentOfInertia,
clock: Clock,
period: Time)
(implicit props: TwoSidedDriveProperties) extends TwoSidedDriveHardware {
val leftMotor = new SimulatedMotor(clock, period)
val rightMotor = new SimulatedMotor(clock, period)
private val maxMotorForce = mass * props.maxAcceleration / 2
private val leftForceOutput = leftMotor.outputStream.map(_.toEach * maxMotorForce)
private val rightForceOutput = rightMotor.outputStream.map(_.toEach * maxMotorForce)
def incrementVelocities(leftInputForce: Force,
rightInputForce: Force,
leftVelocity: Velocity,
rightVelocity: Velocity,
angularVelocity: AngularVelocity,
dt: Time): RobotVelocities = {
val leftFriction = PhysicsUtil.frictionAndEMF(leftVelocity, constantFriction, maxMotorForce)
val netLeftForce = leftInputForce + leftFriction
val rightFriction = PhysicsUtil.frictionAndEMF(rightVelocity, constantFriction, maxMotorForce)
val netRightForce = rightInputForce + rightFriction
// Newton's second law for linear acceleration
val leftAccel = netLeftForce / mass
val rightAccel = netRightForce / mass
// Euler's method to integrate
val newLeftVelocity = leftVelocity + leftAccel * dt
val newRightVelocity = rightVelocity + rightAccel * dt
// radius from center, located halfway between wheels
val radius = track / 2
val netTorque = (netRightForce * radius - netLeftForce * radius).asTorque
// Newton's second law for angular acceleration
val angularAcceleration = netTorque / momentOfInertia
val newAngularVelocity = angularVelocity + angularAcceleration * dt
RobotVelocities(newLeftVelocity, newRightVelocity, newAngularVelocity)
}
private val InitialSpeeds = RobotVelocities(MetersPerSecond(0), MetersPerSecond(0), RadiansPerSecond(0))
private val twoSidedOutputs = leftForceOutput.zip(rightForceOutput).map(o =>
TwoSidedDriveForce(o._1, o._2))
private val velocities = twoSidedOutputs.zipWithDt.scanLeft(InitialSpeeds) {
case (accVelocities, (outputs, dt)) =>
incrementVelocities(
outputs.left,
outputs.right,
accVelocities.left,
accVelocities.right,
accVelocities.angular,
dt)
}
def listenTo[T](stream: Stream[T]): () => Option[T] = {
var previous: Option[T] = None
val handle = stream.foreach(v => previous = Some(v))
() => {
// hold reference to handle to prevent garbage collection
handle.hashCode()
previous
}
}
override val leftVelocity = velocities.map(_.left)
override val rightVelocity = velocities.map(_.right)
// convert triginometric velocity to compass velocity
override lazy val turnVelocity = velocities.map(-1 * _.angular)
override val leftPosition = leftVelocity.integral
override val rightPosition = rightVelocity.integral
override lazy val turnPosition = turnVelocity.integral
val position = XYPosition(turnPosition.map(a => Degrees(90) - a), forwardPosition)
val zippedPositions = forwardPosition.zip(turnPosition).zip(position)
val historyStream = zippedPositions.zip(forwardVelocity).zip(turnVelocity).zipWithTime.map {
case (((((fPos, tPos), pos), fVel), tVel), time) =>
MomentInHistory(
time = time,
forwardPosition = fPos,
forwardVelocity = fVel,
angle = tPos,
position = pos,
turnSpeed = tVel)
}
val positionListening = listenTo(position)
val velocityListening = listenTo(forwardVelocity)
val angleListening = listenTo(turnPosition)
}
class TwoSidedDriveContainerSimulator extends TwoSidedDrive { self =>
override type Hardware = SimulatedTwoSidedHardware
override type Properties = TwoSidedDriveProperties
/**
* Output the current signal to actuators with the hardware. In this case
* it simply updates the value that simulated motors will publish the next
* time that update() in an instance of SimulatedTwoSidedHardware is called
*
* @param hardware the simulated hardware to output with
* @param signal the signal to output
*/
override protected def output(hardware: Hardware, signal: TwoSidedSignal): Unit = {
hardware.leftMotor.set(signal.left)
hardware.rightMotor.set(signal.right)
}
override protected def controlMode(implicit hardware: SimulatedTwoSidedHardware,
props: TwoSidedDriveProperties): UnicycleControlMode = {
NoOperation
}
class Drivetrain(implicit hardware: Hardware,
props: Signal[Properties]) extends Component[TwoSidedSignal] {
override def defaultController: Stream[TwoSidedSignal] = self.defaultController
override def applySignal(signal: TwoSidedSignal): Unit = {
output(hardware, signal)
}
}
}
object PhysicsUtil {
/**
* motor back emf is proportional to speed
* @param velocity
* @param props
* @return
*/
private def emfForce(velocity: Velocity, maxMotorForce: Force)
(implicit props: UnicycleProperties): Force = {
val normalizedVelocity = velocity / props.maxForwardVelocity
-1 * normalizedVelocity * maxMotorForce
}
/**
* friction accounting constant friction and motor back emf
* @param velocity
* @param constantFriction
* @param props
* @return
*/
def frictionAndEMF(velocity: Velocity, constantFriction: Force, maxMotorForce: Force)
(implicit props: TwoSidedDriveProperties): Force = {
val direction = -1 * Math.signum(velocity.value)
emfForce(velocity, maxMotorForce) + direction * constantFriction
}
}