-
Notifications
You must be signed in to change notification settings - Fork 35
/
cali.lua
272 lines (227 loc) · 8.56 KB
/
cali.lua
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
--- cali
-- utility for calibrating crow's CV i/o
--
-- keep druid open while running!
--
-- when the script starts, it will run the calibration automatically
-- input[1] and all outputs will be calibrated automatically
-- then you'll be prompted to add a patch cable
-- connect output[1] to input[2]
-- now the calibration will complete
-- if ALWAYS_TEST is true, you'll be prompted to remove the patch cable
-- test results will appear gradually
--
-- re-run the calibration with: calibrate()
-- run the test-suite with: test()
-- print the raw offsets & scalars with: raw()
-- settings
local AUTOSTART = true -- if false, you can calibrate by typing: calibrate()
local ALWAYS_TEST = true -- if true, the test() function will autorun after calibrate()
-- debug: use when working on this script
local AUTOSAVE = true -- set false while debugging to reduce flash write cycles
local ALWAYS_RAW = false -- set true to print raw offset & scale values after calibrate()
local ITERATIONS = 1 -- set higher than 1 to show that you have found the *correct* solution
--- these functions are generally unused
-- values seem to be accurate enough by simply measuring & calculating directly
-- no need to measure, update, re-measure (etc)
function lerp(from, to, coeff)
return from + coeff*(to - from)
end
-- diff_multiplier is 100 ~ 2000 or so. sets convergence rate / sensitivity
function adaptive_filter(past, new, diff_multiplier)
local coeff = math.min(1, diff_multiplier * math.abs(past - new))
local dest = lerp(past, new, coeff)
return dest
end
-- specifically tuned for minimal standard deviation across multiple calls
-- see: util/adda-measurement.lua for methodology
-- settling time increased for out[3]. limit ~12ms. 20ms for safety
function read_avg(chan)
local reps = 256
local sum = 0
clock.sleep(0.02) -- input & output settling time
for i=1,reps do
sum = sum + input[chan].volts -- accumulate readings
clock.sleep(0.001)
end
return sum / reps -- average
end
function xvolts(ch, v)
for n=1,4 do
output[n].volts = n==ch and v or 0
end
end
function sample(t)
if t.volts then xvolts(t.source, t.volts) end -- set source voltage exclusively
cal.source(t.source) -- select source
return read_avg(t.input)
end
function input_scale(n)
local gnd, vref
if n==1 then
gnd = sample{input=n, source='gnd'}
vref = sample{input=n, source='2v5'}
else -- n==2
gnd = sample{input=n, source=1, volts=0}
vref = sample{input=n, source=1, volts=2.5}
end
local past = cal.input[n].scale -- capture past scalar
local new = 2.5/(vref-gnd) -- determine new scalar
new = new * past -- account for past scalar
-- move toward solution
cal.input[n].scale = adaptive_filter(past, new, 1000)
print(string.format('input[%i].scale = % 6.5f (% 6.5f, % 6.5f)',n,cal.input[n].scale,gnd,vref))
end
function input_offset(n)
local gnd
if n==1 then
gnd = sample{input=n, source='gnd'}
else -- n==2
gnd = sample{input=n, source=1, volts=0}
end
local past = cal.input[n].offset -- capture past offset
local new = gnd -- determine new offset
new = past - new -- account for past offset
-- move toward solution
cal.input[n].offset = adaptive_filter(past, new, 300)
print(string.format('input[%i].offset = % 6.5f (% 6.5f)',n,cal.input[n].offset,gnd))
end
function output_scale(n)
local gnd = sample{input=1, source=n, volts=0}
local vref = sample{input=1, source=n, volts=2.5}
local past = cal.output[n].scale -- capture past scalar
local new = 2.5/(vref-gnd) -- determine new scalar
new = new * past -- account for past scalar
-- move toward solution
cal.output[n].scale = adaptive_filter(past, new, 1000)
print(string.format('output[%i].scale = % 6.5f (% 6.5f, % 6.5f)',n,cal.output[n].scale,gnd,vref))
end
function output_offset(n)
local gnd = sample{input=1, source=n, volts=0}
local past = cal.output[n].offset -- capture past offset
local new = gnd -- determine new offset
new = new / cal.output[n].scale -- account for scale being applied already
new = past - new -- account for past offset
-- move toward solution
cal.output[n].offset = adaptive_filter(past, new, 300)
print(string.format('output[%i].offset = % 6.5f (% 6.5f)',n,cal.output[n].offset,gnd))
end
function await_input(n)
if sample{input=n, source=1, volts=-3} > -2 then
if n == 1 then print '\ndisconnect all patch cables'
else print '\nconnect output[1] -> input[2]' end
while sample{input=n, source=1, volts=-3} > -2 do
tell('stream',n,input[n].volts)
clock.sleep(0.1)
end
print'ok!'
clock.sleep(1) -- wait 1 second to ensure both ends of cable are disconnected
end
end
function clear_cal_settings()
for n=1,2 do
cal.input[n].offset = 0
cal.input[n].scale = 1.0
end
for n=1,4 do
cal.output[n].offset = 0
cal.output[n].scale = 1.0
end
end
function find_error_in_cents(result, expected)
return math.floor(0.5 + 1200*(result - expected))
end
function test()
clock.run(function()
-- ensure there's no cable remaining from calibration
await_input(1)
local pass = true -- assume true unless we see an error
local failmsgs = {}
-- 30, 3, 5, 12, 30
print'\nvoltage listed to the nearest millivolt'
print'error amounts listed in cents beneath the readings'
print' expect: -2.5 0.0 2.5 5.0 7.5\n'
-- input 1
local gnd = sample{input=1, source='gnd'}
local vref = sample{input=1, source='2v5'}
local gnd_err = find_error_in_cents(gnd, 0)
local vref_err = find_error_in_cents(vref, 2.5)
print(string.format('input[1] % 01.3f % 01.3f',gnd, vref))
print(string.format(' % 7d % 7d',gnd_err,vref_err))
if math.abs(gnd_err) > 3 then pass = false end
if math.abs(vref_err) > 3 then pass = false end
-- input 2
gnd = sample{input=2, source='gnd'}
gnd_err = find_error_in_cents(gnd, 0)
print(string.format('input[2] % 01.3f', gnd))
print(string.format(' % 7d',gnd_err))
if math.abs(gnd_err) > 3 then pass = false end
for n=1,4 do
local V = {-2.5, 0, 2.5, 5, 7.5} -- test voltages
local E = { 30, 3, 5, 12, 30} -- allowable cents error at each voltage
local t = {} -- test results
local e = {} -- error calculation in cents
for k,v in ipairs(V) do
t[k] = sample{input=1, source=n, volts = v}
e[k] = find_error_in_cents(t[k], v)
if math.abs(e[k]) > E[k] then
pass = false
table.insert(failmsgs, 'output['..n..'] @'..v..'V out of range')
end
end
print(string.format('output[%i] % 01.3f % 01.3f % 01.3f % 01.3f % 01.3f',n
,t[1],t[2],t[3],t[4],t[5]))
print(string.format(' % 7d % 7d % 7d % 7d % 7d'
,e[1],e[2],e[3],e[4],e[5]))
end
if pass then
print('\n---- PASS ----')
else
print('\n!!!! FAIL !!!!')
print(table.concat(failmsgs, ", "))
end
end)
end
function raw()
local function pprintset(key)
print(key..'s: ')
for k,v in ipairs(cal[key]) do
print(string.format(' %i offset: % 01.4f, scale:% 01.4f', k, v.offset, v.scale))
end
end
print' offset should be near 0'
print' scale should be near 1'
pprintset'input'
pprintset'output'
end
function calibrate()
clock.run( function()
clear_cal_settings() -- clear everything before we start
-- input 1
await_input(1)
for i=1,ITERATIONS do
input_scale(1)
input_offset(1)
end
-- outputs
for i=1,ITERATIONS do
for n=1,4 do
output_scale(n)
output_offset(n)
end
end
-- input 2
await_input(2)
for i=1,ITERATIONS do
input_scale(2)
input_offset(2)
end
if AUTOSAVE then cal.save() end
if ALWAYS_RAW then raw() end
if ALWAYS_TEST then test() end
end)
end
function init()
print('///////////////////////////')
if AUTOSTART then calibrate() end
end