Загрузка данных


-- ============================================================
-- VTOL SERVER  (server.lua)
-- Peripherals: Create_RotationSpeedController, redstone_link_bridge
-- CC:Sable API: sublevel, aero
-- Communicates with GUI via rednet (protocol "vtol")
-- ============================================================

-- ── Частоты редстоун-линков ──────────────────────────────────
local FREQ = {
  RIGHT_FWD  = { "minecraft:purple_glazed_terracotta", "minecraft:red_glazed_terracotta"   },
  RIGHT_BACK = { "minecraft:purple_glazed_terracotta", "minecraft:blue_glazed_terracotta"  },
  LEFT_FWD   = { "minecraft:purple_glazed_terracotta", "minecraft:yellow_glazed_terracotta"},
  LEFT_BACK  = { "minecraft:purple_glazed_terracotta", "minecraft:lime_glazed_terracotta"  },
}

-- ── Константы ────────────────────────────────────────────────
local RPM_MIN        = 1
local RPM_MAX        = 256
local RPM_HOVER      = 128     -- стартовый RPM для зависания
local LINK_ON        = 15
local LINK_OFF       = 0

local ALT_KP         = 4.0    -- пропорц. высота
local ALT_KD         = 2.0    -- дифф.   высота
local NAV_KP         = 0.25   -- пропорц. навигация (в RPM-единицах на блок)
local NAV_KD         = 0.5
local NAV_YAW_KP     = 1.8    -- поворот к цели
local NAV_YAW_KD     = 0.8
local NAV_ARRIVE_R   = 3.0    -- радиус «прибыл»
local LOOP_DT        = 0.1    -- секунды

-- ── Состояние ────────────────────────────────────────────────
local state = {
  mode       = "HOLD",    -- HOLD | NAV | MANUAL
  rpm        = RPM_HOVER,
  thrust     = { rf=0, rb=0, lf=0, lb=0 },  -- 0 или 15
  target     = { x=0, y=0, z=0 },
  status     = "Инициализация...",
  gui_id     = nil,
}

local altPrev_err = 0
local navPrev_ex  = 0
local navPrev_ez  = 0
local yawPrev_err = 0

-- ── Peripherals ──────────────────────────────────────────────
local rsc  = peripheral.find("Create_RotationSpeedController")
    or error("Не найден Create_RotationSpeedController")
local rlb  = peripheral.find("redstone_link_bridge")
    or error("Не найден redstone_link_bridge")
local modem = peripheral.find("modem") or error("Не найден модем")
rednet.open(peripheral.getName(modem))

-- ── Вспомогательные ─────────────────────────────────────────
local function clampRPM(v) return math.max(RPM_MIN, math.min(RPM_MAX, math.floor(v+0.5))) end
local function sign(v) return v > 0 and 1 or (v < 0 and -1 or 0) end

local function setRPM(r)
  state.rpm = clampRPM(r)
  rsc.setTargetSpeed(state.rpm)
end

local function sendLinks()
  local t = state.thrust
  rlb.sendLinkSignal(FREQ.RIGHT_FWD[1],  FREQ.RIGHT_FWD[2],  t.rf)
  rlb.sendLinkSignal(FREQ.RIGHT_BACK[1], FREQ.RIGHT_BACK[2], t.rb)
  rlb.sendLinkSignal(FREQ.LEFT_FWD[1],   FREQ.LEFT_FWD[2],   t.lf)
  rlb.sendLinkSignal(FREQ.LEFT_BACK[1],  FREQ.LEFT_BACK[2],  t.lb)
end

local function allStop()
  state.thrust = {rf=0, rb=0, lf=0, lb=0}
  sendLinks()
end

-- Получить позу (x,y,z) и рыскание из sublevel
local function getPose()
  local ok, pose = pcall(sublevel.getLogicalPose)
  if not ok or not pose then return nil end
  -- pose.position = {x,y,z}, pose.orientation = кватернион {w,x,y,z}
  local p = pose.position
  -- Yaw из кватерниона: atan2(2*(w*y - x*z), 1-2*(y^2+z^2))   [вокруг оси Y Minecraft]
  local q = pose.orientation
  local yaw = math.atan2(2*(q.w*q.y - q.x*q.z), 1 - 2*(q.y*q.y + q.z*q.z))
  return p.x, p.y, p.z, yaw
end

local function getVel()
  local ok, v = pcall(sublevel.getVelocity)
  if not ok then return 0,0,0 end
  return v.x, v.y, v.z
end

-- ── Трансляция состояния GUI ─────────────────────────────────
local function broadcast()
  if not state.gui_id then return end
  local px,py,pz,yaw = getPose()
  local packet = {
    type   = "STATE",
    mode   = state.mode,
    rpm    = state.rpm,
    thrust = state.thrust,
    target = state.target,
    pos    = px and {x=px,y=py,z=pz} or nil,
    yaw    = yaw,
    status = state.status,
    rpmMax = rsc.getTargetSpeed and rsc.getTargetSpeed() or state.rpm,
  }
  rednet.send(state.gui_id, packet, "vtol")
end

-- ── Логика режимов ───────────────────────────────────────────

-- HOLD: держать текущую высоту PD-регулятором
local holdY = nil
local function tickHold()
  local _,py,_,_ = getPose()
  if not py then state.status="Нет данных позиции"; return end
  if not holdY then holdY = py end

  local _,vy,_ = getVel()
  local err = holdY - py
  local derr = -vy  -- дериватив = минус вертикальная скорость
  local corr = ALT_KP*err + ALT_KD*derr
  altPrev_err = err

  setRPM(state.rpm + corr)
  allStop()
  state.status = string.format("HOLD  Y=%.1f цель=%.1f Δ=%.2f", py, holdY, err)
