SHOW:
|
|
- or go back to the newest paste.
1 | -- | |
2 | -- Control turbines of a active cooled Big Reactor (http://big-reactors.com/). | |
3 | -- | |
4 | -- Author: kla_sch | |
5 | -- | |
6 | -- History: | |
7 | -- v0.1, 2014-12-29: | |
8 | -- - first version | |
9 | -- | |
10 | -- v0.2, 2015-01-02: | |
11 | -- - Big Reactor since 0.3.4A: Feature to disengage coil to | |
12 | -- startup faster (see config value useDisengageCoils). | |
13 | -- - minor bugfixes | |
14 | -- - some code improvments (thanks to wieselkatze) | |
15 | -- | |
16 | -- Save as "startup" | |
17 | ||
18 | ||
19 | -- | |
20 | -- Constant values (configuration) | |
21 | -- =============================== | |
22 | ||
23 | ||
24 | -- Maximum loop time of controller (default: 0.5s) | |
25 | local loopT=0.5 | |
26 | ||
27 | -- Display loop time, of controller has been switched off (default: 1s). | |
28 | local displayT=1 | |
29 | ||
30 | -- Modem channel to listen for status requests. If it is set to 0, | |
31 | -- the remote status requests are disabled. | |
32 | -- The sender sould simply send "BR_turbine_get_state" to this channel. The | |
33 | -- turbine replies with status informations to the replay channel. | |
34 | local stateRequestChannel = 32768 -- Broadcast channel. | |
35 | ||
36 | -- | |
37 | -- Enable remote control. | |
38 | -- Set to "true" if you want to enable this feature. | |
39 | -- | |
40 | local remoteControl = false | |
41 | ||
42 | -- | |
43 | -- Use disengaged coil for faster speedup. (Big Reactor >= 0.3.4A). | |
44 | -- Set this to "false" if you want to disable this feature. | |
45 | -- | |
46 | local useDisengageCoils = true | |
47 | ||
48 | ||
49 | -- | |
50 | -- Internal values: | |
51 | -- ================ | |
52 | ||
53 | -- File to save last known controller state: | |
54 | local saveFilename = "turbine_crtl.save" | |
55 | ||
56 | local Kp, Kd, Ki, errSum, errOld -- global PID controller values | |
57 | local tSpeed -- target speed | |
58 | local turbine -- wraped turbine | |
59 | local maxFRate -- maximum float rate of turbine | |
60 | local floatRateChanged = false -- flag: true on float rate change | |
61 | ||
62 | ||
63 | -- | |
64 | -- Find the first connected big turbine and return the wraped handle. | |
65 | -- | |
66 | -- If no turbine was found this function terminate the program. | |
67 | -- | |
68 | -- Return: | |
69 | -- Handle of first connected turbine found. | |
70 | -- | |
71 | local function getTurbineHandle() | |
72 | local pList = peripheral.getNames() | |
73 | local i, name | |
74 | for i, name in pairs(pList) do | |
75 | if peripheral.getType(name) == "BigReactors-Turbine" then | |
76 | return peripheral.wrap(name) | |
77 | end | |
78 | end | |
79 | ||
80 | error("No turbine connected: Exit program") | |
81 | end | |
82 | ||
83 | ||
84 | -- | |
85 | -- Search for any modem and open it to recieve modem requests. | |
86 | -- | |
87 | local function searchAndOpenModems() | |
88 | if stateRequestChannel <= 0 then | |
89 | return -- feature disabled | |
90 | end | |
91 | ||
92 | local pList = peripheral.getNames() | |
93 | local i, name | |
94 | for i, name in pairs(pList) do | |
95 | if peripheral.getType(name) == "modem" then | |
96 | peripheral.call(name, "open", stateRequestChannel) | |
97 | peripheral.call(name, "open", os.getComputerID()) | |
98 | end | |
99 | end | |
100 | end | |
101 | ||
102 | ||
103 | -- | |
104 | -- Saves current controller state | |
105 | -- | |
106 | local function saveControllerState() | |
107 | local tmpFilename = saveFilename .. ".tmp" | |
108 | ||
109 | fs.delete(tmpFilename) | |
110 | local sFile = fs.open(tmpFilename, "w") | |
111 | if sFile == nil then | |
112 | error("cannot open status file for writing.") | |
113 | end | |
114 | ||
115 | sFile.writeLine("V0.1") | |
116 | sFile.writeLine(tostring(tSpeed)) | |
117 | sFile.writeLine(tostring(loopT)) | |
118 | sFile.writeLine(tostring(Kp)) | |
119 | sFile.writeLine(tostring(errSum)) | |
120 | sFile.writeLine(tostring(errOld)) | |
121 | sFile.writeLine("EOF") | |
122 | sFile.close() | |
123 | ||
124 | fs.delete(saveFilename) | |
125 | fs.move(tmpFilename, saveFilename) | |
126 | end | |
127 | ||
128 | ||
129 | -- | |
130 | -- Initialize basic PID controller values | |
131 | -- | |
132 | local function initControllerValues() | |
133 | local Tn = loopT | |
134 | local Tv = loopT * 10 | |
135 | ||
136 | Ki = Kp / Tn | |
137 | Kd = Kp * Tv | |
138 | end | |
139 | ||
140 | ||
141 | -- | |
142 | -- Read number from file | |
143 | -- | |
144 | -- Parameters: | |
145 | -- sFile - opened file to read from | |
146 | -- | |
147 | -- Return | |
148 | -- the number of nil, if an error has occurred | |
149 | -- | |
150 | local function readNumber(sFile) | |
151 | local s = sFile.readLine() | |
152 | if s == nil then | |
153 | return nil | |
154 | end | |
155 | return tonumber(s) | |
156 | end | |
157 | ||
158 | -- | |
159 | -- Restore last known controller state | |
160 | -- | |
161 | -- Returns: | |
162 | -- true, if the last saved state has successfully readed. | |
163 | local function restoreControllerState() | |
164 | local tmpFilename = saveFilename .. ".tmp" | |
165 | local sFile = fs.open(saveFilename, "r") | |
166 | if sFile == nil and fs.exists(tmpFilename) then | |
167 | fs.move(tmpFilename, saveFilename) | |
168 | sFile = fs.open(saveFilename) | |
169 | end | |
170 | ||
171 | if sFile == nil then | |
172 | return false -- cannot read any file | |
173 | end | |
174 | ||
175 | local version = sFile.readLine() | |
176 | if version == nil then | |
177 | sFile.close() | |
178 | return false -- empty file | |
179 | end | |
180 | if version ~= "V0.1" then | |
181 | sFile.close() | |
182 | return false -- unknown version | |
183 | end | |
184 | ||
185 | local tSpeedNum = readNumber(sFile) | |
186 | if tSpeedNum == nil then | |
187 | sFile.close() | |
188 | return false -- cannot read target speed | |
189 | end | |
190 | ||
191 | local loopTNum = readNumber(sFile) | |
192 | if loopTNum == nil then | |
193 | sFile.close() | |
194 | return false -- cannot read loop speed | |
195 | end | |
196 | ||
197 | local KpNum = readNumber(sFile) | |
198 | if KpNum == nil then | |
199 | sFile.close() | |
200 | return false -- cannot read Kp | |
201 | end | |
202 | ||
203 | local errSumNum = readNumber(sFile) | |
204 | if errSumNum == nil then | |
205 | sFile.close() | |
206 | return false -- cannot read error sum | |
207 | end | |
208 | ||
209 | local errOldNum = readNumber(sFile) | |
210 | if errOldNum == nil then | |
211 | sFile.close() | |
212 | return false -- cannot read last error | |
213 | end | |
214 | ||
215 | local eofStr = sFile.readLine() | |
216 | if eofStr == nil or eofStr ~= "EOF" then | |
217 | sFile.close() | |
218 | return false -- EOF marker not found. File corrupted? | |
219 | end | |
220 | ||
221 | sFile.close() | |
222 | ||
223 | -- Restore saved values | |
224 | tSpeed = tSpeedNum | |
225 | loopT = loopTNum | |
226 | Kp = KpNum | |
227 | errSum = errSumNum | |
228 | errOld = errOldNum | |
229 | ||
230 | initControllerValues() | |
231 | ||
232 | if tSpeed == 0 then | |
233 | turbine.setActive(false) | |
234 | else | |
235 | turbine.setActive(true) | |
236 | end | |
237 | ||
238 | return true | |
239 | end | |
240 | ||
241 | ||
242 | -- | |
243 | -- Write text with colors, if possible (advance monitor) | |
244 | -- | |
245 | -- Parameters: | |
246 | -- mon - handle of monitor | |
247 | -- color - text color | |
248 | -- text - text to write | |
249 | -- | |
250 | local function writeColor(mon, color, text) | |
251 | if mon.isColor() then | |
252 | mon.setTextColor(color) | |
253 | end | |
254 | mon.write(text) | |
255 | if mon.isColor() then | |
256 | mon.setTextColor(colors.white) | |
257 | end | |
258 | end | |
259 | ||
260 | ||
261 | -- | |
262 | -- Scale the monitor text size to needed size of output text. | |
263 | -- | |
264 | -- This function try to scale the monitor text size, so that it is enoth for | |
265 | -- "optHeight" lines with "optWidth" characters. If it is not possible | |
266 | -- (text scale is not supported or the connected monitor is too small), | |
267 | -- it also accept "minHeight" lines and "minWidth" characters. | |
268 | -- | |
269 | -- Parameters: | |
270 | -- mon - handle of monitor. | |
271 | -- minWidth - Minimum number of columns needed. | |
272 | -- optWidth - Optimal number of columns desired. | |
273 | -- minHeight - Minimum number of rows needed. | |
274 | -- optHeight - Optimal number of rows desired. | |
275 | -- | |
276 | -- Return: | |
277 | -- Size of monitor after scaling: width, height. | |
278 | -- If the monitor is too small, it returns nul,nil. | |
279 | -- | |
280 | local function scaleMonitor(mon, minWidth, optWidth, minHeight, optHeight) | |
281 | if mon.setTextScale ~= nil then | |
282 | mon.setTextScale(1) | |
283 | end | |
284 | ||
285 | local width, height = mon.getSize() | |
286 | ||
287 | if mon.setTextScale == nil then | |
288 | -- Scale not available | |
289 | if width < minWidth or height < minHeight then | |
290 | return nil, nil -- too small | |
291 | else | |
292 | return width, height | |
293 | end | |
294 | end | |
295 | ||
296 | if width < optWidth or height < optHeight then | |
297 | -- too small: try to scale down. | |
298 | mon.setTextScale(0.5) | |
299 | ||
300 | width, height = mon.getSize() | |
301 | if width < minWidth or height < minHeight then | |
302 | return nil, nil -- still too small | |
303 | end | |
304 | else | |
305 | -- Huge monitors? Try to scale up, if possible (max scale=5). | |
306 | local scale = math.min(width / optWidth, height / optHeight, 5) | |
307 | scale = math.floor(scale * 2) / 2 -- multiple of 0.5 | |
308 | ||
309 | if scale > 1 then | |
310 | mon.setTextScale(scale) | |
311 | width, height = mon.getSize() | |
312 | end | |
313 | end | |
314 | ||
315 | return width, height | |
316 | end | |
317 | ||
318 | ||
319 | -- Display turbine status to a monitor | |
320 | -- | |
321 | -- Parameters: | |
322 | -- mon - Wraped handle of monitor | |
323 | -- turbine - Wraped handle of turbine. | |
324 | -- tSpeed - Target speed. | |
325 | -- | |
326 | local function displayStateOnMonitor(mon, turbine, tSpeed) | |
327 | ||
328 | -- scale it, if possible. | |
329 | local width, height = scaleMonitor(mon, 15, 16, 5, 5) | |
330 | ||
331 | if width == nil or height == nil then | |
332 | return -- Montitor is too small | |
333 | end | |
334 | ||
335 | mon.clear() | |
336 | ||
337 | mon.setCursorPos(1,1) | |
338 | mon.write("Turbine: ") | |
339 | if tSpeed == 0 then | |
340 | writeColor(mon, colors.red, "off") | |
341 | else | |
342 | writeColor(mon, colors.green, string.format("%d", tSpeed)) | |
343 | if width > 16 then | |
344 | mon.write(" RPM") | |
345 | end | |
346 | end | |
347 | ||
348 | mon.setCursorPos(1,3) | |
349 | local speed = math.floor(turbine.getRotorSpeed()*10+0.5)/10 | |
350 | mon.write("Speed: ") | |
351 | if (speed == tSpeed) then | |
352 | writeColor(mon, colors.green, speed) | |
353 | else | |
354 | writeColor(mon, colors.orange, speed) | |
355 | end | |
356 | if width > 16 then | |
357 | mon.write(" RPM") | |
358 | end | |
359 | ||
360 | local maxFlow = turbine.getFluidFlowRateMax() | |
361 | local actFlow = turbine.getFluidFlowRate() | |
362 | if width >= 16 then | |
363 | -- bigger monitor | |
364 | mon.setCursorPos(1,4) | |
365 | mon.write("MFlow: " .. string.format("%d", maxFlow) .. " mB/t") | |
366 | ||
367 | mon.setCursorPos(1,5) | |
368 | mon.write("AFlow: ") | |
369 | if actFlow < maxFlow then | |
370 | writeColor(mon, colors.red, string.format("%d", actFlow)) | |
371 | else | |
372 | writeColor(mon, colors.green, string.format("%d", actFlow)) | |
373 | end | |
374 | mon.write(" mB/t") | |
375 | else | |
376 | -- 1x1 monitor | |
377 | mon.setCursorPos(1,4) | |
378 | mon.write("Flow (act/max):") | |
379 | mon.setCursorPos(1,5) | |
380 | mon.write("(") | |
381 | ||
382 | if actFlow < maxFlow then | |
383 | writeColor(mon, colors.red, string.format("%d",actFlow)) | |
384 | else | |
385 | writeColor(mon, colors.green, string.format("%d",actFlow)) | |
386 | end | |
387 | ||
388 | mon.write("/") | |
389 | mon.write(string.format("%d", maxFlow)) | |
390 | mon.write(" mB/t)") | |
391 | end | |
392 | ||
393 | end | |
394 | ||
395 | ||
396 | -- Display turbine status to any connected monitor and also to console. | |
397 | -- | |
398 | -- Parameters: | |
399 | -- turbine - Wraped handle of turbine. | |
400 | -- tSpeed - Target speed. | |
401 | -- | |
402 | function displayState(turbine, tSpeed) | |
403 | displayStateOnMonitor(term, turbine, tSpeed) | |
404 | term.setCursorPos(1,7) | |
405 | term.write("* Keys: [o]ff, [m]edium (900), [f]ast (1800)") | |
406 | term.setCursorPos(1,8) | |
407 | ||
408 | local pList = peripheral.getNames() | |
409 | local i, name | |
410 | for i, name in pairs(pList) do | |
411 | if peripheral.getType(name) == "monitor" then | |
412 | -- found monitor as peripheral | |
413 | displayStateOnMonitor(peripheral.wrap(name), turbine, tSpeed) | |
414 | end | |
415 | end | |
416 | end | |
417 | ||
418 | -- | |
419 | -- Test the speedup time of the turbine. | |
420 | -- | |
421 | -- Parameters: | |
422 | -- turbine - Wraped handle of turbine. | |
423 | -- loopT - Loop timer. | |
424 | -- tSpeed - Target speed | |
425 | local function testSpeedup(turbine, loopT, tSpeed) | |
426 | turbine.setFluidFlowRateMax(maxFRate) | |
427 | ||
428 | if turbine.setInductorEngaged then | |
429 | -- always engage coil | |
430 | turbine.setInductorEngaged(true) | |
431 | end | |
432 | ||
433 | local KpSum=0 | |
434 | local nKp=0 | |
435 | local oldSpeed=-1 | |
436 | ||
437 | for i=0,5 do | |
438 | displayState(turbine, tSpeed) | |
439 | speed = turbine.getRotorSpeed() | |
440 | if oldSpeed >= 0 then | |
441 | KpSum = KpSum + (speed-oldSpeed) | |
442 | nKp = nKp + 1 | |
443 | end | |
444 | oldSpeed = speed | |
445 | sleep(loopT) | |
446 | end | |
447 | ||
448 | if KpSum * loopT / nKp > 5 then | |
449 | -- Too fast: increase loop speed | |
450 | loopT = 5 * nKp / KpSum | |
451 | return 5, loopT | |
452 | else | |
453 | return (KpSum / nKp), loopT | |
454 | end | |
455 | end | |
456 | ||
457 | ||
458 | -- | |
459 | -- Main program | |
460 | -- | |
461 | ||
462 | sleep(2) -- wait for 2s | |
463 | ||
464 | -- wrap turbine | |
465 | turbine = getTurbineHandle() | |
466 | ||
467 | searchAndOpenModems() -- search and open any modem. | |
468 | ||
469 | if restoreControllerState() == false then | |
470 | -- restore of old values failed. | |
471 | tSpeed = 0 | |
472 | Kp=0 | |
473 | end | |
474 | ||
475 | ||
476 | maxFRate = turbine.getFluidFlowRateMaxMax() | |
477 | while true do | |
478 | displayState(turbine, tSpeed) | |
479 | ||
480 | if tSpeed ~= 0 then | |
481 | -- calculate PID controller | |
482 | local speed = turbine.getRotorSpeed() | |
483 | ||
484 | err = tSpeed - speed | |
485 | local errSumOld = errSum | |
486 | errSum = errSum + err | |
487 | ||
488 | if useDisengageCoils | |
489 | and floatRateChanged | |
490 | and err > 0 | |
491 | and turbine.setInductorEngaged | |
492 | then | |
493 | -- Turbine startup: disengage coils | |
494 | turbine.setInductorEngaged(false) | |
495 | end | |
496 | ||
497 | if turbine.setInductorEngaged and err < 0 then | |
498 | -- speed too fast: engage coils | |
499 | turbine.setInductorEngaged(true) | |
500 | end | |
501 | ||
502 | local p = Kp * err | |
503 | local i = Ki * loopT * errSum | |
504 | local d = Kd * (err - errOld) * loopT | |
505 | ||
506 | if i < 0 or i > maxFRate then | |
507 | errSum=errSumOld -- error too heavy => reset to old value. | |
508 | i = Ki * loopT * errSum | |
509 | end | |
510 | ||
511 | local fRate = p + i + d | |
512 | errOld = err | |
513 | ||
514 | ||
515 | -- cut extreme flow rates. | |
516 | if fRate < 0 then | |
517 | fRate = 0 | |
518 | elseif fRate > maxFRate then | |
519 | fRate = maxFRate | |
520 | end | |
521 | ||
522 | turbine.setFluidFlowRateMax(fRate) | |
523 | ||
524 | tID = os.startTimer(loopT) -- Wait for loopT secounds. | |
525 | else | |
526 | -- Turbine switched off: | |
527 | tID = os.startTimer(displayT) -- Wait for displayT secounds. | |
528 | end | |
529 | ||
530 | saveControllerState() | |
531 | floatRateChanged = false | |
532 | repeat | |
533 | -- Event loop | |
534 | local evt, p1, p2, p3, p4, p5 = os.pullEvent() | |
535 | if evt == "char" then | |
536 | -- character typed | |
537 | local oldTSpeed = tSpeed | |
538 | if p1 == "o" then -- off | |
539 | tSpeed = 0 | |
540 | turbine.setActive(false) | |
541 | saveControllerState() | |
542 | elseif p1 == "m" then -- medium speed = 900 RPM | |
543 | turbine.setActive(true) | |
544 | tSpeed = 900 | |
545 | floatRateChanged=true | |
546 | elseif p1 == "f" then -- fast speed = 1800 RPM | |
547 | turbine.setActive(true) | |
548 | tSpeed = 1800 | |
549 | floatRateChanged=true | |
550 | end | |
551 | ||
552 | if turbine.setInductorEngaged then | |
553 | -- engage coil by default | |
554 | turbine.setInductorEngaged(true) | |
555 | end | |
556 | ||
557 | ||
558 | if (p1 == "m" or p1 == "f") and tSpeed ~= oldTSpeed then | |
559 | -- Initialize PID controller values | |
560 | if Kp == 0 then | |
561 | Kp, loopT = testSpeedup(turbine, loopT, tSpeed) | |
562 | end | |
563 | ||
564 | initControllerValues() | |
565 | ||
566 | errSum = 0 | |
567 | errOld = 0 | |
568 | ||
569 | saveControllerState() | |
570 | end | |
571 | elseif evt == "modem_message" | |
572 | and stateRequestChannel >= 0 | |
573 | and stateRequestChannel == p2 | |
574 | and tostring(p4) == "BR_turbine_get_state" | |
575 | then | |
576 | -- Send status informations | |
577 | local cLabel = os.getComputerLabel() | |
578 | if cLabel == nil then | |
579 | -- No label: use coputer number (ID) as tio create a name | |
580 | cLabel = "ComputerID:" .. tostring(os.getComputerID()) | |
581 | end | |
582 | ||
583 | if turbine.getInductorEngaged ~= nil then | |
584 | inductorEngaged = turbine.getInductorEngaged() | |
585 | else | |
586 | inductorEngaged = nil | |
587 | end | |
588 | ||
589 | -- State structure | |
590 | local rState = { | |
591 | version = "Turbine V0.1", -- Version of data structure | |
592 | label = cLabel, | |
593 | computerId = os.getComputerID(), | |
594 | remoteControl = remoteControl, | |
595 | active = turbine.getActive(), | |
596 | tSpeed = tSpeed, | |
597 | energyStored = turbine.getEnergyStored(), | |
598 | rotorSpeed = turbine.getRotorSpeed(), | |
599 | inputAmount = turbine.getInputAmount(), | |
600 | inputType = turbine.getInputType(), | |
601 | outputAmount = turbine.getOutputAmount(), | |
602 | outputType = turbine.getOutputType(), | |
603 | fluidAmountMax = turbine.getFluidAmountMax(), | |
604 | fluidFlowRate = turbine.getFluidFlowRate(), | |
605 | fluidFlowRateMax = turbine.getFluidFlowRateMax(), | |
606 | fluidFlowRateMaxMax = turbine.getFluidFlowRateMaxMax(), | |
607 | energyProducedLastTick = turbine.getEnergyProducedLastTick(), | |
608 | inductorEngaged = inductorEngaged | |
609 | } | |
610 | ||
611 | peripheral.call(p1, "transmit", p3, stateRequestChannel, | |
612 | textutils.serialize(rState)) | |
613 | elseif evt == "modem_message" | |
614 | and remoteControl | |
615 | and p2 == os.getComputerID() | |
616 | and string.sub(tostring(p4), 1, 21) == "BR_turbine_set_speed:" | |
617 | then | |
618 | -- Remote Request: Speed change | |
619 | local oldTSpeed = tSpeed | |
620 | tSpeed = tonumber(string.sub(tostring(p4), 22)) | |
621 | if (tSpeed == 0) then | |
622 | turbine.setActive(false) | |
623 | saveControllerState() | |
624 | else | |
625 | turbine.setActive(true) | |
626 | floatRateChanged=true | |
627 | end | |
628 | ||
629 | if tSpeed ~= 0 and tSpeed ~= oldTSpeed then | |
630 | -- Initialize PID controller values | |
631 | if Kp == 0 then | |
632 | Kp = testSpeedup(turbine, loopT, tSpeed) | |
633 | end | |
634 | ||
635 | initControllerValues() | |
636 | ||
637 | errSum = 0 | |
638 | errOld = 0 | |
639 | ||
640 | saveControllerState() | |
641 | end | |
642 | ||
643 | if turbine.setInductorEngaged then | |
644 | -- engage coil by default | |
645 | turbine.setInductorEngaged(true) | |
646 | end | |
647 | elseif evt == "peripheral" | |
648 | and peripheral.getType(p1) == "modem" | |
649 | and stateRequestChannel >= 0 | |
650 | then | |
651 | -- new modem connected | |
652 | peripheral.call(p1, "open", stateRequestChannel) | |
653 | peripheral.call(p1, "open", os.getComputerID()) | |
654 | end | |
655 | ||
656 | -- exit loop on timer event or float rate change | |
657 | until (evt == "timer" and p1 == tID) or floatRateChanged | |
658 | end | |
659 | ||
660 | -- | |
661 | -- EOF | |
662 | -- |