PLCLAB
5층 엘리베이터 PLC 로직·I/O 전체 보기
CONTROL LOGIC / SIGNAL MAP
01.1 / FROM INPUT TO OUTPUT

버튼 하나가 모터 출력이 되기까지

실제 실행 코드의 제어 경로 18개와 가상 I/O 17개를 연결해 봅니다.

PLC 시뮬레이터로 이동
아래 레더는 JavaScript PLC 실행 로직을 읽기 쉽게 풀어 쓴 교육용 도식입니다. 제조사 PLC에 바로 내려받는 실행 레더가 아니며, 카드의 좌우 순서가 실제 스캔 전체 순서를 뜻하지 않습니다. 상세 분기·계산은 발췌 코드와 전체 소스에 있습니다.

전체 로직 한눈에 보기

ALL 18 NETWORKS / ONE SHEET

18개 로직을 하나의 도면으로 모았습니다. 번호는 아래 상세 설명과 같습니다. 먼저 전체를 보고, 확대해서 조건과 출력을 읽어보세요.

원본 크게 열기 ↗전체 도면 저장
전체 PLC 제어 로직 18개. 1~6은 스캔·안전·호출, 7~12는 이동·문·화재 입력, 13~18은 감속·귀환·하차·복귀입니다. 각 조건의 상세 내용은 아래 같은 번호의 카드에 있습니다.

휴대폰에서는 확대 후 도면을 좌우·위아래로 스크롤할 수 있습니다. 이 도면은 교육용 요약이며 실행 가능한 PLC 프로그램은 아닙니다.

한눈에 보는 제어 흐름

SCAN 50 ms

입력·기억

층 호출, 문 버튼, 안전·화재 입력

호출 기억 / 고장·화재 래치

모드·상태 판단

고장 → 출력 중지
화재 → 지정 층 귀환
일반 → 방향 선택·층 서비스

출력·가상 설비

Q0.0 상승 · Q0.1 하강
Q0.2 문 열기 · Q0.3 문 닫기
↓ 위치·문 비율 → 가상 센서

일반 운전IDLE → MOVING → OPENING → OPEN → CLOSING → IDLE

현재 층 호출은 MOVING 생략 · 끼임은 OPENING으로 재개방 · 고장은 FAULT

화재 귀환FIRE_PREP → FIRE_CLOSING → FIRE_PREP → FIRE_MOVING → FIRE_OPENING → FIRE_HOLD

열린 문일 때 닫기 경유 · 역방향이면 FIRE_BRAKE → FIRE_TURN(0.4초) → FIRE_PREP · 이미 귀환층이면 FIRE_OPENING

I/O 주소와 담당 기능

12 INPUTS · 4 OUTPUTS · 1 LATCH

화면의 가상 주소 표기입니다. 실제 PLC 단자에 연결되어 있지 않습니다. 위치·문 끝단 신호는 가상 설비 상태에서 계산됩니다.

주소변수신호가 만들어지는 곳로직에서 하는 일
I0.0DoorClosed문 닫힘door === 0출발 허용·이동 중 문 인터록
I0.1DoorOpened문 완전 열림door === 1문 동작 완료 표시
I0.2Obstacle문 끼임사용자 스위치유지시간 초기화·닫기 중 재개방
I0.3Overload과부하사용자 스위치출발 방지·문 재개방·이동 중 고장
I0.4Estop비상정지ON = 비상정지 요청고장 래치·출력 중지
I0.5FireAlarm건물 화재사용자 스위치화재 귀환 래치 설정
I0.6MainSmoke1층 연기사용자 스위치독립 귀환 시작·2층 대체 귀환
I1.0FloorSensor[1]1층 위치ZONE AND HERE = 1층 위치 표시·도착 판단
I1.1FloorSensor[2]2층 위치ZONE AND HERE = 2층 위치 표시·도착 판단
I1.2FloorSensor[3]3층 위치ZONE AND HERE = 3층 위치 표시·도착 판단
I1.3FloorSensor[4]4층 위치ZONE AND HERE = 4층 위치 표시·도착 판단
I1.4FloorSensor[5]5층 위치ZONE AND HERE = 5층 위치 표시·도착 판단
Q0.0Up상승 모터정상 이동·화재 이동·감속 분기하강 출력과 동시에 켜지지 않음
Q0.1Down하강 모터정상 이동·화재 이동·감속 분기상승 출력과 동시에 켜지지 않음
Q0.2OpenMotor문 열기 모터정상·화재 열기 / 귀환 끼임문 열림 비율 증가
Q0.3CloseMotor문 닫기 모터정상·화재 닫기문 열림 비율 감소
M0.0FireActive화재 귀환 기억화재 입력으로 SET입력이 꺼져도 관리자 복귀까지 유지

레더 로직과 기능 캡션

직렬 조건은 AND, 조건 안의 OR는 대안입니다. SET/RESET은 기억의 설정·해제, 화살표는 상태 전이를 뜻합니다.

전체 로직 18개 표시

NETWORK 01

스캔 시작과 출력 초기화

LSCAN 50 msQ0.0~Q0.3 := 0R

화면에서 이 부분 PLC 라이브 모니터의 SCAN COUNT와 Q 출력

