device_control 0.0.0.3 → 0.3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +54 -13
- data/VERSION +1 -1
- data/lib/device_control.rb +320 -0
- data/test/device_control.rb +445 -0
- metadata +3 -3
- data/lib/pid_controller.rb +0 -267
- data/test/pid_controller.rb +0 -283
data/lib/pid_controller.rb
DELETED
@@ -1,267 +0,0 @@
|
|
1
|
-
# There is a pattern for how both Controllers (e.g. thermostat) and Devices
|
2
|
-
# (e.g. heater) operate.
|
3
|
-
# They each have an _input_ varying over time which determines the _output_.
|
4
|
-
# A thermostat (Controller) listens for temperature and tells the heater how
|
5
|
-
# high to turn it up (or just on / off). A heater (Device) listens to its
|
6
|
-
# control knob and yields heat as an output.
|
7
|
-
#
|
8
|
-
# We capture this pattern with a single method: _update_. It accepts the
|
9
|
-
# latest input and provides an _output_ based on the input. When the input
|
10
|
-
# is read in, perhaps some internal state is changed on
|
11
|
-
# the processor which will affect the _output_.
|
12
|
-
#
|
13
|
-
# Any class which mixes in Updateable can define its own _input=_ method,
|
14
|
-
# which may update any ivars. Any such class must define an _output_ method.
|
15
|
-
#
|
16
|
-
module Updateable
|
17
|
-
def update(val)
|
18
|
-
self.input = val
|
19
|
-
self.output
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# A Device is like a heater. It has a control knob, maybe on/off or perhaps
|
24
|
-
# a variable control. Its output (maybe on/off) depends on the control knob.
|
25
|
-
class Device
|
26
|
-
include Updateable
|
27
|
-
|
28
|
-
attr_reader :knob
|
29
|
-
|
30
|
-
def initialize
|
31
|
-
@knob = 0.0
|
32
|
-
end
|
33
|
-
|
34
|
-
def input=(val)
|
35
|
-
@knob = val.to_f
|
36
|
-
end
|
37
|
-
alias_method :knob=, :input=
|
38
|
-
|
39
|
-
def output
|
40
|
-
@knob # do nothing by default
|
41
|
-
end
|
42
|
-
|
43
|
-
def to_s
|
44
|
-
format("Knob: %.3f\tOutput: %.3f", @knob, self.output)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Alright, fine, let's make a Heater
|
49
|
-
# Input is the control knob (turned far enough to on, else off)
|
50
|
-
# Output is watts
|
51
|
-
class Heater < Device
|
52
|
-
# convert electricity into thermal output
|
53
|
-
EFFICIENCY = 0.999
|
54
|
-
|
55
|
-
attr_reader :watts
|
56
|
-
|
57
|
-
def initialize(watts, threshold: 0)
|
58
|
-
super()
|
59
|
-
@watts = watts
|
60
|
-
@threshold = threshold
|
61
|
-
end
|
62
|
-
|
63
|
-
# output is all or none
|
64
|
-
def output
|
65
|
-
@knob > @threshold ? (@watts * self.class::EFFICIENCY) : 0
|
66
|
-
end
|
67
|
-
|
68
|
-
def to_s
|
69
|
-
format("Power: %d W\tKnob: %.1f\tThermal: %.1f W",
|
70
|
-
@watts, @knob, self.output)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
class Cooler < Heater
|
75
|
-
# not nearly as efficient as a heater at turning electrons into therms
|
76
|
-
EFFICIENCY = 0.35
|
77
|
-
end
|
78
|
-
|
79
|
-
# A Controller is like a thermostat. It has a setpoint, and it reads a
|
80
|
-
# measurement from the environment, and it adjusts its output to try to make
|
81
|
-
# the measurement match the setpoint.
|
82
|
-
class Controller
|
83
|
-
include Updateable
|
84
|
-
|
85
|
-
attr_reader :measure
|
86
|
-
attr_accessor :setpoint
|
87
|
-
|
88
|
-
def initialize(setpoint)
|
89
|
-
@setpoint, @measure = setpoint, 0.0
|
90
|
-
end
|
91
|
-
|
92
|
-
def input=(val)
|
93
|
-
@measure = val.to_f
|
94
|
-
end
|
95
|
-
alias_method :measure=, :input=
|
96
|
-
|
97
|
-
# just output the error
|
98
|
-
def output
|
99
|
-
@setpoint - @measure
|
100
|
-
end
|
101
|
-
|
102
|
-
def to_s
|
103
|
-
format("Setpoint: %.3f\tMeasure: %.3f", @setpoint, @measure)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
class Thermostat < Controller
|
108
|
-
# true or false; can drive a Heater or a Cooler
|
109
|
-
# true means input below setpoint; false otherwise
|
110
|
-
def output
|
111
|
-
@setpoint - @measure > 0
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
class Flexstat < Thermostat
|
116
|
-
def self.cold_val(hot_val)
|
117
|
-
case hot_val
|
118
|
-
when true, false
|
119
|
-
!hot_val
|
120
|
-
when 0,1
|
121
|
-
hot_val == 0 ? 1 : 0
|
122
|
-
when Numeric
|
123
|
-
0
|
124
|
-
when :on, :off
|
125
|
-
hot_val == :on ? :off : :on
|
126
|
-
else
|
127
|
-
raise "#{hot_val.inspect} not recognized"
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def initalize(hot_val: false, cold_val: nil)
|
132
|
-
@hot_val = hot_val
|
133
|
-
@cold_val = cold_val.nil? ? self.class.cold_val(hot_val) : cold_val
|
134
|
-
end
|
135
|
-
|
136
|
-
def output
|
137
|
-
super ? @cold_val : @hot_val
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# now consider e.g.
|
142
|
-
# h = Heater.new(1000)
|
143
|
-
# ht = Thermostat.new(20)
|
144
|
-
# c = Cooler.new(1000)
|
145
|
-
# ct = Thermostat.new(25)
|
146
|
-
# temp = 26.4
|
147
|
-
# heat_knob = ht.update(temp) ? 1 : 0
|
148
|
-
# heating_watts = h.update(heat_knob)
|
149
|
-
# cool_knob = ct.update(temp) ? 0 : 1
|
150
|
-
# cooling_watts = c.update(cool_knob)
|
151
|
-
# etc
|
152
|
-
|
153
|
-
# A StatefulController tracks its error over time: current, last, accumulated
|
154
|
-
#
|
155
|
-
class StatefulController < Controller
|
156
|
-
HZ = 1000
|
157
|
-
TICK = Rational(1) / HZ
|
158
|
-
|
159
|
-
attr_accessor :dt
|
160
|
-
attr_reader :error, :last_error, :sum_error
|
161
|
-
|
162
|
-
def initialize(setpoint, dt: TICK)
|
163
|
-
super(setpoint)
|
164
|
-
@dt = dt
|
165
|
-
@error, @last_error, @sum_error = 0.0, 0.0, 0.0
|
166
|
-
end
|
167
|
-
|
168
|
-
# update @error, @last_error, and @sum_error
|
169
|
-
def input=(val)
|
170
|
-
@measure = val
|
171
|
-
@last_error = @error
|
172
|
-
@error = @setpoint - @measure
|
173
|
-
if @error * @last_error <= 0 # zero crossing; reset the accumulated error
|
174
|
-
@sum_error = @error * @dt
|
175
|
-
else
|
176
|
-
@sum_error += @error * @dt
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
def to_s
|
181
|
-
[super,
|
182
|
-
format("Error: %+.3f\tLast: %+.3f\tSum: %+.3f",
|
183
|
-
@error, @last_error, @sum_error),
|
184
|
-
].join("\n")
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
# A PIDController is a StatefulController that calculates
|
189
|
-
# * Proportion (current error)
|
190
|
-
# * Integral (accumulated error)
|
191
|
-
# * Derivative (error slope, last_error)
|
192
|
-
# The sum of these terms is the output
|
193
|
-
#
|
194
|
-
class PIDController < StatefulController
|
195
|
-
# Ziegler-Nichols method for tuning PID gain knobs
|
196
|
-
# https://en.wikipedia.org/wiki/Ziegler%E2%80%93Nichols_method
|
197
|
-
ZN = {
|
198
|
-
# Kp Ti Td Ki Kd
|
199
|
-
# Var: Ku Tu Tu Ku/Tu Ku*Tu
|
200
|
-
'P' => [1/2r],
|
201
|
-
'PI' => [9/20r, 4/5r, nil, 27/50r],
|
202
|
-
'PD' => [ 4/5r, nil, 1/8r, nil, 1/10r],
|
203
|
-
'PID' => [ 3/5r, 1/2r, 1/8r, 6/5r, 3/40r],
|
204
|
-
'PIR' => [7/10r, 2/5r, 3/20r, 7/4r, 21/200r],
|
205
|
-
# less overshoot than standard PID
|
206
|
-
'some' => [ 1/3r, 1/2r, 1/3r, 2/3r, 1/11r],
|
207
|
-
'none' => [ 1/5r, 1/2r, 1/3r, 2/5r, 2/30r],
|
208
|
-
}
|
209
|
-
|
210
|
-
# _ku_ = ultimate gain, _tu_ = oscillation period
|
211
|
-
# output includes ti and td, which are not necessary
|
212
|
-
# typically kp, ki, and kd are used
|
213
|
-
def self.tune(type, ku, tu)
|
214
|
-
record = ZN[type.downcase] || ZN[type.upcase] || ZN.fetch(type)
|
215
|
-
kp, ti, td, ki, kd = *record
|
216
|
-
kp *= ku if kp
|
217
|
-
ti *= tu if ti
|
218
|
-
td *= tu if td
|
219
|
-
ki *= (ku / tu) if ki
|
220
|
-
kd *= (ku * tu) if kd
|
221
|
-
{ kp: kp, ti: ti, td: td, ki: ki, kd: kd }
|
222
|
-
end
|
223
|
-
|
224
|
-
attr_accessor :kp, :ki, :kd, :p_range, :i_range, :d_range, :o_range
|
225
|
-
|
226
|
-
def initialize(setpoint, dt: TICK)
|
227
|
-
super
|
228
|
-
|
229
|
-
# gain / multipliers for PID; tunables
|
230
|
-
@kp, @ki, @kd = 1.0, 1.0, 1.0
|
231
|
-
|
232
|
-
# optional clamps for PID terms and output
|
233
|
-
@p_range = (-Float::INFINITY..Float::INFINITY)
|
234
|
-
@i_range = (-Float::INFINITY..Float::INFINITY)
|
235
|
-
@d_range = (-Float::INFINITY..Float::INFINITY)
|
236
|
-
@o_range = (-Float::INFINITY..Float::INFINITY)
|
237
|
-
|
238
|
-
yield self if block_given?
|
239
|
-
end
|
240
|
-
|
241
|
-
def output
|
242
|
-
(self.proportion +
|
243
|
-
self.integral +
|
244
|
-
self.derivative).clamp(@o_range.begin, @o_range.end)
|
245
|
-
end
|
246
|
-
|
247
|
-
def proportion
|
248
|
-
(@kp * @error).clamp(@p_range.begin, @p_range.end)
|
249
|
-
end
|
250
|
-
|
251
|
-
def integral
|
252
|
-
(@ki * @sum_error).clamp(@i_range.begin, @i_range.end)
|
253
|
-
end
|
254
|
-
|
255
|
-
def derivative
|
256
|
-
(@kd * (@error - @last_error) / @dt).clamp(@d_range.begin, @d_range.end)
|
257
|
-
end
|
258
|
-
|
259
|
-
def to_s
|
260
|
-
super +
|
261
|
-
[format(" Gain:\t%.3f\t%.3f\t%.3f",
|
262
|
-
@kp, @ki, @kd),
|
263
|
-
format(" PID:\t%+.3f\t%+.3f\t%+.3f\t= %.5f",
|
264
|
-
self.proportion, self.integral, self.derivative, self.output),
|
265
|
-
].join("\n")
|
266
|
-
end
|
267
|
-
end
|
data/test/pid_controller.rb
DELETED
@@ -1,283 +0,0 @@
|
|
1
|
-
require 'pid_controller'
|
2
|
-
require 'minitest/autorun'
|
3
|
-
|
4
|
-
# create a basic class that includes Updateable as a Mixin
|
5
|
-
# the class should define #initialize, #input= and #output at minimum
|
6
|
-
class Doubler
|
7
|
-
include Updateable
|
8
|
-
|
9
|
-
attr_accessor :input
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@input = 0.0
|
13
|
-
end
|
14
|
-
|
15
|
-
def output
|
16
|
-
@input * 2
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
describe Updateable do
|
21
|
-
describe "a mixin that provides the _update_ pattern" do
|
22
|
-
before do
|
23
|
-
@o = Doubler.new
|
24
|
-
end
|
25
|
-
|
26
|
-
it "has an _update_ method that accepts an _input_ and returns _output_" do
|
27
|
-
expect(@o.input).must_equal 0.0
|
28
|
-
expect(@o.output).must_equal 0.0
|
29
|
-
|
30
|
-
output = @o.update(45)
|
31
|
-
expect(@o.input).must_equal 45
|
32
|
-
expect(@o.output).must_equal output
|
33
|
-
end
|
34
|
-
|
35
|
-
it "requires an _output_ method" do
|
36
|
-
k = Class.new(Object) do
|
37
|
-
include Updateable
|
38
|
-
end
|
39
|
-
o = k.new
|
40
|
-
expect { o.update(45) }.must_raise NoMethodError
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
describe Device do
|
46
|
-
before do
|
47
|
-
@device = Device.new
|
48
|
-
end
|
49
|
-
|
50
|
-
it "has an _output_" do
|
51
|
-
expect(@device.output).must_be_kind_of Float
|
52
|
-
end
|
53
|
-
|
54
|
-
it "has a string representation" do
|
55
|
-
expect(@device.to_s).must_be_kind_of String
|
56
|
-
end
|
57
|
-
|
58
|
-
it "has an _update_ method from Updateable" do
|
59
|
-
expect(@device.update(2.34)).must_be_kind_of Float
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
describe Heater do
|
64
|
-
before do
|
65
|
-
@h = Heater.new(1000)
|
66
|
-
end
|
67
|
-
|
68
|
-
it "has an _output_ when _knob_ is greater than zero" do
|
69
|
-
expect(@h.knob).must_equal 0
|
70
|
-
expect(@h.output).must_equal 0
|
71
|
-
@h.knob = 1
|
72
|
-
expect(@h.output).must_be :>, 0
|
73
|
-
end
|
74
|
-
|
75
|
-
it "has a string representation" do
|
76
|
-
expect(@h.to_s).must_be_kind_of String
|
77
|
-
end
|
78
|
-
|
79
|
-
it "has _update_ from Updateable" do
|
80
|
-
expect(@h.knob).must_equal 0
|
81
|
-
expect(@h.output).must_equal 0
|
82
|
-
output = @h.update(1)
|
83
|
-
expect(output).must_be :>, 0
|
84
|
-
expect(@h.knob).must_equal 1
|
85
|
-
expect(@h.output).must_equal output
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
describe Controller do
|
90
|
-
before do
|
91
|
-
@sp = 500
|
92
|
-
@c = Controller.new(@sp)
|
93
|
-
end
|
94
|
-
|
95
|
-
it "has an _output_, the difference between setpoint and measure" do
|
96
|
-
expect(@c.output).must_be_kind_of Float
|
97
|
-
expect(@c.output).must_equal @sp
|
98
|
-
end
|
99
|
-
|
100
|
-
it "has a string representation" do
|
101
|
-
expect(@c.to_s).must_be_kind_of String
|
102
|
-
end
|
103
|
-
|
104
|
-
it "has an _update_ method from Updateable" do
|
105
|
-
expect(@c.update(499)).must_equal 1.0
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
describe Thermostat do
|
110
|
-
before do
|
111
|
-
@t = Thermostat.new 25
|
112
|
-
end
|
113
|
-
|
114
|
-
it "outputs true when it's too cold; when measure < setpoint" do
|
115
|
-
expect(@t.update 20).must_equal true
|
116
|
-
expect(@t.update 30).must_equal false
|
117
|
-
end
|
118
|
-
|
119
|
-
it "outputs false when it's too hot; when measure > setpoint" do
|
120
|
-
expect(@t.update 30).must_equal false
|
121
|
-
expect(@t.update 20).must_equal true
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
describe StatefulController do
|
126
|
-
it "tracks error, last_error, sum_error" do
|
127
|
-
sc = StatefulController.new(100)
|
128
|
-
expect(sc.error).must_equal 0.0
|
129
|
-
expect(sc.last_error).must_equal 0.0
|
130
|
-
expect(sc.sum_error).must_equal 0.0
|
131
|
-
|
132
|
-
output = sc.update 50
|
133
|
-
expect(sc.output).must_equal output
|
134
|
-
expect(sc.measure).must_equal 50
|
135
|
-
expect(sc.error).must_be_within_epsilon 50.0
|
136
|
-
expect(sc.last_error).must_equal 0.0
|
137
|
-
expect(sc.sum_error).must_be_within_epsilon(50.0 * sc.dt)
|
138
|
-
|
139
|
-
output = sc.update 75
|
140
|
-
expect(sc.output).must_equal output
|
141
|
-
expect(sc.measure).must_equal 75
|
142
|
-
expect(sc.error).must_be_within_epsilon 25.0
|
143
|
-
expect(sc.last_error).must_be_within_epsilon 50.0
|
144
|
-
expect(sc.sum_error).must_be_within_epsilon(75.0 * sc.dt)
|
145
|
-
end
|
146
|
-
|
147
|
-
it "resets sum_error after crossing setpoint" do
|
148
|
-
sc = StatefulController.new(100)
|
149
|
-
sc.update 50
|
150
|
-
sc.update 75
|
151
|
-
expect(sc.sum_error).must_be_within_epsilon(75.0 * sc.dt)
|
152
|
-
sc.update 125
|
153
|
-
expect(sc.error).must_equal(-25.0)
|
154
|
-
expect(sc.sum_error).must_equal(sc.error * sc.dt)
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
describe PIDController do
|
159
|
-
it "informs Ziegler-Nichols tuning" do
|
160
|
-
# P only, not PID
|
161
|
-
hsh = PIDController.tune('P', 5, 0.01)
|
162
|
-
expect(hsh[:kp]).must_be :>, 0
|
163
|
-
expect(hsh[:ki]).must_be_nil
|
164
|
-
expect(hsh[:kd]).must_be_nil
|
165
|
-
expect(hsh[:ti]).must_be_nil
|
166
|
-
expect(hsh[:td]).must_be_nil
|
167
|
-
|
168
|
-
hsh = PIDController.tune('PI', 5, 0.01)
|
169
|
-
expect(hsh[:kp]).must_be :>, 0
|
170
|
-
expect(hsh[:ki]).must_be :>, 0
|
171
|
-
expect(hsh[:kd]).must_be_nil
|
172
|
-
expect(hsh[:ti]).must_be :>, 0
|
173
|
-
expect(hsh[:td]).must_be_nil
|
174
|
-
|
175
|
-
hsh = PIDController.tune('PID', 5, 0.01)
|
176
|
-
expect(hsh[:kp]).must_be :>, 0
|
177
|
-
expect(hsh[:ki]).must_be :>, 0
|
178
|
-
expect(hsh[:kd]).must_be :>, 0
|
179
|
-
expect(hsh[:ti]).must_be :>, 0
|
180
|
-
expect(hsh[:td]).must_be :>, 0
|
181
|
-
end
|
182
|
-
|
183
|
-
it "has an optional _dt_ argument to initialize" do
|
184
|
-
pid = PIDController.new(1000, dt: 0.1)
|
185
|
-
expect(pid).must_be_kind_of PIDController
|
186
|
-
expect(pid.setpoint).must_equal 1000
|
187
|
-
expect(pid.dt).must_equal 0.1
|
188
|
-
end
|
189
|
-
|
190
|
-
it "has PID gain settings" do
|
191
|
-
pid = PIDController.new(1000)
|
192
|
-
expect(pid.kp).must_be :>, 0
|
193
|
-
pid.kp = 1000
|
194
|
-
expect(pid.kp).must_equal 1000
|
195
|
-
pid.ki = 1000
|
196
|
-
expect(pid.ki).must_equal 1000
|
197
|
-
pid.kd = 1000
|
198
|
-
expect(pid.kd).must_equal 1000
|
199
|
-
end
|
200
|
-
|
201
|
-
it "clamps the _proportion_ term" do
|
202
|
-
pid = PIDController.new(1000)
|
203
|
-
pid.p_range = (0..1)
|
204
|
-
pid.update(500)
|
205
|
-
expect(pid.proportion).must_equal 1.0
|
206
|
-
pid.update(1500)
|
207
|
-
expect(pid.proportion).must_equal 0.0
|
208
|
-
end
|
209
|
-
|
210
|
-
it "clamps the _integral_ term" do
|
211
|
-
pid = PIDController.new(1000)
|
212
|
-
pid.i_range = (-1.0 .. 1.0)
|
213
|
-
pid.setpoint = 10_000
|
214
|
-
pid.update(500)
|
215
|
-
expect(pid.integral).must_equal 1.0
|
216
|
-
pid.update(10_001)
|
217
|
-
pid.update(20_000)
|
218
|
-
expect(pid.integral).must_equal(-1.0)
|
219
|
-
end
|
220
|
-
|
221
|
-
it "clamps the _derivative_ term" do
|
222
|
-
pid = PIDController.new(1000)
|
223
|
-
pid.d_range = (-1.0 .. 0.0)
|
224
|
-
pid.update(0)
|
225
|
-
pid.update(10)
|
226
|
-
expect(pid.derivative).must_equal(-1.0)
|
227
|
-
pid.update(990)
|
228
|
-
expect(pid.derivative).must_equal(-1.0)
|
229
|
-
pid.update(1000)
|
230
|
-
pid.update(990)
|
231
|
-
expect(pid.derivative).must_equal(0.0)
|
232
|
-
end
|
233
|
-
|
234
|
-
it "clamps the _output_" do
|
235
|
-
pid = PIDController.new(1000)
|
236
|
-
pid.o_range = (0.0 .. 1.0)
|
237
|
-
pid.update(0)
|
238
|
-
expect(pid.output).must_equal(1.0)
|
239
|
-
pid.update(2000)
|
240
|
-
expect(pid.output).must_equal(0.0)
|
241
|
-
end
|
242
|
-
|
243
|
-
it "calculates _proportion_ based on current error" do
|
244
|
-
pid = PIDController.new(1000)
|
245
|
-
pid.kp = 1.0
|
246
|
-
pid.update(0)
|
247
|
-
expect(pid.proportion).must_equal 1000.0
|
248
|
-
pid.update(1)
|
249
|
-
expect(pid.proportion).must_equal 999.0
|
250
|
-
pid.update(1001)
|
251
|
-
expect(pid.proportion).must_equal(-1.0)
|
252
|
-
end
|
253
|
-
|
254
|
-
it "calculates _integral_ based on accumulated error" do
|
255
|
-
pid = PIDController.new(1000)
|
256
|
-
pid.ki = 1.0
|
257
|
-
pid.update(0)
|
258
|
-
# sum error should be 1000; dt is 0.001
|
259
|
-
expect(pid.integral).must_equal(1.0)
|
260
|
-
pid.update(999)
|
261
|
-
expect(pid.integral).must_be_within_epsilon(1.001)
|
262
|
-
pid.update(1100) # zero crossing
|
263
|
-
expect(pid.integral).must_be_within_epsilon(-0.1)
|
264
|
-
end
|
265
|
-
|
266
|
-
it "calculates _derivative_ based on error slope" do
|
267
|
-
pid = PIDController.new(1000)
|
268
|
-
pid.kp = 1.0
|
269
|
-
pid.update(0)
|
270
|
-
# error should be 1000; last_error 0
|
271
|
-
expect(pid.derivative).must_equal(1_000_000)
|
272
|
-
pid.update(500)
|
273
|
-
expect(pid.derivative).must_equal(-500_000)
|
274
|
-
pid.update(999)
|
275
|
-
expect(pid.derivative).must_equal(-499_000)
|
276
|
-
pid.update(1001)
|
277
|
-
expect(pid.derivative).must_equal(-2000)
|
278
|
-
pid.update(1100)
|
279
|
-
expect(pid.derivative).must_equal(-99_000)
|
280
|
-
pid.update(900)
|
281
|
-
expect(pid.derivative).must_equal(200_000)
|
282
|
-
end
|
283
|
-
end
|