View difference between Paste ID: 3ZhtbKH3 and gTEBHv3D
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
--