매 스캔 시작 시 모터 출력을 모두 끕니다. 이후 정상·화재 상태 분기에서 이번 스캔의 출력만 켭니다.

실제 코드 위치 · scan(dt=.05)

controller.js 56행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

    this.scanCount++;this.outputs={up:false,down:false,open:false,close:false};
    // Evaluate existing motion interlocks before changing operating mode.
    if(this.isMoving()&&this.overload)this.trip('이동 중 과부하');
    if(this.isMoving()&&this.door>0)this.trip('문 닫힘 인터록');
    if((this.fireAlarm||this.mainSmoke)&&!this.fireActive){this.fireActive=true;this.recallFloor=this.mainSmoke?2:1;this.calls.clear();this.log('화재 입력 수신 · 호출 취소 · 목표 '+this.recallFloor+'층');if(!this.fault)this.planRecall(this.isMoving());}
NETWORK 02

고장 래치

LE-STOP OR (MOVING AND (OVERLOAD OR DOOR>0))SET FAULTR

화면에서 이 부분 안전 입력의 비상정지·과부하 / 운전 정지

비상정지는 항상 고장을 래치합니다. 이동 상태(MOVING / FIRE_MOVING / FIRE_BRAKE)에서 과부하 또는 문 열림이 있으면 고장으로 전환하고 출력을 갱신하지 않습니다.

실제 코드 위치 · scan(dt=.05)

controller.js 62행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

    if(this.estop)this.trip('비상정지');
    if(this.isMoving()&&this.overload)this.trip('이동 중 과부하');
    if(this.isMoving()&&this.door>0)this.trip('문 닫힘 인터록');
    if(this.fault)return;
    this.timer+=dt;
NETWORK 03

고장 리셋

LResetPBNOT E-STOPNOT OVERLOADFAULTRESET FAULTR

화면에서 이 부분 고장 리셋 버튼 / 화재 중 고장 회복

고장이 있어도 비상정지·과부하가 남아 있으면 복귀하지 않습니다. 화재 래치가 있으면 FIRE_PREP, 아니면 문 상태에 따라 OPENING 또는 IDLE로 돌아갑니다.

실제 코드 위치 · resetFault()

controller.js 9행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

  resetFault(){if(this.estop||this.overload)return false;if(this.fault){this.log('고장 리셋 · '+(this.fireActive?'화재 귀환 재개':'호출 유지'));this.fault='';this.transition(this.fireActive?'FIRE_PREP':this.door>0?'OPENING':'IDLE');}return true;}
  openDoor(){if(this.fireActive||this.fireAlarm||this.mainSmoke)return;if(['IDLE','OPEN','CLOSING'].includes(this.state)&&Math.abs(this.pos-Math.round(this.pos))<.001&&!this.fault){this.transition('OPENING');this.timer=0;}}
  closeDoor(){if(this.fireActive||this.fireAlarm||this.mainSmoke)return;if(this.state==='OPEN'&&!this.obstacle&&!this.overload)this.transition('CLOSING');}
  resetFire(){if(!this.fireActive||this.fireAlarm||this.mainSmoke||this.fault||this.state!=='FIRE_HOLD')return false;this.fireActive=false;this.recallFloor=1;this.dir=0;this.transition('OPEN');this.log('관리자 화재 복귀 승인 · 일반 운전 재개');return true;}
  isMoving(){return ['MOVING','FIRE_MOVING','FIRE_BRAKE'].includes(this.state);}
NETWORK 04

층별 호출 기억

LCall[n]NOT (FIRE_ACTIVE OR FIRE OR SMOKE)SET REQUEST[n]R

화면에서 이 부분 1~5층 승강장 버튼·목적지 버튼 / 호출 메모리

승강장과 카 내부의 같은 층 버튼은 하나의 호출 집합으로 합칩니다. 현재 층에서 문 동작 중 호출하면 대기시간을 초기화하고, 닫는 중이면 다시 엽니다.

실제 코드 위치 · call(n)

controller.js 6행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

  call(n){if(this.fireActive||this.fireAlarm||this.mainSmoke)return;if(!Number.isInteger(n)||n<1||n>5)return;if(Math.abs(this.pos-n)<.001&&['OPENING','OPEN','CLOSING'].includes(this.state)){if(this.state==='CLOSING')this.transition('OPENING');this.timer=0;return;}if(!this.calls.has(n)){this.calls.add(n);this.log(n+'층 호출 저장');}}
  transition(s){if(this.state!==s){this.state=s;this.timer=0;}}
  trip(reason){if(!this.fault){this.fault=reason;this.log('운전 정지 · '+reason);}this.transition('FAULT');}
  resetFault(){if(this.estop||this.overload)return false;if(this.fault){this.log('고장 리셋 · '+(this.fireActive?'화재 귀환 재개':'호출 유지'));this.fault='';this.transition(this.fireActive?'FIRE_PREP':this.door>0?'OPENING':'IDLE');}return true;}
  openDoor(){if(this.fireActive||this.fireAlarm||this.mainSmoke)return;if(['IDLE','OPEN','CLOSING'].includes(this.state)&&Math.abs(this.pos-Math.round(this.pos))<.001&&!this.fault){this.transition('OPENING');this.timer=0;}}