end

-- NAV: лететь к target.x/y/z
local function tickNav()
  local px,py,pz,yaw = getPose()
  if not px then state.status="Нет данных позиции"; return end
  local tx,ty,tz = state.target.x, state.target.y, state.target.z

  -- Высота
  local _,vy,_ = getVel()
  local ey = ty - py
  local corrAlt = ALT_KP*ey + ALT_KD*(-vy)
  setRPM(state.rpm + corrAlt)

  -- Горизонталь
  local dx = tx - px
  local dz = tz - pz
  local dist = math.sqrt(dx*dx + dz*dz)

  if dist < NAV_ARRIVE_R then
    allStop()
    holdY = py
    state.status = string.format("ARRIVED  dist=%.1f", dist)
    return
  end

  -- Нужный курс (yaw) на цель
  local targetYaw = math.atan2(dx, dz)   -- MC: +Z = север, atan2(x,z) = heading
  local yawErr = targetYaw - (yaw or 0)
  -- Нормализация [-π, π]
  yawErr = (yawErr + math.pi) % (2*math.pi) - math.pi

  local vx,_,vz = getVel()
  -- Поступательная ошибка вдоль нужного направления
  local ex = dx/dist   -- единичный вектор
  local ez = dz/dist
  local speed = vx*ex + vz*ez

  local fwdCmd  = NAV_KP*dist  + NAV_KD*(-speed)
  local yawCmd  = NAV_YAW_KP*yawErr + NAV_YAW_KD*(-yawPrev_err)
  yawPrev_err   = yawErr

  -- Маппинг команд в линки
  -- fwdCmd > 0 → вперёд (RF+LF), < 0 → назад (RB+LB)
  -- yawCmd > 0 → по часовой (LF+RB), < 0 → против (RF+LB)
  local newThrust = {rf=0, rb=0, lf=0, lb=0}
  if fwdCmd > 0.5 then
    newThrust.rf = LINK_ON; newThrust.lf = LINK_ON
    newThrust.rb = LINK_OFF; newThrust.lb = LINK_OFF
  elseif fwdCmd < -0.5 then
    newThrust.rb = LINK_ON; newThrust.lb = LINK_ON
    newThrust.rf = LINK_OFF; newThrust.lf = LINK_OFF
  end

  -- Поворот перекрывает одну сторону
  if math.abs(yawErr) > 0.1 then
    if yawErr > 0 then
      -- вправо: LF + RB
      newThrust.lf = LINK_ON; newThrust.rb = LINK_ON
      newThrust.rf = LINK_OFF; newThrust.lb = LINK_OFF
    else
      -- влево: RF + LB
      newThrust.rf = LINK_ON; newThrust.lb = LINK_ON
      newThrust.lf = LINK_OFF; newThrust.rb = LINK_OFF
    end
  end

  state.thrust = newThrust
  sendLinks()
  state.status = string.format("NAV dist=%.1f yaw=%.1f°", dist, math.deg(yawErr))
end

-- MANUAL: GUI сам шлёт thrust-команды, сервер только слушает и применяет
local function tickManual()
  sendLinks()  -- применяем то, что было установлено командой
  state.status = "MANUAL"
end

-- ── Обработка команд от GUI ──────────────────────────────────
local function handleCmd(id, msg)
  state.gui_id = id
  local cmd = msg.cmd

  if cmd == "PING" then
    broadcast(); return
  end

  if cmd == "SET_MODE" then
    state.mode = msg.mode
    if msg.mode == "HOLD" then
      local _,py = getPose(); holdY = py
      allStop()
    elseif msg.mode == "MANUAL" then
      allStop()
    elseif msg.mode == "NAV" then
      yawPrev_err = 0
    end
    state.status = "Режим: "..state.mode

  elseif cmd == "SET_TARGET" then
    state.target = { x=msg.x, y=msg.y, z=msg.z }
    state.status = string.format("Цель: %.0f %.0f %.0f", msg.x, msg.y, msg.z)

  elseif cmd == "SET_RPM" then
    setRPM(msg.rpm)

  elseif cmd == "THRUST" then
    -- MANUAL thrust override
    state.thrust = msg.thrust
    sendLinks()

  elseif cmd == "HOLD_HERE" then
    local _,py = getPose(); holdY = py
    state.mode = "HOLD"
    allStop()
    state.status = "HOLD на Y="..(holdY and string.format("%.1f",holdY) or "?")

  elseif cmd == "EMERGENCY_STOP" then
    state.mode = "HOLD"
    allStop()
    setRPM(RPM_HOVER)
    holdY = nil
    state.status = "!! АВАРИЙНАЯ ОСТАНОВКА !!"
  end
end

-- ── Главный цикл ─────────────────────────────────────────────
print("[SERVER] Запущен. Ожидание GUI...")
local tickTimer = os.startTimer(LOOP_DT)

while true do
  local ev, p1, p2, p3 = os.pullEvent()

  if ev == "rednet_message" then
    local senderId, msg = p1, p2
    if type(msg) == "table" then
      handleCmd(senderId, msg)
    end

  elseif ev == "timer" and p1 == tickTimer then
    if state.mode == "HOLD" then
      tickHold()
    elseif state.mode == "NAV" then
      tickNav()
    elseif state.mode == "MANUAL" then
      tickManual()
    end
    broadcast()
    tickTimer = os.startTimer(LOOP_DT)
  end
end