-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfission.lua
More file actions
317 lines (278 loc) · 11.7 KB
/
Copy pathfission.lua
File metadata and controls
317 lines (278 loc) · 11.7 KB
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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
-- fission.lua — Mekanism 核分裂炉 自動制御(CC: Tweaked)
--
-- 状態機械(安全最優先):
-- DISARMED : 炉OFF。R/ARM を押すまで点火しない。
-- RUNNING : 炉ON。温度に応じて burn rate を自動調整。
-- SCRAMMED : 安全トリップで緊急停止。再アームするまで再点火しない(ラッチ)。
--
-- 操作はキー(R/S/Q)とモニタータッチ(ボタン)の両対応。共通の関数を呼ぶ。
--
-- 鉄則:
-- * 安全判定が NG の瞬間に必ず scram する(毎サイクル評価)。
-- * トリップ後は自動再起動しない。人間の再アームを要求する。
-- * 重要値が読めなければ危険とみなす(reactor.lua のフェイルセーフ)。
local cfg = require("config")
local Reactor = require("reactor")
local Turbine = require("turbine")
local Boiler = require("boiler")
local state = require("state")
local ui = require("ui")
local reactor = Reactor.new(cfg.adapterName)
if not reactor:ok() then
error("Fission Reactor Logic Adapter not found. Check adjacency or wired modem.", 0)
end
-- タービン(任意)。設定で有効なら検出する。
local turbine = nil
if cfg.turbine and cfg.turbine.enabled then
local t = Turbine.new(cfg.turbine.name)
if t:ok() then
turbine = t
elseif cfg.turbine.required then
error("Turbine not found (turbine.required=true). Check connection.", 0)
end
end
-- ボイラー(任意)。水量を読めれば「水枯れの早期検知」に使う(最重要の上流信号)。
-- 未接続なら nil = 制約は自動的に無効(従来動作)。有線モデムでボイラーバルブを繋ぐと有効化。
local boiler = nil
if cfg.boiler == nil or cfg.boiler.enabled ~= false then
local b = Boiler.new(cfg.boiler and cfg.boiler.name)
if b:ok() then boiler = b end
end
local out = ui.resolveOutput(cfg.monitorName, cfg.monitorTextScale)
-- 保存済み設定(プロファイル)を復元して config に反映する。
-- 反映ロジックは config.lua に集約(cfg.applyProfile)。温度系 + 出力 setpoint 群 + softTemp を
-- まとめて反映するので、ここで個別に書くと反映漏れが起きる。必ず集約版を呼ぶ。
local function applyProfile(name)
local p = cfg.profiles[name]
if not p then return false end
cfg.profile = name
cfg.applyProfile(cfg, p)
return true
end
do
local saved = state.load()
if saved.profile then applyProfile(saved.profile) end
end
-- 起動時デバッグダンプ
if cfg.debug then
ui.dumpRaw(reactor)
end
-- 能力監査: メソッド名が MOD バージョンで違わないか起動時に確認する。
local missingCritical, missingOptional = reactor:audit()
if #missingCritical > 0 then
term.clear(); term.setCursorPos(1, 1)
print("!! Missing safety-critical methods (safety cannot work):")
for _, m in ipairs(missingCritical) do print(" - " .. m) end
print("")
print("API names may differ by Mekanism version. Set config.debug=true to")
print("dump raw values, then fix method names in reactor.lua. Aborting for safety.")
error("aborting: critical methods missing", 0)
end
if #missingOptional > 0 then
print("Note: these are display/optional only (no safety impact), not found:")
for _, m in ipairs(missingOptional) do print(" - " .. m) end
print("Press Enter to continue...")
read()
end
-- 状態と最後に描画したボタン群。
local fsmState = "DISARMED"
local buttons = {}
-- ブラックボックス記録: 直近の軌跡を保持し、落ちた瞬間に凍結して画面に出す(実機の落ち方を捕まえる)。
local rec = {} -- リングバッファ({b,c,h,T,e})
local recMax = 80 -- 直近 80 tick(dt=0.5 で 40 秒)
local crashLog = nil -- SCRAM 時に凍結したコピー
local scramReason = nil
-- 起動直後は安全のため炉を止める(既に動いていても一旦 scram)。
do
local r0 = reactor:read()
if r0.status and cfg.startDisarmed then
reactor:scram()
elseif r0.status then
fsmState = "RUNNING"
end
end
local function draw(r, msg)
buttons = ui.render(out, r, fsmState, msg, cfg)
end
-- 状態を変えず現在値を読んで再描画する(操作直後の即時フィードバック用)。
local function refresh(msg)
local r = reactor:read()
r.turbinePct = turbine and turbine:energyPct() or nil
draw(r, msg)
end
-- 共通アクション(キーとタッチで共有)-------------------------------------
-- 通常停止: burn を 0 に絞ってから止める。次の点火が前回の高 burn で始まらない=穏やか・安全。
local function doStop()
reactor:call("setBurnRate", 0)
reactor:scram()
fsmState = "DISARMED"
refresh("STOPPED")
end
-- 緊急停止: 即叩き落とし(burn を絞らない)。本当の緊急時だけ。手動なので DISARMED(事故ラッチはしない)。
local function doScram()
reactor:scram()
fsmState = "DISARMED"
refresh("EMERGENCY SCRAM")
end
-- 点火: 安全な時だけ着火。前回の burn が高いまま残っていても安全に立ち上がるよう 0 から始める。
local function doIgnite()
local r = reactor:read()
r.turbinePct = turbine and turbine:energyPct() or nil
local safe, reason = reactor:checkSafety(r, cfg)
if safe then
reactor:call("setBurnRate", 0) -- 0 から立ち上げ(前回の高 burn 残りを無効化)
reactor:activate()
fsmState = "RUNNING"
crashLog, scramReason = nil, nil -- 再点火でブラックボックス表示を解除
draw(r, "IGNITED")
else
draw(r, "IGNITE DENIED: " .. reason) -- tick で上書きされない(入力後 tick しない)
end
end
local function doProfile(name)
if applyProfile(name) then
state.save({ profile = cfg.profile }) -- 再起動後も維持
refresh("PROFILE -> " .. name)
end
end
-- 計測用(更新が本当に毎秒回っているか画面で確認するため)。
local beat, cycle, lastEpoch, periodSec = 0, 0, nil, 0
-- 1 サイクル分の制御ロジック。-------------------------------------------
local function tick()
beat = beat + 1
cycle = cycle + 1
local now = os.epoch("utc")
if lastEpoch then periodSec = (now - lastEpoch) / 1000 end
lastEpoch = now
-- 表示専用の重い値は extrasRefreshSec ごとにだけ読む(peripheral 呼び出し削減)。
local every = math.max(1, math.floor((cfg.extrasRefreshSec or 2) / (cfg.tickInterval or 1) + 0.5))
local full = (cycle % every == 1)
local r = reactor:read(full)
local turbinePct = turbine and turbine:energyPct() or nil
r.turbinePct = turbinePct
r.turbineSteamPct = turbine and turbine:steamPct() or nil -- タービン蒸気(過負荷の早期信号)
r.turbineProduction = turbine and turbine:productionRate() or nil -- 発電量(表示用。読めなければ非表示)
r.boilerWaterPct = boiler and boiler:waterPct() or nil -- ボイラー水(水枯れ=最重要の上流信号)
r.beat = beat
r.period = periodSec
-- ブラックボックス: 毎 tick の軌跡を記録(落ちる直前の動きを残す)。
rec[#rec + 1] = { b = r.burnRate, c = r.coolantPct, h = r.heatedPct, T = r.temp, e = turbinePct }
if #rec > recMax then table.remove(rec, 1) end
-- 直近の burn 振れ幅(ハンチング量の可視化。HOLD なら 0 付近、爆下げループなら大きい)。
local bmn, bmx = 1e9, -1e9
for _, s in ipairs(rec) do
local b = s.b or 0
if b < bmn then bmn = b end
if b > bmx then bmx = b end
end
r.burnP2P = (bmx >= bmn) and (bmx - bmn) or 0
r.recSpan = #rec * (cfg.tickInterval or 0.5)
local safe, reason = reactor:checkSafety(r, cfg)
if not safe then
reactor:scram() -- 安全NGの瞬間は getStatus の読めなさに依らず必ず止める(停止済みへの scram は no-op)
fsmState = "SCRAMMED"
if not crashLog then -- 落ちた瞬間の軌跡を凍結(再アームまで保持)
crashLog = {}
for i = 1, #rec do crashLog[i] = rec[i] end
scramReason = reason
end
buttons = ui.renderBlackBox(out, crashLog, scramReason, cfg)
return
end
if fsmState == "RUNNING" then
local msg
if not r.status then
reactor:activate()
msg = "igniting..."
else
msg = reactor:autoAdjust(r, cfg, turbinePct) -- 制御内容を表示(自動制御の可視化)
end
draw(r, msg)
elseif fsmState == "SCRAMMED" then
reactor:scram() -- ラッチ中は status に依らず止め続ける(無条件 scram は no-op で安全)
buttons = ui.renderBlackBox(out, crashLog, scramReason, cfg) -- 落ちた軌跡を出し続ける(スクショ用)
else
draw(r, "STANDBY (press ARM/R to start)")
end
end
-- 入力処理 ---------------------------------------------------------------
local function handleAction(action)
if action == "stop" then
doStop()
elseif action == "scram" then
doScram()
elseif action == "ignite" then
doIgnite()
elseif action == "profile:safety" then
doProfile("safety")
elseif action == "profile:balance" then
doProfile("balance")
elseif action == "profile:performance" then
doProfile("performance")
end
end
local function onKey(key)
if key == keys.i then doIgnite()
elseif key == keys.s then doStop()
elseif key == keys.x then doScram()
elseif key == keys.q then return true end
return false
end
local function onTouch(x, y)
for _, b in ipairs(buttons) do
if x >= b.x1 and x <= b.x2 and y >= b.y1 and y <= b.y2 then
handleAction(b.action)
return
end
end
end
-- メインループ -----------------------------------------------------------
-- 制御は sleep ベースの専用コルーチンで回す。
-- 旧実装は手動タイマー + フィルタ無し os.pullEvent だったが、忙しいサーバー
-- (多数の MOD)でイベントが大量に飛ぶと CC のイベントキュー(上限256)が溢れ、
-- 周期タイマーのイベントがドロップ → tick が二度と回らない事故が起きた。
-- sleep(内部でフィルタ付き pullEvent)はイベント嵐に強いので、これで回す。
local running = true
local function controlLoop()
while running do
tick() -- 自動制御 + 再描画
sleep(cfg.tickInterval) -- フィルタ付き待機(嵐に強い)
end
end
local function inputLoop()
while running do
local ev, p1, p2, p3 = os.pullEvent()
if ev == "key" then
if onKey(p1) then running = false; return end
elseif ev == "monitor_touch" then
onTouch(p2, p3) -- ARM/SCRAM/プロファイル(即描画)
elseif ev == "monitor_resize" then
tick() -- サイズ変更に即追従
elseif ev == "terminate" then
running = false; return
end
end
end
local function main()
parallel.waitForAny(controlLoop, inputLoop)
end
-- 終了時: 安全のため炉を止めてから抜ける。
local okRun, err = pcall(main)
reactor:scram()
-- 終了/エラーをコンピュータ端末とモニターの「両方」に出す
-- (モニターだけ見ていてクラッシュに気づけない問題を防ぐ)。
local function report(o)
if not o then return end
if o.setBackgroundColor then o.setBackgroundColor(colors.black) end
if o.setTextColor then o.setTextColor(colors.white) end
if o.clear then o.clear() end
if o.setCursorPos then o.setCursorPos(1, 1) end
o.write("fission-control stopped. Reactor SCRAMmed.")
if not okRun then
if o.setCursorPos then o.setCursorPos(1, 3) end
if o.setTextColor then o.setTextColor(colors.red) end
o.write("ERROR: " .. tostring(err))
end
end
report(term)
if out ~= term then report(out) end