NETWORK 05

같은 층 호출 처리

LIDLEZONEREQUEST[HERE]OPENING / RESET REQUESTR

화면에서 이 부분 현재 층 호출 → 문 열림

정차한 현재 층 호출은 이동하지 않고 문을 엽니다. 해당 층 요청을 지우고 도착 이벤트를 기록합니다.

실제 코드 위치 · scan() / IDLE

controller.js 71행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

        if(aligned&&this.calls.has(Math.round(this.pos))){this.calls.delete(Math.round(this.pos));this.transition('OPENING');this.log(this.floor+'층 도착 · 문 열림');}
        else if(!this.overload&&this.door===0){this.dir=this.chooseDirection();if(this.dir)this.transition('MOVING');}
        break;
      }
      case 'MOVING': {
NETWORK 06

이동 방향 선택

LIDLENOT OVERLOADDOOR=0요청 있음MOVING / DIR ±1R

화면에서 이 부분 여러 층 호출 시 운행 순서

현재 방향에 호출이 남으면 방향을 유지합니다. 한쪽만 있으면 그 방향, 양쪽이면 가장 가까운 층을 고릅니다. 거리가 같으면 낮은 층을 우선합니다.

실제 코드 위치 · chooseDirection()

controller.js 54행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

  chooseDirection(){const above=[...this.calls].some(n=>n>this.pos+.001),below=[...this.calls].some(n=>n<this.pos-.001);if(this.dir===1&&above)return 1;if(this.dir===-1&&below)return -1;if(above&&!below)return 1;if(below&&!above)return -1;if(above&&below){const nearest=[...this.calls].sort((a,b)=>Math.abs(a-this.pos)-Math.abs(b-this.pos)||a-b)[0];return nearest>this.pos?1:-1;}return 0;}
  scan(dt=.05){
    this.scanCount++;this.outputs={up:false,down:false,open:false,close:false};
    // Evaluate existing motion interlocks before changing operating mode.
    if(this.isMoving()&&this.overload)this.trip('이동 중 과부하');
NETWORK 07

상승·하강 및 층 도착

LMOVINGDIR=+1 / -1Q0.0 / Q0.1R

화면에서 이 부분 승강로 카 이동 / 중간 호출층 정차

층간 2초로 가상 위치를 갱신합니다. 요청 층 또는 1·5층에 도달하면 이동 출력을 끄고 요청을 지운 뒤 OPENING으로 전환합니다. 위 안전 검사에서 고장이면 이 분기까지 오지 않습니다.

실제 코드 위치 · scan() / MOVING

controller.js 75행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'MOVING': {
        if(!this.dir){this.transition('IDLE');break;}
        this.outputs.up=this.dir===1;this.outputs.down=this.dir===-1;
        const next=Math.max(1,Math.min(5,this.pos+this.dir*dt/2));
        const crossed=this.dir===1?Math.ceil(this.pos+1e-7):Math.floor(this.pos-1e-7);
NETWORK 08

수동 문 열기

LOpenPBZONEIDLE OR OPEN OR CLOSINGNOT FAULT / FIREOPENINGR

화면에서 이 부분 문 열기 버튼

층 위치가 맞고 허용 상태일 때만 문 열기 요청을 받습니다. 화재 신호·귀환 래치 중에는 일반 문 버튼을 무시합니다.

실제 코드 위치 · openDoor()

controller.js 10행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

  openDoor(){if(this.fireActive||this.fireAlarm||this.mainSmoke)return;if(['IDLE','OPEN','CLOSING'].includes(this.state)&&Math.abs(this.pos-Math.round(this.pos))<.001&&!this.fault){this.transition('OPENING');this.timer=0;}}
  closeDoor(){if(this.fireActive||this.fireAlarm||this.mainSmoke)return;if(this.state==='OPEN'&&!this.obstacle&&!this.overload)this.transition('CLOSING');}
  resetFire(){if(!this.fireActive||this.fireAlarm||this.mainSmoke||this.fault||this.state!=='FIRE_HOLD')return false;this.fireActive=false;this.recallFloor=1;this.dir=0;this.transition('OPEN');this.log('관리자 화재 복귀 승인 · 일반 운전 재개');return true;}
  isMoving(){return ['MOVING','FIRE_MOVING','FIRE_BRAKE'].includes(this.state);}
  planRecall(wasMoving){
NETWORK 09

문 열기 출력과 완료

LOPENINGNOT FAULTQ0.2 → OPENR

화면에서 이 부분 문 애니메이션 / I0.1 문 열림

가상 문 열림 비율을 초당 1씩 늘립니다. 완전히 열리면 문 열기 출력을 끄고 OPEN으로 전환합니다. 문 끝단 센서는 가상 문 위치에서 계산합니다.

실제 코드 위치 · scan() / OPENING

controller.js 84행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'OPENING':this.outputs.open=true;this.door=Math.min(1,this.door+dt);if(this.door>=1){this.outputs.open=false;this.transition('OPEN');}break;
      case 'OPEN':if(this.obstacle||this.overload)this.timer=0;else if(this.timer>=2.5)this.transition('CLOSING');break;
      case 'CLOSING':if(this.obstacle||this.overload){this.transition('OPENING');this.log('문 다시 열기 · '+(this.obstacle?'장애물 감지':'과부하'));}else{this.outputs.close=true;this.door=Math.max(0,this.door-dt);if(this.door<=0){this.outputs.close=false;this.transition('IDLE');}}break;
    }
  }
NETWORK 10

문 열림 유지와 닫기 요청

LOPENNOT OBSTACLENOT OVERLOAD2.5 s OR ClosePBCLOSINGR

화면에서 이 부분 T1 문 열림 유지 막대 / 문 닫기 버튼

문 열림 상태에서 장애물·과부하가 있으면 유지 타이머를 0으로 되돌립니다. 방해가 없고 2.5초가 지나거나 문 닫기 버튼을 누르면 닫습니다.

실제 코드 위치 · scan() / OPEN + closeDoor()

controller.js 85행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'OPEN':if(this.obstacle||this.overload)this.timer=0;else if(this.timer>=2.5)this.transition('CLOSING');break;
      case 'CLOSING':if(this.obstacle||this.overload){this.transition('OPENING');this.log('문 다시 열기 · '+(this.obstacle?'장애물 감지':'과부하'));}else{this.outputs.close=true;this.door=Math.max(0,this.door-dt);if(this.door<=0){this.outputs.close=false;this.transition('IDLE');}}break;
    }
  }
  inputs(){return {closed:this.door===0,opened:this.door===1,zone:Math.abs(this.pos-Math.round(this.pos))<.001,safe:!this.estop&&!this.fault,overload:this.overload,obstacle:this.obstacle};}
