앱 블루프린트
이 페이지에서는 플랫폼을 설계 의도대로 활용하는 IronFlock 앱을 구축하는 방법을 설명합니다. 핵심을 한 문장으로 요약하면 다음과 같습니다: 컨테이너는 모든 디바이스에서 실행되며 앱의 모든 상태를 앱의 테이블에 보관하고, 보드는 앱의 사용자 인터페이스이며, 이 둘은 서로 직접 통신하지 않습니다. 디바이스가 직접 제공하는 웹 UI는 원격 접근 터널을 통해 사용자에게 전달되어 보드 안에 표시됩니다.
각 구성 요소는 해당 페이지에서 자세히 설명합니다. 이 페이지에서는 구성 요소들이 어떻게 맞물리는지를 보여 줍니다.
앱의 구성 요소
앱은 여러 디바이스에서 동시에 실행되며, 이 디바이스는 대개 기계 옆에 설치된 산업용 PC입니다. 앱은 다음으로 구성됩니다:
| 구성 요소 | 설명 | 정의 위치 |
|---|---|---|
| 엣지 코드 | 앱이 설치된 모든 디바이스에서 실행되는 컨테이너 또는 Docker Compose 스택 | Dockerfile 또는 docker-compose.yml |
| SDK | 코드와 플랫폼을 잇는 유일한 연결 고리: 테이블, 실시간 업데이트, 디바이스 함수, 파일 | IronFlock SDK (Python, JavaScript, Go) |
| 테이블 | 프로젝트 자체 데이터베이스에 저장되는 앱의 상태 | data-template.yml |
| 보드 | 테이블에 실시간으로 바인딩되어 모든 디바이스를 아우르는 앱의 중심 사용자 인터페이스 | board-template.yml |
| 파라미터 | 개별 디바이스 또는 디바이스 그룹의 설정 | env-template.yml |
| 원격 접근 | 웹 UI처럼 컨테이너가 제공하는 포트로 연결되는 터널 | port-template.yml |
| AI 에이전트 | 디바이스 함수를 도구로 사용하는 IronFlock 어시스턴트용 에이전트 | ai-template.yml |
사용자가 프로젝트에 앱을 설치하면 IronFlock은 해당 프로젝트의 데이터베이스에 앱의 테이블을 만들고, 앱의 보드를 설치하며, 모든 디바이스에 있는 앱의 컨테이너와 테이블, 보드를 실시간으로 연결하는 비공개 메시징 영역을 엽니다. SDK는 디바이스 에이전트가 주입한 신원으로 이 영역에 참여하므로, 코드에는 주소도 자격 증명도 필요하지 않습니다.
규칙
- 테이블이 곧 앱의 상태입니다. 보드, 다른 디바이스, 재시작된 컨테이너, AI 에이전트가 필요로 하는 것은 모두 테이블에 저장됩니다. 컨테이너는 메모리에 올려 둔 설정 사본처럼 테이블에서 다시 만들 수 있는 것만 보관합니다.
- 구성 요소는 서로 직접 통신하지 않고 테이블을 통해 소통합니다. 디바이스는 측정한 값과 자신의 상태를 쓰고, 보드는 이를 표시합니다. 보드는 사용자가 결정한 내용을 쓰고, 디바이스는 이를 적용합니다. 어느 쪽도 상대를 호출하지 않으므로 디바이스가 오프라인이어도 보드는 작동하고, 아무도 보드를 열어 두지 않아도 디바이스는 계속 작동합니다.
- 테이블마다 쓰기 주체는 하나입니다. 각 테이블은 디바이스(각 디바이스가 자기 자신에 관해), 보드, 또는 하나의 서비스 중 한쪽만 씁니다. 어느 쪽인지는 테이블의
description에 밝혀 두십시오. 같은 행을 두 주체가 쓰면 서로의 변경 사항을 덮어쓰게 됩니다. - 명령이 아니라 의도한 상태를 씁니다. 디바이스의 동작을 바꾸려면 목표(한계값, 설정값, 레시피, 스케줄)를 테이블에 쓰고, 모든 디바이스가 가장 최신 행을 적용하게 하십시오. 오프라인이었던 디바이스는 다시 연결되면 이를 적용하며, 두 번 적용해도 달라지는 것은 없습니다.
- 변경될 때 쓰고, 실시간으로 따라가며, 절대 폴링하지 마십시오. 엔티티의 행은 값 중 하나가 바뀔 때 쓰고, 측정값은 샘플링 주기에 맞춰 쓰십시오. 읽는 쪽은 모두 테이블을 실시간으로 따라갑니다: 보드의 바인딩, 디바이스의 테이블 구독, 디바이스 웹 UI의 WebSocket이 그렇습니다. 타이머로 테이블을 반복해서 읽는 것은 언제나 잘못된 설계입니다.
- UI는 보드이며, 디바이스의 웹 UI는 이를 보완합니다. 디바이스에서 웹 페이지를 제공하는 것은 보드가 보여 줄 수 없는 것에 한정하십시오: 기계 자체의 HMI, 영상, 빠르게 갱신되는 라이브 뷰, 기계 옆의 운영자 화면 등입니다. 보드는 이를 임베드합니다.
데이터 종류별 저장 위치
| 데이터 | 위치 | 쓰는 쪽 | 읽는 쪽 |
|---|---|---|---|
| 시간에 따른 판독값 | tsp가 있는 테이블 | publishToTable, publishRowsToTable | 시간 창과 집계를 사용하는 차트 |
| 디바이스, 기계, 주문의 현재 상태 | 엔티티 테이블(maintainLatestFlagFor) | appendToTable: 키, tsp, 변경된 컬럼을 담은 행 | latest 필터 |
| 사용자가 설정하는 한계값, 설정값, 레시피 | 보드의 양식이 쓰는 테이블 | 양식 | 모든 디바이스(아래 참조) |
| 사용자가 생성, 편집, 삭제하는 레코드 | deleted 컬럼이 있는 엔티티 테이블 | 삭제 시 플래그를 설정하는 양식 | 보드의 목록 및 편집 페이지, latest 필터와 deleted에 대한 필터로 라이브; 아래 참조 |
| 운영자를 위한 오류 및 알림 | error-logs 테이블 | userMessage를 포함한 reportError | 보드의 토스트 위젯 |
| 이미지, 문서, 녹화 파일 | 앱의 파일 저장소 | files.put | 컬럼에 담긴 파일 URL을 위젯이 표시 |
| 비밀번호 및 API 키 | secret: true 컬럼 | 모든 테이블 쓰기 | revealSecrets, verifySecret(앱 자체 컨테이너에서만) |
| 개별 디바이스의 설치 정보: 기계 주소, 시리얼 포트 | 앱 파라미터 | 디바이스 또는 그룹의 앱 설정 | 환경 변수 |
| 나중에 아무도 필요로 하지 않는 구성 요소 간 메시지 | 자체 토픽 | publish | subscribe |
| 특정 디바이스의 즉각적인 응답이 필요한 요청 | 디바이스 함수 | registerDeviceFunction | callDeviceFunction, 위젯 액션, AI 도구 |
추가인가, 발행인가? SDK 1.10.0부터는 둘이 같습니다: publishToTable도 추가하므로, 둘 다 재시작 중인 데이터 백엔드를 기다리며, 데이터 백엔드가 행을 거부하면 이를 알려 줍니다. 고빈도 판독값은 publishRowsToTable로 묶어 배치로 보내십시오.
파라미터인가, 설정 테이블인가? 파라미터는 디바이스 또는 디바이스 그룹에 속합니다: 디바이스를 설치하거나 구성하는 사람이 앱 설정에서 값을 정하고, 코드는 환경에서 이를 읽습니다. 설치마다 달라지는 값에는 파라미터를 사용하십시오. 앱이 실행되는 동안 사용자가 바꾸는 것 — 한계값, 레시피, 스케줄, 그리고 여러 디바이스에 적용되는 모든 것 — 은 보드가 쓰는 테이블에 두어야 합니다. 보드는 사용자가 앱을 다루는 곳이고, 모든 디바이스가 변경 사항을 즉시 따르며, 테이블에는 누가 언제 무엇을 바꾸었는지 이력이 남습니다.
엔티티 테이블: 현재 상태, 업데이트, 삭제
엔티티 테이블(maintainLatestFlagFor)은 그 엔티티들의 이력입니다: 모든 변경은 키, tsp, 변경된 컬럼을 담은 새 행입니다. 어떤 것도 행을 편집하거나 삭제하지 않으며, 그럴 필요도 없습니다. 읽는 쪽 모두가 읽는 시점에 현재 상태를 고르기 때문입니다:
- 현재 상태는 뷰가 아니라 읽기 모드입니다.
latest필터는 읽기가 실행되는 시점에tsp로 고른 엔티티별 최신 행을 반환합니다: 위젯 필터 설정의 latest 토글,getHistory의{"latest": true}. 테이블은 모든 버전을 보존하고, 필터는 엔티티별로 하나를 보여줍니다. - 라이브입니다. latest 필터로 테이블에 바인딩된 위젯은 모든 새 행을 도착하는 즉시 받아 엔티티의 행을 그것으로 교체합니다. 변경 사항은 새로 고치거나 일정을 잡을 것 없이 1초 안에 표시됩니다.
- 삭제는 컬럼입니다. 테이블에 boolean
deleted컬럼을 두십시오. 보드의 양식은 Delete-Flag Column 설정을 통해 이 컬럼을 씁니다: 삭제 시true, 저장할 때마다false를 쓰므로 삭제된 레코드를 저장하면 복원됩니다. 레코드를 보여주는 위젯은 이 컬럼으로 필터링합니다. latest 필터는 먼저 엔티티별 최신 행을 고른 다음 나머지 조건을 적용하므로, 최신 행이 삭제된 엔티티는 그 행이 도착하는 즉시 위젯에서 빠지고, 더 오래된 삭제되지 않은 행이 그 자리를 차지하는 일은 없습니다. - 이를 위해 변환을 만들지 마십시오. 엔티티별 최신 행을 고르거나 삭제된 행을 숨기는 변환은 두 필터가 하는 일을 재현하지만, 자신의 일정에 따라서만 보드에 도달하므로 모든 변경이 늦게 표시됩니다. 변환은 읽기로 표현할 수 없는 것을 위한 것입니다: 조인, 산술, 현재 행에 대한 집계. 변환에 엔티티의 현재 상태가 필요하다면 그 SQL에서
DISTINCT ON (<key>) ... ORDER BY <key>, tsp DESC로 선택하고deleted를 그곳에서 필터링하십시오. - 코드도 같은 방식으로 읽습니다. latest 필터와
deleted조건을 사용한getHistory는 첫 번째 앱 구축에서처럼 현재의 삭제되지 않은 엔티티를 돌려주고, 테이블 구독은 새 행을 하나씩 전달하므로 코드는 이를 엔티티의 사본에 적용하거나, 행이deleted라고 하면 엔티티를 버립니다.
자세한 내용은 엔티티의 최신 상태 추적과 레코드 소프트 삭제에 있습니다.
보드가 쓰는 내용 따라가기
디바이스는 보드에서 설정한 한계값처럼 보드가 쓴 상태를 다음 세 단계로 적용합니다:
- 테이블을 읽기 전에 먼저 구독하여 읽기와 구독 사이에 빠지는 행이 없게 하십시오.
- 시작할 때
latest필터로 가장 최신 행을 한 번 읽어 적용하십시오. - 현재 적용 중인 행보다 새로운 행만 적용하십시오(
tsp기준). 프로그램이 시작되는 바로 그 순간에 저장된 행은 읽기와 구독 양쪽으로 도착할 수 있습니다.
Python
await ironflock.subscribe_to_table("settings", lambda row, *args, **kwargs: apply(row))
rows = await ironflock.getHistory("settings", {"limit": 1, "filterAnd": [{"latest": True}]})
for row in rows or []:
apply(row)이것이 전부이며, 오프라인이 되는 디바이스에도 그대로 적용됩니다. 연결은 SDK가 처리하므로 코드에서 다룰 필요가 없습니다: SDK는 재연결하고, 구독과 디바이스 함수를 복원한 다음, 디바이스가 오프라인이던 동안 저장된 행을 오래된 것부터 각 테이블 구독에 전달하며, 실시간으로 도착하는 행은 그 뒤에 이어집니다. 디바이스가 오프라인일 때 저장된 한계값은 디바이스가 돌아오는 즉시 전달됩니다. 따라잡기는 tsp를 기준으로 합니다: 핸들러가 받은 가장 최신 행보다 새로운 행을 전달하되, 가장 최신 10,000행까지만 전달합니다. 따라잡기 읽기는 다른 이력 읽기와 마찬가지로 최대 재연결 대기 시간만큼 데이터 백엔드를 기다리며, 읽기가 실패하면 해당 행은 전달되지 않고 경고가 기록됩니다. 따라잡기는 SDK 1.10.0 버전부터 포함되어 있습니다.
명령, 디바이스 함수, 토픽
상태를 우선하십시오. 사용자가 “명령”하는 것의 대부분은 의도한 상태입니다: 설정값, 켜짐 또는 꺼짐, 레시피 같은 것입니다. 위에서처럼 이를 테이블에 쓰십시오.
라벨 인쇄나 세척 사이클 시작처럼 한 번만 실행되어야 하는 명령은 보드가 쓰는 테이블에 id를 가진 행으로 기록합니다. 디바이스는 실행한 id를 자체 테이블에 기록하므로, 같은 명령을 두 번 받아도 한 번만 실행됩니다.
디바이스 함수는 디바이스가 registerDeviceFunction으로 등록하는 프로시저입니다. 호출하는 쪽이 디바이스의 응답을 바로 받아야 할 때 사용하십시오:
- AI 에이전트:
ai-template.yml의 도구가 어시스턴트가 선택한 디바이스에서 함수를 호출합니다. AI 에이전트 및 도구를 참조하십시오. - 보드의 스위치 위젯과 SCADA 위젯은 디바이스가 온라인일 때 이를 호출합니다.
- 자체 컨테이너와 디바이스는
callDeviceFunction으로 서로를 호출합니다.
디바이스 함수에서는 읽기 쉬운 텍스트나 작은 JSON을 반환하고, 예상된 실패(전원이 꺼진 기계 등)는 오류를 발생시키는 대신 결과로 반환하십시오.
토픽은 나중에 아무도 필요로 하지 않는 구성 요소 간 메시지를 전달합니다: myapp.events처럼 자체 네임스페이스에 있는 이름에 publish와 subscribe를 사용합니다. 보드는 토픽을 구독할 수 없습니다: 보드가 표시하는 것은 모두 테이블에 있어야 합니다. 디바이스가 오프라인인 동안 발행된 메시지는 저장되지 않으므로, 반드시 도착해야 하는 것에는 테이블을 사용하십시오.
디바이스의 컨테이너
디바이스 에이전트가 컨테이너에 제공하는 것과 컨테이너에 기대하는 것은 다음과 같습니다:
- 신원: 에이전트는
DEVICE_KEY,DEVICE_NAME,DEVICE_SERIAL_NUMBER,SWARM_KEY,APP_KEY,APP_NAME,ENV(DEV또는PROD),TUNNEL_DOMAIN,DEVICE_LAN_IP,DEVICE_PORT_FOR_<port>와 함께 SDK가 사용할 앱 자체의 자격 증명을 설정합니다. SDK는 필요한 값을 스스로 읽습니다. 이 값들은 절대 하드코딩하지 마십시오. - 파라미터: 프로덕션 설치는 사용자가 설정한 값을 받고, 나머지 파라미터는 각자의
defaultValue를 받습니다. 개발 실행은 기본값만 받습니다. 파라미터에는 구체적인 이름을 붙이십시오(HOST가 아니라PLC_HOST). - 저장소: 단일 이미지 앱은 앱 전용이며 재시작과 업데이트 후에도 유지되는(앱과 함께 삭제됨)
/data와, 디바이스의 모든 앱이 공유하는/shared를 받습니다. 그래도 앱의 상태는 테이블에 보관하고, 이 폴더들은 다운로드한 모델이나 캐시처럼 디바이스에 속하는 파일에 사용하십시오. - 하드웨어: 단일 이미지 컨테이너는 Docker의 기본 권한으로 실행되며,
/dev도 추가 기능도 없습니다. 앱에 하드웨어가 필요하면 특권 접근을 선언하십시오. 사용자가 허용하면 컨테이너는 디바이스의/dev에 접근할 수 있는 특권 모드로 실행됩니다. - 로그: 프로그램이 stdout과 stderr에 쓰는 모든 내용이 IronFlock에서 앱의 로그가 됩니다. 버퍼링 없이 출력하십시오(
python -u). - 시작: 컨테이너는 시작하고 30초가 지난 시점에 실행 중이어야 하며, 그렇지 않으면 시작이 실패한 것으로 간주됩니다.
- 크래시: 프로덕션 앱이 종료되면 에이전트가 앱을 다시 시작하며, 실패할 때마다 더 오래 기다립니다. 개발 실행은 다시 시작되지 않습니다. 응답하지 않는 센서처럼 예상되는 실패는 프로그램 안에서 처리하고, 복구할 수 없는 오류가 발생하면 프로그램이 종료되도록 두십시오.
- 중지: 프로그램은 컨테이너의 메인 프로세스(PID 1)이며, PID 1은 직접 처리하지 않는 한 중지 신호 SIGTERM을 무시합니다. SIGTERM을 받으면 종료하십시오. 그렇지 않으면 앱을 중지할 때 Docker가 프로세스를 강제로 종료할 때까지 기다려야 합니다. 프로그램은 셸을 거치지 말고 exec 형식(
CMD ["python", "-u", "main.py"])으로 시작하십시오. - 개발 실행: 클라우드 IDE에서 앱을 실행하면 개발 실행이 됩니다(
ENV가DEV). 개발 실행은 앱의 개발용 테이블에 쓰고 파라미터의 기본값을 받습니다. 개발 실행의http및https포트는 터널을 받으며, 이 터널은 디바이스에 대한 개발 권한이 있는 사용자만 IDE의 사이드 메뉴에서 열 수 있습니다.tcp및udp포트도 터널을 받으며, 이 터널은 프로덕션과 마찬가지로 주소를 아는 누구에게나 열려 있고, 터널이 올라오면 SDK가 그 주소를 반환합니다.
Docker Compose
앱이 여러 프로세스를 실행하거나, 자체 코드 외에 MQTT 브로커나 데이터베이스 같은 서드파티 서비스를 함께 묶는 경우 docker-compose.yml을 사용하십시오. 기본 사항은 Docker 및 Compose를 참조하십시오. 단일 이미지와 비교하면 다음이 다릅니다:
- 모든 서비스가 주입된 환경을 받습니다. 에이전트는 위의 변수를 담은 별도의 환경 파일을 서비스마다 제공하며, 직접 작성한
env_file:은 대체됩니다. Compose 파일의${VAR}은 주입된 값으로 해석됩니다. /data가 없습니다. 서비스는 앱 폴더를 받지 않습니다. 파일은 명명된 볼륨에 보관하십시오: 명명된 볼륨은 중지와 업데이트 후에도 유지되며, 디바이스에서 앱을 제거하면 삭제됩니다.- 게시된 포트는 다시 매핑됩니다. 게시된 각 포트는 디바이스의 모든 인터페이스에서 비어 있는 호스트 포트를 받으며,
DEVICE_PORT_FOR_<port>가 코드에 실제 포트를 알려 줍니다.port-template.yml에서는 Compose 파일에 적힌 그대로의 호스트 포트를 사용하십시오. 사용자가 접근해야 하는 포트만 게시하고, 서비스끼리는 Compose 네트워크에서 서비스 이름으로 통신하게 하십시오.${VAR}로 작성한 호스트 포트는 관리되지 않습니다: 다시 매핑되지도 않고 터널도 생기지 않습니다. - 특권 설정에는 사용자의 허용이 필요합니다. 에이전트는 특권도 장치도 추가하지 않으며, 서비스는 파일에 선언된 것만 받습니다.
privileged,cap_add,devices,network_mode: host또는 호스트 경로의 바인드 마운트 같은 설정에는data-template.yml에 특권 접근 선언이 필요하며, 사용자가 허용하지 않으면 에이전트는 앱을 시작하기 전에 이를 제거합니다. 앱에 필요한 것만 요청하십시오.privileged: true대신devices: [/dev/ttyUSB0]를 사용합니다.devices:항목은 반드시 존재해야 하는 하드웨어를 지정하며, 해당 하드웨어가 없으면 컨테이너가 시작되지 않습니다.device_cgroup_rules를/dev볼륨과 함께 사용하면 모든 디바이스에서 작동합니다. profiles:는 사용할 수 없습니다. profiles가 지정된 서비스는 릴리스에서 제외됩니다.- 시작 규칙은 동일합니다. 30초 안에 모든 컨테이너가 실행 중이거나 종료 코드 0으로 종료된 상태여야 하며, 적어도 하나는 실행 중이어야 합니다. 장시간 실행되는 서비스에는
restart: unless-stopped를 지정하십시오.
디바이스의 웹 UI
컨테이너의 웹 서버는 해당 포트의 원격 접근 터널을 통해 어디서나 접근할 수 있게 되며, 보드가 이를 표시합니다:
- 고정 포트에서 제공하고(
0.0.0.0에 바인딩),port-template.yml에protocol: http로 선언하십시오. 프로덕션 설치는 터널을 켜고 WebSocket을 포함해 HTTPS로 제공합니다. - 터널의 URL을 테이블에 쓰십시오. SDK의
getRemoteAccessUrlForPort(port)가 URL을 알려 줍니다. 시작할 때 이 URL을 디바이스 테이블에서 해당 디바이스의 행에 쓰십시오. 개발 실행은 레이블이-dev로 끝나는 자체 터널의 URL을 받아 앱의 개발용 테이블에 쓰며, 이 URL은 디바이스에 대한 개발 권한이 있는 사용자만 열 수 있습니다. - 보드에 임베드하십시오. 디바이스로 필터링하는 디바이스별 페이지에서 Embed 위젯의 URL을 그 컬럼에 바인딩하십시오. UI Builder에서는 이것도 다른 데이터 바인딩과 다르지 않습니다.
페이지 자체에 관해서는 다음과 같습니다:
- 열 수 있는 사람: http 터널은 비공개입니다. 사용자가 포트를 공개로 전환하지 않는 한, IronFlock에 로그인하고 디바이스에 대한 접근 권한이 있는 사람만 열 수 있습니다. 디바이스가 속한 네트워크에서는 페이지가 누구에게나 열려 있으므로, 공장 네트워크에서 보여서는 안 되는 내용은 표시하지 말고, 기계 상태를 바꾸는 모든 작업에는 자체 로그인을 요구하십시오.
- 상대 URL: 에셋 로드, API 호출, WebSocket 연결을 모두 페이지 기준 상대 경로로 처리하십시오. 그래야 디바이스 네트워크에서도, 터널을 통해서도, 리버스 프록시 뒤에서도 작동합니다.
- 폴링 대신 푸시: 업데이트는 WebSocket으로 보내십시오.
- 브라우저에 플랫폼 자격 증명을 두지 않기: 페이지는 컨테이너에 있는 자체 서버와 통신하고, SDK 연결은 그 서버가 유지합니다.
직접 만든 페이지 대신 공장 네트워크에 있는 기계의 웹 HMI를 보여 주려면, 컨테이너에서 해당 HMI를 리버스 프록시하십시오. 외부 HMI 임베드를 참조하십시오.
여러 디바이스
- 모든 행에 디바이스가 기록됩니다: IronFlock은 행을 쓴 디바이스의 키를
device_key에 저장합니다. 디바이스별 테이블은device_key를 키로 삼고, 디바이스 이름은path: kwargs.DEVICE_NAME으로 컬럼에 저장하며, 차트는 그 이름으로 분할하십시오. - 디바이스 목록과 디바이스별 페이지: 디바이스 테이블로 만든 사이드 내비게이션과, 모든 디바이스에 쓸 수 있으며 라우트의 디바이스로 위젯을 필터링하는 페이지 하나를 사용합니다. 보드 구축을 참조하십시오.
- 디바이스별 또는 기계별 설정: 설정 테이블은 대상을 가리키는 컬럼을 키로 삼고(보드가 쓰는 행에는 자체 디바이스가 없습니다), 대상 페이지의 양식에서 그 컬럼을 채우십시오. 각 디바이스는 자신의 키에 해당하는 가장 최신 행을 적용합니다.
- 떠난 디바이스의 행은 그대로 남습니다. 사용자가 목록에서 디바이스를 제거할 수 있어야 한다면, 디바이스 테이블에
deleted컬럼을 추가하고 보드에는 이를 설정하는 양식을 추가하십시오.
체크리스트
- 모든 상태가 테이블에 있고, 각 테이블의 설명에 하나뿐인 쓰기 주체가 명시되어 있으며, 코드가 쓰는 모든 컬럼이
data-template.yml에 선언되어 있습니다. - 모든 행에
tsp가 있고, 상태 행은 변경될 때 키와 변경된 컬럼만 담아 씁니다. 엔티티 테이블은latest필터와deleted에 대한 필터로 읽으며, 중복을 제거하거나 정리하는 변환을 거쳐 읽지 않습니다. - 보드가 쓴 상태를 SDK 1.10.0 이상에서 위의 세 단계로 따라가며, 코드는 연결이나 세션을 전혀 다루지 않습니다.
- 장시간 실행되는 루프의 테이블 쓰기는 플랫폼에 1분 동안 연결할 수 없을 때 쓰기가 발생시키는 오류를 처리하며, 예상된 실패는
reportError와userMessage로 보고합니다. - 프로그램은 SIGTERM을 받으면 종료하고, exec 형식으로 시작하며, 버퍼링 없이 로그를 출력합니다.
- 아무것도 폴링하지 않습니다: 보드는 바인딩하고, 디바이스는 구독하며, 웹 UI는 푸시합니다.
- 앱이 제공하는 각 포트가
port-template.yml에 선언되어 있고, 그 URL은getRemoteAccessUrlForPort로 얻어 테이블에 쓰며, 보드는 그 테이블에서 URL을 가져와 임베드합니다. - Compose 앱에는
env_file:,profiles:,${VAR}호스트 포트가 없고, 파일은 명명된 볼륨에 보관하며, 사용자에게 필요한 포트만 게시합니다.
스타터 앱으로 시작하기
Claude Code용 IronFlock 플러그인에는 이 블루프린트를 따르는 스타터 앱이 포함되어 있습니다: 모든 디바이스에서 기계의 데이터를 읽고, 각 디바이스의 상태와 판독값을 테이블에 보관하며, 사용자가 보드에서 설정한 한계값을 적용하고, 보드가 각 디바이스 페이지에 임베드하는 라이브 뷰를 제공하는 프로그램입니다. Claude Code는 이 앱을 바탕으로 새 앱을 만들고 사용 사례에 맞게 조정합니다.