NETWORK 11

문 닫기와 재개방

LCLOSINGNOT (OBSTACLE OR OVERLOAD)Q0.3 → IDLER

화면에서 이 부분 문 끼임 센서 / 과부하 / 문 다시 열기

정상 닫기 중 문 비율을 줄이고 닫힘 완료 시 출력을 끄고 IDLE로 갑니다. 끼임·과부하가 들어오면 닫기 대신 OPENING으로 바꿉니다.

실제 코드 위치 · scan() / CLOSING

controller.js 86행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'CLOSING':if(this.obstacle||this.overload){this.transition('OPENING');this.log('문 다시 열기 · '+(this.obstacle?'장애물 감지':'과부하'));}else{this.outputs.close=true;this.door=Math.max(0,this.door-dt);if(this.door<=0){this.outputs.close=false;this.transition('IDLE');}}break;
    }
  }
  inputs(){return {closed:this.door===0,opened:this.door===1,zone:Math.abs(this.pos-Math.round(this.pos))<.001,safe:!this.estop&&!this.fault,overload:this.overload,obstacle:this.obstacle};}
}
NETWORK 12

독립 화재 입력과 대체 귀환층

LI0.5 OR I0.6NOT M0.0SET M0.0 / CLEAR REQUESTR

화면에서 이 부분 화재 신호·1층 연기 스위치 / 귀환 목적지

화재 또는 1층 연기 입력은 비상정지 입력을 켜지 않고 귀환 래치를 설정합니다. 일반 호출을 비웁니다. 연기 입력이면 2층, 아니면 1층이며 귀환 중 연기 추가 시에도 2층으로 바꿉니다.

실제 코드 위치 · scan() / fire latch

controller.js 60행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

    if((this.fireAlarm||this.mainSmoke)&&!this.fireActive){this.fireActive=true;this.recallFloor=this.mainSmoke?2:1;this.calls.clear();this.log('화재 입력 수신 · 호출 취소 · 목표 '+this.recallFloor+'층');if(!this.fault)this.planRecall(this.isMoving());}
    if(this.fireActive&&this.mainSmoke&&this.recallFloor!==2){this.recallFloor=2;this.log('1층 연기 감지 · 대체 귀환층 2층으로 변경');if(!this.fault)this.planRecall(this.isMoving());}
    if(this.estop)this.trip('비상정지');
    if(this.isMoving()&&this.overload)this.trip('이동 중 과부하');
    if(this.isMoving()&&this.door>0)this.trip('문 닫힘 인터록');
NETWORK 13

반대 방향 이동의 감속과 전환

L이동 중DIR × 귀환 방향 ≤ 0FIRE_BRAKE → FIRE_TURNR

화면에서 이 부분 상승 중 화재 시험 / 감속·방향 전환

진행 방향의 다음 층까지 가상 감속 후 문을 열지 않고 정지합니다. 감속 시간은 max(0.4초, 남은 층 거리×4초), 그 뒤 0.4초 정지 대기합니다. 목표 방향으로 이동 중이면 닫힌 문 상태에서 귀환 이동을 이어갑니다.

실제 코드 위치 · planRecall() / FIRE_BRAKE / FIRE_TURN

controller.js 14행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

  planRecall(wasMoving){
    const delta=this.recallFloor-this.pos;
    if(wasMoving&&this.dir&&this.dir*Math.sign(delta)<=0){
      const aligned=Math.abs(this.pos-Math.round(this.pos))<1e-6;
      const end=aligned?Math.round(this.pos):this.dir>0?Math.ceil(this.pos):Math.floor(this.pos);
NETWORK 14

귀환 준비

LFIRE_PREPAT TARGET ? OPEN : CLOSE / MOVER

화면에서 이 부분 화재 진행 ①~② / 귀환 시작 전 문 처리

이미 귀환층이면 문 열기로 갑니다. 다른 층이고 문이 열려 있으면 먼저 닫고, 닫혔으며 과부하가 없으면 목표 방향으로 귀환 이동합니다.

실제 코드 위치 · scanFire() / FIRE_PREP

controller.js 26행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'FIRE_PREP':
        if(Math.abs(this.pos-this.recallFloor)<.001){this.floor=this.recallFloor;this.transition('FIRE_OPENING');}
        else if(this.door>0)this.transition('FIRE_CLOSING');
        else if(!this.overload){this.dir=Math.sign(this.recallFloor-this.pos);this.transition('FIRE_MOVING');this.log(this.recallFloor+'층으로 무정차 귀환');}
        break;
NETWORK 15

귀환 문 닫기와 방해 대기

LFIRE_CLOSINGNOT OBSTACLENOT OVERLOADQ0.3 → FIRE_PREPR

화면에서 이 부분 화재 중 끼임 대기 / AI 귀환 시간의 지연 구간

문 끼임·과부하가 있으면 문을 다시 열며 같은 상태에서 기다립니다. 해제되면 닫고 완료 후 FIRE_PREP로 돌아갑니다. 일반 문 닫기와는 상태 전이가 다릅니다.

실제 코드 위치 · scanFire() / FIRE_CLOSING

controller.js 31행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'FIRE_CLOSING':
        if(this.obstacle||this.overload){this.outputs.open=this.door<1;this.door=Math.min(1,this.door+dt);}
        else{this.outputs.close=true;this.door=Math.max(0,this.door-dt);if(this.door===0){this.outputs.close=false;this.transition('FIRE_PREP');}}
        break;
      case 'FIRE_BRAKE': {
NETWORK 16

지정 층 무정차 귀환

LFIRE_MOVINGDIR=sign(TARGET−POS)Q0.0 / Q0.1 → FIRE_OPENINGR

화면에서 이 부분 화재 진행 ③ / AI 시간 구성의 이동 구간

귀환층까지 초당 0.5층 이동합니다. 일반 호출에는 정차하지 않으며 도착 순간 이동 출력을 끄고 문 열기로 전환합니다.

실제 코드 위치 · scanFire() / FIRE_MOVING

controller.js 42행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'FIRE_MOVING': {
        this.dir=Math.sign(this.recallFloor-this.pos);const old=this.pos;
        this.pos+=this.dir*Math.min(Math.abs(this.recallFloor-this.pos),dt/2);
        this.outputs.up=this.dir===1;this.outputs.down=this.dir===-1;
        const crossed=this.dir>0?Math.floor(this.pos+1e-7):Math.ceil(this.pos-1e-7);if(crossed>=1&&crossed<=5&&(this.dir>0?crossed>old:crossed<old))this.floor=crossed;
NETWORK 17

하차 후 운행 중지

LFIRE_OPENINGQ0.2 → FIRE_HOLDR

화면에서 이 부분 화재 진행 ④ / 하차·운행 중지

문이 완전히 열릴 때까지 동작합니다. FIRE_HOLD에서는 출력을 모두 끈 상태로 문을 열어 두고 일반 호출을 계속 거부합니다.

실제 코드 위치 · scanFire() / FIRE_OPENING / FIRE_HOLD

controller.js 49행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

      case 'FIRE_OPENING':this.outputs.open=true;this.door=Math.min(1,this.door+dt);if(this.door===1){this.outputs.open=false;this.transition('FIRE_HOLD');this.log('문 열린 상태로 운행 중지 · 관리자 복귀 대기');}break;
      case 'FIRE_HOLD':break;
      default:this.transition('FIRE_PREP');
    }
  }
NETWORK 18

관리자 화재 복귀

LFireResetPBM0.0NOT FIRE / SMOKE / FAULTFIRE_HOLDRESET M0.0 → OPENR

화면에서 이 부분 관리자 화재 복귀 버튼

귀환 완료 후 화재·연기 입력과 고장이 모두 해제되어야 복귀할 수 있습니다. 귀환층을 기본 1층으로 되돌리고 일반 OPEN 상태로 전환합니다.

실제 코드 위치 · resetFire()

controller.js 12행부터 5행 발췌 · 전체 소스는 아래에서 다운로드할 수 있습니다.

  resetFire(){if(!this.fireActive||this.fireAlarm||this.mainSmoke||this.fault||this.state!=='FIRE_HOLD')return false;this.fireActive=false;this.recallFloor=1;this.dir=0;this.transition('OPEN');this.log('관리자 화재 복귀 승인 · 일반 운전 재개');return true;}
  isMoving(){return ['MOVING','FIRE_MOVING','FIRE_BRAKE'].includes(this.state);}
  planRecall(wasMoving){
    const delta=this.recallFloor-this.pos;
    if(wasMoving&&this.dir&&this.dir*Math.sign(delta)<=0){

주소를 부여하지 않은 내부 변수와 화면 입력

호출·상태·방향

calls / Request[1..5]: 층별 호출 기억
state / State: 제어 상태
dir / Direction: −1 하강, 0 대기, +1 상승
fault / Fault: 고장 기억
recallFloor / RecallFloor: 귀환 목적지

위치·문·시간

pos / Pos: 1~5층 연속 위치
floor / Floor: 마지막 통과·도착 층
door / Door: 닫힘 0~열림 1
ZONE: 정수 층 위치 오차 < 0.001
timer: 상태 전환 때 초기화하는 경과시간. T1 표시는 OPEN의 2.5초 유지 구간
brake: 감속 시작·종료 위치와 시간

HMI 버튼

Call[n]: 승강장·카 호출을 같은 층 요청으로 통합
OpenPB / ClosePB: 문 버튼
ResetPB / FireResetPB: 고장·관리자 복귀
이 버튼들에는 별도 I 주소를 지정하지 않았습니다. 일시정지·배속·전체 초기화·코드 저장은 시뮬레이터 기능이며 설비 I/O가 아닙니다.

ST의 HoldTime·PhaseTime은 유지·전환 시간, BrakeStart·BrakeEnd·BrakeDuration·Ratio는 감속 계산, Here·Above·Below·Nearest·Best·Distance·Reached·Step·OldPos·Delta·WasMoving·Replan·n은 센서·이동·탐색 계산에 쓰는 내부 변수입니다. 임의의 M·D·T 주소를 추가하지 않았습니다.

AI 페이지에서 PLC가 들어가는 부분

페이지PLC 사용연결과 캡션
01 PLC 시뮬레이터동일 ElevatorPLC 실행버튼 입력 → controller.js scan() → 가상 출력·위치 → 화면
02 AI 데이터 분석동일 ElevatorPLC를 반복 실행ai-engine.js가 초기 위치·문·방향·귀환층·끼임 시간을 바꿔 PLC를 실행합니다. FIRE_HOLD까지의 시간을 학습 정답으로 수집하고 kNN으로 예측합니다. 예측값은 PLC 출력으로 돌아가지 않습니다.
03 에너지 운전계획기존 PLC 미사용energy-engine.js의 별도 3대 군관리·전력 모델. 가동 계획은 계산되지만 Q0.0~Q0.3에 연결하지 않습니다. 따라서 추가된 실제 PLC 레더·I/O 주소는 없습니다.
04 고장 징후 탐지기존 PLC 미사용health-engine.js가 합성 전류·닫힘 시간·진동을 만듭니다. 아날로그 입력 주소에 매핑하지 않았으며 AI 경보가 FAULT나 비상정지를 작동시키지 않습니다.
05 AI 활용안 15설명 페이지제안·적용 사례의 소개이며, PLC 프로그램이 추가로 실행되는 페이지가 아닙니다.

실제 실행 코드와 ST 예제

ST 전체 코드 펼치기
(* PLC LAB - 5-floor educational virtual elevator
   IEC 61131-3 Structured Text example. Invoke every 50 ms.
   Pos and Door are a SOFTWARE PLANT, not physical drive control.
   Generic syntax: adapt declarations/conversions for the chosen PLC.
   Call[1..5], OpenPB, ClosePB, ResetPB are one-scan input pulses.
   Estop is TRUE when emergency stop is requested.
   States: 0 idle, 10 moving, 20 opening, 30 open, 40 closing, 90 fault.
   Simulation I/O map:
   I0.0 DoorClosed, I0.1 DoorOpened, I0.2 Obstacle,
   I0.3 Overload, I0.4 Estop; I1.0..I1.4 floor sensors.
   Q0.0 Up, Q0.1 Down, Q0.2 OpenMotor, Q0.3 CloseMotor.
   This is not certified or suitable for a real elevator.
   Fire extension: I0.5 FireAlarm, I0.6 MainSmoke, M0.0 FireActive.
   FireResetPB is an administrator pulse, allowed after recall completion.
   Fire states: 50 prepare, 51 close, 52 decelerate, 53 travel,
   54 open, 55 hold, 56 stationary direction-change delay.
   RecallFloor: 1 primary; 2 preselected alternate (demo assumption only).
   Fire input NEVER sets Estop. Existing protections remain effective.
   Sensor exception rules of commercial fire service are NOT replicated.
   The brake curve and 0.4 s delay are illustrative, not drive parameters.
*)
FUNCTION_BLOCK FB_Elevator5F
VAR_INPUT
  Call : ARRAY[1..5] OF BOOL;
  Estop, Overload, Obstacle : BOOL;
  OpenPB, ClosePB, ResetPB : BOOL;
  FireAlarm, MainSmoke, FireResetPB : BOOL;
END_VAR
VAR_OUTPUT
  Up, Down, OpenMotor, CloseMotor : BOOL;
  DoorClosed, DoorOpened, Zone, Fault : BOOL;
  FloorSensor : ARRAY[1..5] OF BOOL;
  Request : ARRAY[1..5] OF BOOL;
  Pos : REAL := 1.0;
  Door : REAL := 0.0;
  Floor : INT := 1;
  State : INT := 0;
  Direction : INT := 0;
  FireActive : BOOL := FALSE;
  RecallFloor : INT := 1;
END_VAR
VAR
  n, Here, Nearest : INT;
  Above, Below, Reached : BOOL;
  HoldTime : REAL := 0.0;
  Distance, Best : REAL;
  Step : REAL := 0.025; (* 0.05 s / 2.0 s per floor *)
  WasMoving, Replan : BOOL;
  BrakeStart, BrakeEnd, BrakeDuration, PhaseTime, Ratio, Delta, OldPos : REAL;
END_VAR

(* Read virtual sensor image; latch momentary calls. *)
Here := REAL_TO_INT(Pos);
Zone := ABS(Pos - INT_TO_REAL(Here)) < 0.001;
DoorClosed := Door <= 0.0;
DoorOpened := Door >= 1.0;
FOR n := 1 TO 5 DO
  FloorSensor[n] := Zone AND (Here = n);
  IF Call[n] AND NOT (FireActive OR FireAlarm OR MainSmoke) THEN
    IF Zone AND (Here = n) AND ((State = 20) OR (State = 30) OR (State = 40)) THEN
      HoldTime := 0.0;
      IF State = 40 THEN State := 20; END_IF;
    ELSE Request[n] := TRUE;
    END_IF;
  END_IF;
END_FOR;

(* Outputs default OFF every scan. Fault stays latched after release. *)
Up := FALSE; Down := FALSE; OpenMotor := FALSE; CloseMotor := FALSE;
WasMoving := (State = 10) OR (State = 52) OR (State = 53);
IF Estop OR (WasMoving AND (Overload OR NOT DoorClosed)) THEN
  Fault := TRUE; State := 90;
END_IF;
IF ResetPB AND NOT Estop AND NOT Overload AND Fault THEN
  Fault := FALSE;
  IF FireActive THEN State := 50;
  ELSIF Door > 0.0 THEN State := 20; ELSE State := 0; END_IF;
END_IF;

(* Recall latches independently from emergency stop. *)
Replan := FALSE;
IF (FireAlarm OR MainSmoke) AND NOT FireActive THEN
  FireActive := TRUE; RecallFloor := 1;
  IF MainSmoke THEN RecallFloor := 2; END_IF;
  FOR n := 1 TO 5 DO Request[n] := FALSE; END_FOR;
  Replan := TRUE;
END_IF;
IF FireActive AND MainSmoke AND (RecallFloor <> 2) THEN
  RecallFloor := 2; Replan := TRUE;
END_IF;
IF FireResetPB AND FireActive AND NOT FireAlarm AND NOT MainSmoke
  AND NOT Fault AND (State = 55) THEN
  FireActive := FALSE; RecallFloor := 1; Direction := 0;
  State := 30; HoldTime := 0.0;
END_IF;
IF Replan AND NOT Fault THEN
  Delta := INT_TO_REAL(RecallFloor) - Pos;
  IF WasMoving AND (Direction <> 0) AND (INT_TO_REAL(Direction) * Delta <= 0.0) THEN
    BrakeStart := Pos;
    IF Zone THEN BrakeEnd := INT_TO_REAL(Here);
    ELSIF Direction > 0 THEN
      FOR n := 5 TO 1 BY -1 DO
        IF INT_TO_REAL(n) > Pos THEN BrakeEnd := INT_TO_REAL(n); END_IF;
      END_FOR;
    ELSE
      FOR n := 1 TO 5 DO
        IF INT_TO_REAL(n) < Pos THEN BrakeEnd := INT_TO_REAL(n); END_IF;
      END_FOR;
    END_IF;
    BrakeDuration := MAX(0.4, ABS(BrakeEnd - BrakeStart) * 4.0);
    PhaseTime := 0.0; State := 52;
  ELSIF WasMoving AND DoorClosed AND (ABS(Delta) > 0.001) THEN
    IF Delta > 0.0 THEN Direction := 1; ELSE Direction := -1; END_IF;
    State := 53;
  ELSE State := 50;
  END_IF;
END_IF;

IF NOT Fault THEN
 IF FireActive THEN
  CASE State OF
  50:
    IF ABS(Pos - INT_TO_REAL(RecallFloor)) < 0.001 THEN
      Floor := RecallFloor; State := 54;
    ELSIF Door > 0.0 THEN State := 51;
    ELSIF NOT Overload THEN
      IF INT_TO_REAL(RecallFloor) > Pos THEN Direction := 1; ELSE Direction := -1; END_IF;
      State := 53;
    END_IF;
  51:
    IF Obstacle OR Overload THEN
      OpenMotor := Door < 1.0; Door := MIN(1.0, Door + 0.05);
    ELSE
      CloseMotor := TRUE; Door := MAX(0.0, Door - 0.05);
      IF Door <= 0.0 THEN CloseMotor := FALSE; State := 50; END_IF;
    END_IF;
  52:
    PhaseTime := PhaseTime + 0.05;
    Ratio := MIN(1.0, PhaseTime / BrakeDuration);
    Pos := BrakeStart + (BrakeEnd - BrakeStart) * (2.0 * Ratio - Ratio * Ratio);
    Up := (Ratio < 1.0) AND (BrakeEnd > BrakeStart);
    Down := (Ratio < 1.0) AND (BrakeEnd < BrakeStart);
    IF Ratio >= 1.0 THEN
      Pos := BrakeEnd; Floor := REAL_TO_INT(BrakeEnd);
      PhaseTime := 0.0; State := 56;
    END_IF;
  56:
    PhaseTime := PhaseTime + 0.05;
    IF PhaseTime >= 0.4 THEN State := 50; END_IF;
  53:
    Delta := INT_TO_REAL(RecallFloor) - Pos; OldPos := Pos;
    IF Delta > 0.0 THEN Direction := 1; ELSE Direction := -1; END_IF;
    Pos := Pos + INT_TO_REAL(Direction) * MIN(ABS(Delta), Step);
    Up := Direction = 1; Down := Direction = -1;
    FOR n := 1 TO 5 DO
      IF ((Direction = 1) AND (INT_TO_REAL(n) > OldPos) AND (INT_TO_REAL(n) <= Pos + 0.000001))
        OR ((Direction = -1) AND (INT_TO_REAL(n) < OldPos) AND (INT_TO_REAL(n) >= Pos - 0.000001))
      THEN Floor := n; END_IF;
    END_FOR;
    IF ABS(Pos - INT_TO_REAL(RecallFloor)) < 0.000001 THEN
      Pos := INT_TO_REAL(RecallFloor); Floor := RecallFloor;
      Up := FALSE; Down := FALSE; State := 54;
    END_IF;
  54:
    OpenMotor := TRUE; Door := MIN(1.0, Door + 0.05);
    IF Door >= 1.0 THEN OpenMotor := FALSE; State := 55; END_IF;
  55: (* Hold doors open, refuse ordinary calls, await administrator reset. *)
    Up := FALSE; Down := FALSE;
  ELSE State := 50;
  END_CASE;
 ELSE
  IF OpenPB AND Zone AND ((State = 0) OR (State = 30) OR (State = 40)) THEN
    State := 20; HoldTime := 0.0;
  END_IF;
  IF ClosePB AND (State = 30) AND NOT Obstacle AND NOT Overload THEN
    State := 40;
  END_IF;

  CASE State OF
  0: (* Same-floor service, otherwise retain direction while work remains. *)
    IF Zone AND Request[Here] THEN
      Request[Here] := FALSE; State := 20;
    ELSIF NOT Overload AND DoorClosed THEN
      Above := FALSE; Below := FALSE; Best := 99.0; Nearest := 0;
      FOR n := 1 TO 5 DO
        IF Request[n] THEN
          Above := Above OR (INT_TO_REAL(n) > Pos + 0.001);
          Below := Below OR (INT_TO_REAL(n) < Pos - 0.001);
          Distance := ABS(INT_TO_REAL(n) - Pos);
          IF Distance < Best THEN Best := Distance; Nearest := n; END_IF;
        END_IF;
      END_FOR;
      IF ((Direction = 1) AND Above) OR ((Direction = -1) AND Below) THEN
        State := 10;
      ELSIF Nearest <> 0 THEN
        IF INT_TO_REAL(Nearest) > Pos THEN Direction := 1; ELSE Direction := -1; END_IF;
        State := 10;
      ELSE Direction := 0;
      END_IF;
    END_IF;
  10: (* Virtual movement and floor crossing. *)
    Up := Direction = 1; Down := Direction = -1;
    FOR n := 1 TO 5 DO
      Reached := ((Direction = 1) AND (INT_TO_REAL(n) > Pos + 0.001)
        AND (INT_TO_REAL(n) <= Pos + Step + 0.001))
        OR ((Direction = -1) AND (INT_TO_REAL(n) < Pos - 0.001)
        AND (INT_TO_REAL(n) >= Pos - Step - 0.001));
      IF Reached THEN
        Floor := n;
        IF Request[n] OR (n = 1) OR (n = 5) THEN
          Pos := INT_TO_REAL(n); Request[n] := FALSE; State := 20;
          Up := FALSE; Down := FALSE;
          EXIT;
        END_IF;
      END_IF;
    END_FOR;
    IF State = 10 THEN Pos := LIMIT(1.0, Pos + INT_TO_REAL(Direction) * Step, 5.0); END_IF;
  20:
    OpenMotor := TRUE; Door := MIN(1.0, Door + 0.05);
    IF Door >= 1.0 THEN State := 30; HoldTime := 0.0; OpenMotor := FALSE; END_IF;
  30:
    IF Obstacle OR Overload THEN HoldTime := 0.0;
    ELSE HoldTime := HoldTime + 0.05;
    END_IF;
    IF HoldTime >= 2.5 THEN State := 40; END_IF;
  40:
    IF Obstacle OR Overload THEN State := 20;
    ELSE
      CloseMotor := TRUE; Door := MAX(0.0, Door - 0.05);
      IF Door <= 0.0 THEN State := 0; CloseMotor := FALSE; END_IF;
    END_IF;
  END_CASE;
 END_IF;
END_IF;
END_FUNCTION_BLOCK

브라우저 실행 기준은 controller.js입니다. ST는 같은 제어 흐름을 표현한 교육용 예제이며 스캔 내 실행 순서까지 동일한 코드 변환은 아닙니다. 제조사별 선언·입출력 매핑·안전 설계가 필요합니다.