Skip to Content
IoT 앱 개발데이터 백엔드

데이터 백엔드

IronFlock은 TimescaleDB를 기반으로 각 프로젝트에 전용 데이터베이스를 프로비저닝합니다. 앱이 데이터 스키마를 정의하면 IronFlock이 테이블을 생성하고 디바이스가 앱에 추가되는 순간부터 데이터 수집을 시작합니다.

작동 방식

  1. .ironflock/data-template.yml에 데이터 스키마를 정의합니다.
  2. IronFlock SDK를 사용하여 엣지 코드에서 데이터를 발행합니다.
  3. IronFlock이 앱이 설치된 각 프로젝트에서 자동으로 데이터베이스 테이블을 설정합니다.
  4. 데이터는 디바이스에서 메시징 시스템을 통해 프로젝트 데이터베이스로 흐릅니다.

각 프로젝트는 자체 물리적 데이터베이스를 가집니다 — 프로젝트 간 데이터 공유는 없습니다.

사용자는 자신의 프로젝트에서 앱이 수집하는 데이터를 완전히 제어합니다. 개발자는 이 데이터에 접근할 수 없습니다.

데이터 스키마 정의

.ironflock/ 디렉터리에 data-template.yml 파일을 생성합니다:

data: tables: - tablename: sensordata columns: - id: tsp name: 타임스탬프 description: 측정 타임스탬프 path: args[0].timestamp dataType: timestamp - id: temperature name: 온도 description: 섭씨 온도 측정값 path: args[0].temperature dataType: numeric - id: humidity name: 습도 description: 상대 습도 백분율 path: args[0].humidity dataType: numeric - id: device_id name: 디바이스 ID description: 소스 디바이스 식별자 path: args[0].device_id dataType: string

컬럼 옵션

필드설명
id내부 컬럼 식별자 (타임스탬프 컬럼에는 tsp 사용)
name보드에 표시되는 사람이 읽을 수 있는 컬럼 이름
description선택적 설명
path발행된 데이터 객체에서 값의 경로 (예: args[0].temperature)
dataTypetimestamp, numeric, string, boolean 중 하나
secret이 컬럼을 저장 시 암호화하고 일반 읽기에서는 절대 평문으로 반환하지 않습니다 — 아래 비밀 컬럼 참고

비밀 컬럼

저장은 해야 하지만 절대 표시되어서는 안 되는 값이 있습니다. API 토큰, 디바이스 비밀번호, 라이선스 키 같은 것입니다. 컬럼을 secret: true로 표시하면 데이터 백엔드가 삽입 시점에 이를 암호화합니다:

- tablename: credentials columns: - id: tsp dataType: timestamp - id: device_id dataType: string - id: api_token dataType: string secret: true

그 이후로는 어떤 일반 읽기도 평문을 반환하지 않습니다 — 그 값을 작성한 앱에게조차 반환하지 않습니다:

읽기 경로돌려받는 값
이력 읽기와 실시간 행: 보드, 위젯, SDK, 그리고 테이블을 읽는 다른 앱이 받는 값플레이스홀더 __secret__
SQL (SQL 쿼리 콘솔, 데이터 내보내기, FleetDB Access의 Postgres 로그인) 및 데이터베이스 백업저장된 암호문, ifsec:1:…
SDK의 평문 조회 함수복호화된 값

NULL은 모든 경로에서 NULL 그대로 유지되므로, “설정되었지만 숨겨짐”과 “한 번도 작성되지 않음”은 계속 구분됩니다.

비밀 값을 평문으로 다시 읽는 것은 오직 앱 자신의 컨테이너에서 SDK를 통해서만 가능합니다 — SDK 참조의 비밀 컬럼을 참조하십시오. 보드나 다른 앱은 이를 전혀 얻을 수 없으며, 바로 그 점이 “평문으로 절대 표시되지 않음”을 겉치레가 아닌 사실로 만듭니다.

비밀 값을 잃지 않고 행 업데이트하기. 엔티티 테이블에서 행을 편집하는 클라이언트는 실제 비밀 값을 되돌려 보낼 수가 없습니다 — 읽기는 언제나 플레이스홀더만 돌려주었기 때문입니다. 그래서 데이터 백엔드는 쓰기 시점에 플레이스홀더 __secret__(그리고 보드가 표시하는 •••••••• 마스크)를 이전 값 유지로 취급하며, 비밀 컬럼을 생략해도 마찬가지로 유지됩니다. 명시적으로 null을 보내면 비밀 값이 비워집니다. 따라서 보드 양식에서 기계의 설명을 편집해도 그 접근 코드가 지워지는 일은 결코 없습니다.

여기서 두 가지 결과가 따라 나옵니다: 문자열 리터럴 __secret__와 •••••••• 자체는 비밀 값으로 저장할 수 없고(원시 ifsec:… 암호문도 입력으로 거부됩니다), 엔티티 키가 없는 테이블에서는 플레이스홀더 쓰기가 거부됩니다 — 유지할 이전 행이 없기 때문입니다.

미리 고려해 두어야 할 세 가지 결과가 있습니다. 권고 사항이 아니라 넘어설 수 없는 제한이기 때문입니다:

  • 비밀 컬럼은 문자열만 가능합니다. 암호화의 산출물은 텍스트이므로 numeric, boolean, timestamp 컬럼은 비밀로 지정할 수 없습니다. 필수 컬럼인 tsp 역시 비밀로 지정할 수 없습니다.
  • 비밀 컬럼으로는 필터링, 그룹화, 정렬을 할 수 없습니다. 모든 행이 각자의 무작위 값으로 암호화되므로, 동일한 비밀 값을 담은 두 행도 서로 다른 암호문으로 저장됩니다. 비밀 컬럼에 대한 동등 필터, GROUP BY, ORDER BY, DISTINCT는 애초에 동작할 수 없습니다. “이 값이 일치합니까?”에 답하려면 SDK의 검증 함수를 사용하십시오. 이 함수는 무언가를 반환하는 대신 데이터 백엔드 내부에서 비교를 수행합니다.
  • 비밀 컬럼은 엔티티 키가 될 수 없습니다. 행마다 암호문이 다르므로 엔티티별 최신 조회는 모든 행을 제각기 다른 엔티티로 취급하게 됩니다. 그래서 비밀 컬럼을 maintainLatestFlagFor에 지정하면 그대로 거부됩니다.

뒤의 두 제한은 데이터 템플릿이 검증될 때 확인되므로, 이를 어기는 테이블은 나중에 오작동하는 것이 아니라 릴리스 시점에 실패합니다.

테이블 옵션

columns 외에도 테이블을 어떻게 설명하고 데이터를 얼마나 오래 보관할지 제어하는 몇 가지 선택적 키가 있습니다.

data: tables: - tablename: sensordata description: 생산 현장의 환경 측정값 chunkTimeInterval: 1 hour dropAfter: 30 days columns: # ...
필드설명
tablename테이블 이름
description선택적 설명. UI에 표시되며 AI 에이전트가 테이블을 이해하는 데 사용됩니다
chunkTimeInterval테이블이 나뉘는 시간 파티션의 크기. 기본값은 7 days
dropAfter보존 기간 — 이보다 오래된 파티션은 자동으로 삭제됩니다
downsample긴 시간 창 차트를 빠르게 처리하기 위해 사전 집계된 사본을 유지합니다 — 아래 연속 다운샘플링 참고
maintainLatestFlagFor고유 엔티티를 식별하는 컬럼 — 아래 엔티티의 최신 상태 추적 참고
private이 테이블을 다른 앱에서 숨깁니다 — 아래 다른 앱과 데이터 공유하기 참고

chunkTimeInterval은 시계열 데이터를 디스크에서 어떻게 분할할지 결정합니다. 파티션 하나가 한 번에 조회하는 데이터량과 대략 일치하도록 선택하세요. 매초 수집되는 고빈도 데이터는 작은 청크(분~시간 단위)가, 천천히 변하는 데이터는 큰 청크(주 단위)가 적합합니다. 이는 앱의 기본값일 뿐이며, 프로젝트 소유자가 나중에 자신의 데이터 백엔드에서 조정할 수 있습니다.

dropAfter는 테이블을 롤링 윈도우로 만듭니다. 지정한 기간보다 오래된 파티션이 통째로 삭제되므로 개별 행을 지우는 것보다 훨씬 저렴합니다. 정리 작업은 dropAfter / 4 주기로 실행되므로, 레코드는 파티션이 제거되기 전까지 만료 시점을 최대 해당 기간의 1/4만큼 넘겨 남아 있을 수 있습니다. 데이터를 무기한 보관하려면 dropAfter를 생략하세요.

두 값 모두 PostgreSQL 인터벌 문자열을 받습니다 — 30 minutes, 1 hour, 7 days, 6 months.

연속 다운샘플링

대시보드는 데이터베이스에 데이터 집계를 요청할 수 있습니다 — 시간별 평균, 일별 합계, 기계별 개수 등. 이를 원시 레코드에서 계산하는 것은 하루치라면 괜찮지만 1년치라면 비용이 큽니다. 테이블에 downsample을 추가하면 플랫폼이 지속적으로 갱신되는 사전 집계된 사본을 유지하고, 긴 시간 창 쿼리를 원시 데이터 대신 그 사본으로 응답합니다:

data: tables: - tablename: sensordata dropAfter: 30 days downsample: bucket: 1 minute keepFor: 2 years paths: - payload.temperature columns: # ...
필드설명
bucket사전 집계된 사본의 세분성. 기본값은 1 minute
keepFor다운샘플링된 이력을 보관할 기간. 생략하면 무기한 보관
paths포함할 JSON 필드 경로. 대시보드에서 사용하는 것과 동일한 표기법

bucket은 차트에 제공할 수 있는 가장 미세한 해상도입니다 — 이보다 훨씬 미세한 버킷을 요청하는 차트는 대신 원시 테이블을 읽습니다. 하루를 균등하게 나누는 1 second부터 1 day까지의 고정 폭 인터벌(1 minute, 5 minutes, 1 hour)을 받습니다. 기본값인 1 minute은 사실상 모든 대시보드에 적합하며, 버킷을 더 크게 잡으면 스토리지와 쓰기 처리량이 줄어듭니다.

keepFor는 긴 이력을 애초에 가능하게 해주는 값입니다. 원시 레코드는 dropAfter에 따라 사라지지만 다운샘플링된 사본은 자체 보존 기간을 가집니다. 원시 데이터를 30일, 다운샘플링된 데이터를 2년 보관하면, 보드는 스토리지의 일부만 사용하고도 2년치 시간별 평균을 계속 차트로 보여줄 수 있습니다. dropAfter보다 길게 설정하세요 — 그 반대는 플랫폼이 잘못된 구성으로 간주하여 거부합니다.

paths는 다운샘플링을 JSON 컬럼 내부의 값까지 확장합니다. 숫자 컬럼은 자동으로 포함되지만, JSON 컬럼에는 고정된 키 집합이 없으므로 JSON 필드는 명시적으로 지정해야 합니다. 선언하지 않은 필드도 대시보드에서 그대로 작동합니다 — 단지 원시 테이블에서 계산될 뿐입니다.

그 밖의 모든 것은 자동입니다. 모든 숫자 컬럼에 대해 통계(평균, 합계, 최솟값, 최댓값, 첫 번째 값, 마지막 값, 레코드 수)가 유지되며, 테이블의 엔티티 키(maintainLatestFlagFor, 없으면 발행 디바이스)로 그룹화됩니다. 대시보드는 아무런 설정도 필요 없고 이 기능을 인지할 필요도 없습니다: 위젯은 평소처럼 쿼리하고, 플랫폼이 쿼리마다 사전 집계된 사본으로 응답할 수 있는지 판단합니다 — 예를 들어 사본이 그룹으로 묶지 않는 컬럼을 필터가 참조하는 등 응답할 수 없는 경우에는 투명하게 원시 테이블로 되돌아갑니다.

스키마 변경은 사본을 재구축합니다. 다운샘플링된 테이블의 컬럼을 추가·제거하거나 타입을 바꾸는 것 — 또는 downsample 블록 자체를 수정하는 것 — 은 원시 테이블에서 사전 집계된 사본을 재구축합니다. dropAfter보다 오래된 것은 재구성할 수 없으므로 소실됩니다. 가능하다면 테이블을 만들 때 이 블록도 함께 설정하고, 오래 운영된 테이블에 대한 이후의 스키마 변경은 신중한 결정으로 다루십시오.

엣지 코드에서 데이터 발행

IronFlock SDK를 사용하여 앱에서 데이터를 전송합니다:

from ironflock import IronFlock flock = IronFlock() await flock.start() await flock.publish_to_table("sensordata", { "timestamp": "2025-01-15T10:30:00Z", "temperature": 23.5, "humidity": 62.1, "device_id": "sensor-001" })

모든 행에는 필수 컬럼인 tsp의 값이 필요합니다. 데이터 백엔드는 이 값이 없는 행을 거부합니다. SDK의 테이블 쓰기는 오류로 실패하고, 보드가 발행한 행은 버려지며 앱의 error-logs 테이블에 항목 하나만 남습니다. 위의 sensordata 정의는 path: args[0].timestamp로 이 컬럼을 매핑하므로 tsp는 행의 timestamp 필드에서 가져옵니다. path가 없는 컬럼은 행에서 같은 이름의 필드(args[0].<id>)를 읽습니다. 이 매핑이 없다면 각 행에 자체 tsp 필드가 있어야 합니다.

고빈도 데이터의 경우, 행마다 한 번씩 왕복하는 대신 publish_rows_to_table / publishRowsToTable / PublishRowsToTable 또는 append_rows_to_table / appendRowsToTable / AppendRowsToTable을 사용하여 단일 메시지로 여러 행을 전송하십시오 — 두 방식은 동일하며 삽입 결과를 보고합니다. 각 배치는 원자적으로 삽입됩니다 — 전부 아니면 전무입니다. 자세한 내용은 SDK 참조를 참조하십시오.

변환 테이블

원시 데이터를 자동으로 집계하거나 처리하는 SQL 변환을 정의할 수 있습니다:

data: tables: - tablename: sensordata columns: # ... 원시 데이터 컬럼 ... transforms: - tablename: hourly_averages materialize: true schedule: "0 * * * *" sql: > SELECT time_bucket('1 hour', tsp) AS hour, avg(temperature) AS avg_temp, avg(humidity) AS avg_humidity FROM sensordata GROUP BY hour columns: - id: hour name: 시간 dataType: timestamp - id: avg_temp name: 평균 온도 dataType: numeric - id: avg_humidity name: 평균 습도 dataType: numeric
필드설명
tablename파생 테이블의 이름
materializetrue이면 결과가 저장되고 일정에 따라 갱신됨. false이면 변환은 뷰가 되어 읽을 때마다 계산됨
schedule변환이 실행되는 크론 표현식: 필드 5개(분, 시, 일, 월, 요일), 또는 맨 앞에 초 필드를 더한 6개. */5 * * * *는 5분마다, */10 * * * * *는 10초마다 실행됨. 실행할 때마다 머티리얼라이즈드 결과가 갱신되고, 변환에 바인딩된 보드에 현재 결과가 전송됨. 앱의 코드가 이러한 실행을 요청 시 직접 수행할 수도 있음, 요청 시 갱신 참조
sql변환을 계산하는 SQL 쿼리
columns출력의 컬럼 정의

변환 테이블은 일반 테이블과 마찬가지로 보드 및 SDK를 통해 접근할 수 있습니다.

변환은 자신의 SQL이 반환하는 그대로 읽힙니다: 위젯의 시간 창, 캘린더 필터, latest 모드는 테이블에 적용되며 변환에서는 무시됩니다. 여기서 세 가지 규칙이 따라 나옵니다. 시간 범위는 쿼리 안에서 제한하고(WHERE tsp > now() - interval '7 days'), 시계열은 최신 순으로 정렬하여(ORDER BY <time column> DESC) 행 제한이 걸려도 최신 행이 남게 하며, 보드에서 그리거나 필터링할 모든 컬럼을 선택하십시오 — 변환에는 암묵적인 타임스탬프 컬럼이나 디바이스 컬럼이 없습니다. 한 번의 읽기는 최대 3000행을 반환하므로 쿼리에서 집계하십시오.

변환은 테이블 읽기로 표현할 수 없는 것을 위한 것입니다: 조인, 비율, 여러 테이블의 현재 행에 대한 집계. 엔티티의 현재 상태를 보여주거나 삭제된 레코드를 숨기는 방법은 아닙니다. 그것은 테이블 자체의 읽기 모드인 latest 토글과 deleted 컬럼에 대한 필터(아래 참조)이며, 라이브로 동작합니다: 테이블에 바인딩된 위젯은 모든 새 행을 1초 안에 따라갑니다. 변환은 자신의 일정에 따라서만(또는 앱의 코드가 자신의 쓰기 뒤에 변환을 갱신할 때 — 보드의 저장은 이를 결코 트리거하지 않습니다) 보드에 도달하므로, 엔티티별 최신 행을 고르거나 삭제된 행을 걸러내는 뷰는 같은 결과를 늦게 보여줍니다. 변환이 정말로 엔티티 테이블의 현재 상태를 필요로 할 때, 예를 들어 다른 테이블과 조인하기 위해서라면, 자신의 SQL에서 DISTINCT ON으로 선택하고 deleted 검사를 레코드 소프트 삭제에 나온 순서대로 그곳에서 적용하십시오.

변환을 얻는 방법이 앱과 함께 배포하는 것만 있는 것은 아닙니다: Data Access 권한이 있는 프로젝트 멤버는 앱 없이도 프로젝트의 데이터 뷰에서 동일한 종류의 변환을 저장할 수 있으며, AI 어시스턴트도 마찬가지입니다. 사용자 정의 변환을 참조하십시오.

머티리얼라이즈드 변환이 갱신되는 시점

일정에 따른 실행은 머티리얼라이즈드 변환이 읽는 테이블에 마지막 갱신 이후 쓰기가 있었을 때만 변환을 갱신하며, 그와 관계없이 최소 10분마다 한 번은 갱신합니다. 새로 읽을 것이 없는 실행은 저장된 결과를 그대로 둡니다. 예를 들어 한 시간에 한 행을 받는 테이블을 읽으며 1분마다 실행되도록 일정이 잡힌 변환은 그 행이 들어온 뒤에 갱신되고, 그 밖에는 10분마다 갱신됩니다.

결과가 쓰기 없이도 바뀔 수 있는 경우, 머티리얼라이즈드 변환은 실행할 때마다 갱신됩니다:

  • SQL이 now(), CURRENT_DATE, random()처럼 현재 시각이나 무작위성을 사용하는 경우
  • 같은 입력에 대해 다른 결과를 반환할 수 있는 함수를 호출하는 경우: 휘발성(volatile) 함수, 또는 PostgreSQL에 내장되지 않은 안정(stable) 함수
  • 앱 테이블과 머티리얼라이즈드 변환 이외의 것을 읽는 경우. 예를 들어 머티리얼라이즈드가 아닌 변환, 다운샘플링된 사본, 플랫폼 자체의 테이블

많은 변환이 같은 순간에 실행 시점을 맞으면 플랫폼은 한 번에 몇 개씩 갱신하고 나머지는 차례를 기다리므로, 갱신이 예정된 시각보다 늦게 끝날 수 있습니다. 같은 변환의 이전 갱신이 아직 실행 중이거나 대기 중일 때 실행 시점이 된 실행은 건너뜁니다.

요청 시 갱신

앱 자신의 코드는 일정을 기다리지 않고 변환의 실행을 수행할 수 있습니다: SDK의 refresh_transform / refreshTransform / RefreshTransform은 머티리얼라이즈드 결과를 갱신하고(일반 뷰는 읽기만 합니다) 변환에 바인딩된 보드에 현재 결과를 전송하므로, 보드는 앱이 방금 쓴 배치를 곧바로 보여줍니다. 일정에 따른 실행과 같은 규칙이 적용됩니다: 변환이 읽는 테이블에 마지막 갱신 이후 쓰기가 없었으면 갱신은 건너뛰며(건너뛴 것으로 보고됩니다), 같은 변환의 갱신이 실행 중일 때 도착한 호출은 그 뒤에 대기 중인 갱신 하나를 기다리고, 변환마다 초당 최대 한 번의 갱신만 시작됩니다. SDK의 테이블 쓰기는 반환되기 전에 커밋되므로, 쓰기 뒤에 이어지는 갱신은 그 쓰기를 결코 놓치지 않습니다. 앱 자신의 컨테이너만 그 앱의 변환을 갱신할 수 있습니다. 보드는 행을 받기만 하며, 다른 앱의 데이터를 소비하는 앱은 그 변환을 읽을 수는 있지만 갱신할 수는 없습니다. 프로젝트의 데이터 뷰에서 저장한 변환은 어느 앱에도 속하지 않으며 자신의 일정에 따라서만 실행됩니다. refreshTransform / refresh_transform을 참조하십시오.

엔티티의 최신 상태 추적

실제 엔티티(기계, 자산, 생산 주문)의 현재 상태를 나타내는 테이블의 경우 IronFlock은 최신 상태 추적(latest-state tracking)이라는 패턴을 지원합니다.

무언가가 변경될 때 행을 덮어쓰는 대신 항상 새 행을 추가합니다. 고유한 엔티티를 식별하는 컬럼을 선언해 두면, IronFlock이 테이블을 읽을 때마다 엔티티별로 가장 최근 행을 도출합니다. 이를 통해 모든 변경의 전체 이력을 보유하면서 현재 상태만 쿼리하는 것도 쉽게 할 수 있습니다.

maintainLatestFlagFor로 테이블에서 활성화합니다:

- tablename: machineform maintainLatestFlagFor: ['machinename'] columns: - id: tsp dataType: timestamp - id: machinename dataType: string - id: machinetype dataType: string - id: active dataType: boolean - id: description dataType: string

maintainLatestFlagFor는 고유한 엔티티를 함께 식별하는 컬럼 목록을 받습니다. 행 자체에는 아무것도 기록되지 않습니다. IronFlock은 해당 엔티티 키와 타임스탬프로 테이블을 인덱싱하고, 쿼리 시점에 엔티티별로 가장 최근 행을 선택합니다. 따라서 늦게 도착하거나 순서가 뒤바뀐 채 도착한 행이 오래된 표시를 남기는 일은 결코 발생하지 않습니다.

엔티티 테이블은 엔티티와 타임스탬프마다 하나의 행을 보관합니다. 저장된 행과 엔티티 키 및 tsp가 모두 같은 행은 거부되며, 엔티티 키와 tsp가 모두 같은 두 행이 들어 있는 배치도 거부됩니다: SDK의 쓰기는 URI_ENTITY_KEY_CONFLICT로 실패하고, 앱의 error-logs 테이블에 거부 사실이 기록됩니다. 엔티티의 각 버전마다 고유한 tsp를 지정하십시오. 엔티티 키 컬럼이 비어 있는(NULL) 행은 어떤 엔티티에도 속하지 않습니다: 중복으로 거부되는 일이 없으며, 최신 상태 읽기에서는 제외됩니다.

부분 행으로 엔티티 업데이트하기

엔티티 테이블에 추가되는 행은 그 엔티티의 새 버전입니다 — 그리고 반드시 완전할 필요는 없습니다. 행이 제공하지 않는 컬럼은 해당 엔티티의 직전 최신 행에서 물려받으므로, 필드 하나를 업데이트하려면 엔티티 키, 타임스탬프, 그리고 그 필드만 발행하면 됩니다:

새 행의 컬럼이저장된 버전에 담기는 값
제공된 경우 (0, false, ""도 제공된 것으로 칩니다)제공된 값
명시적으로 null인 경우NULL — 컬럼이 비워집니다
없는 경우직전 최신 행의 값

컬럼을 비우려면 null(Python에서는 None, Go에서는 nil)로 보내십시오. 생략하면 이전 값이 유지됩니다. 의도치 않게 필드를 생략하는 코드에 주의하십시오. 예를 들어 값이 undefined인 JavaScript 속성은 페이로드에서 빠집니다. SDK 레퍼런스에 언어별 사례가 정리되어 있습니다.

알아 두면 좋은 두 가지 세부 사항이 있습니다:

  • 모든 컬럼을 제공하는 행은 직전 행 조회를 아예 건너뛰므로, 완전한 행은 지금까지와 똑같이 저렴합니다 — 고빈도 경로에서는 계속 완전한 행을 보내십시오.
  • 상속은 시간 순서를 존중합니다. 더 오래된 타임스탬프를 달고 도착한 행(백필)은 자신의 tsp 이전이거나 같은 시점의 행에서만 물려받으며, 더 새로운 행에서는 결코 물려받지 않습니다.

maintainLatestFlagFor가 없는 테이블은 단순 추가 의미론을 유지합니다: 행이 제공하지 않는 컬럼은 NULL로 저장됩니다.

현재 기계 상태만 쿼리하려면:

SELECT DISTINCT ON (machinename) * FROM machineform ORDER BY machinename, tsp DESC

특정 기계의 전체 이력을 보려면:

SELECT * FROM machineform WHERE machinename = 'Assembly-Line-01' ORDER BY tsp

이 쿼리를 직접 작성할 일은 거의 없으며, 변환으로 저장해 두어서도 안 됩니다. 이 테이블에 연결된 보드의 위젯에는 필터 설정에 latest 토글이 있어 추가 작업 없이 사용자가 항상 현재 값을 볼 수 있습니다. 이 토글은 라이브입니다: 위젯은 테이블의 모든 새 행을 도착하는 즉시 받아 엔티티의 이전 행을 그것으로 교체하므로, 변경 사항은 기록된 뒤 1초 안에 표시되고 테이블은 모든 버전을 보존합니다. 일정에 따라 다시 계산되는 것은 아무것도 없으며, 변환이 더해 주는 것은 그것뿐일 것입니다. SDK에서는 filterAnd에 {"latest": true}를 추가하여 동일한 모드를 요청합니다 — getHistory를 참조하십시오.

latest_flag에서 업그레이드: 이전 버전의 IronFlock은 latest_flag라는 물리적 boolean 컬럼을 저장했습니다. 이 컬럼은 더 이상 존재하지 않습니다 — 대신 현재 상태를 SQL에서 도출하므로 행이 순서에 어긋나게 도착해도 결과가 정확하게 유지됩니다. latest_flag = true로 필터링하는 기존 보드와 SDK 호출은 계속 작동합니다: IronFlock이 이를 인식하여 최신 상태 모드를 적용합니다. 새 코드에서는 latest 토글 또는 {"latest": true} 필터 항목을 사용해야 합니다.

엔티티 키 변경하기

앱 버전이 릴리스되면 그 앱의 각 테이블의 엔티티 키는 고정됩니다. 클라우드 IDE 사이드 패널의 데이터 구조 업데이트와 새 릴리스의 빌드는, 앱의 최신 릴리스에 있는 테이블의 엔티티 키를 변경하는 data-template.yml을 거부합니다:

  • maintainLatestFlagFor에 컬럼을 추가하거나, 제거하거나, 교체하거나, 컬럼의 순서를 바꾸는 것
  • maintainLatestFlagFor가 없던 테이블에 이를 추가하거나, 이를 제거하는 것

이러한 변경은 앱을 설치한 모든 프로젝트에서 테이블이 반환하는 내용을 아무 경고 없이 바꿔 버립니다. 키에 새로 추가된 컬럼은 기존의 모든 행에서 비어 있고, 최신 상태 읽기는 키 컬럼이 비어 있는 행을 제외하므로 기존 엔티티가 모두 사라집니다. 키에서 제거된 컬럼은 같은 타임스탬프의 행을 가진 엔티티들을 하나로 합칠 수 있는 반면, 테이블은 엔티티와 타임스탬프마다 하나의 행만 보관할 수 있습니다.

다른 엔티티 키를 사용하려면 새 키로 새 테이블을 만들고, 예를 들어 앱의 코드에서 데이터를 그 테이블로 옮기십시오. 데이터를 옮기는 일은 앱의 몫입니다. 아직 어떤 릴리스에도 없는 테이블은 개발하는 동안 엔티티 키를 계속 변경할 수 있습니다. 이 규칙이 생기기 전에 릴리스된 앱 버전은 여전히 엔티티 키를 변경할 수 있습니다: 그 버전으로 업데이트하는 프로젝트는 데이터 구조 업데이트를 거부하고 이전 데이터 구조를 유지합니다.

엔티티와 타임스탬프가 중복된 행

IronFlock이 엔티티와 타임스탬프마다 하나의 행을 강제하기 전에 기록된 테이블에는 여전히 중복이 있을 수 있으며, 엔티티 키를 변경한 개발용 테이블도 마찬가지입니다. 데이터 서비스가 그런 테이블을 설정할 때(시작할 때 또는 데이터 구조 업데이트 후), 중복된 각 그룹의 모든 행을 같은 데이터 백엔드의 <table>__entitykey_dupes라는 테이블로 옮기고 각 그룹에서 한 행을 다시 돌려놓습니다. 그 이후로 테이블은 중복을 거부합니다. 앱의 error-logs 테이블에 몇 개의 행이 옮겨졌는지 알려 주는 항목이 기록됩니다.

새 테이블은 옮겨진 각 행을 그대로 보관하며, 두 개의 컬럼이 추가됩니다: 옮긴 시각인 quarantined_at과, 다시 돌아간 행을 표시하는 restored입니다. 어떤 행이 돌아갈지는 임의로 정해지며, 이는 최신 상태 읽기에서도 이미 그랬습니다. 프로젝트 데이터 뷰의 쿼리 콘솔에서 databackend_<key>.<table>__entitykey_dupes로 쿼리하십시오. 원래 테이블이 비공개이면 새 테이블도 비공개입니다. 행을 옮길 수 없으면 error-logs 항목에 그 이유가 기록되고, 데이터 서비스는 다음 시작 시 다시 시도합니다.

레코드 소프트 삭제

IronFlock의 추가 전용 모델에서는 레코드가 물리적으로 삭제되지 않습니다. 대신 deleted boolean 컬럼을 사용하여 레코드를 삭제된 것으로 표시합니다. 이는 전체 감사 추적을 보존하면서 삭제된 레코드를 대시보드에서 숨깁니다.

모든 엔티티 테이블에 deleted 컬럼을 추가합니다:

- id: deleted name: 삭제됨 dataType: boolean

사용자가 레코드를 삭제할 때(예: 보드의 양식을 통해) 앱은 해당 엔티티에 대해 deleted: true가 포함된 새 행을 발행합니다. maintainLatestFlagFor와 함께 사용하면 이 새 행이 최신 상태가 됩니다. 이후의 부분 행은 다른 컬럼과 마찬가지로 deleted 표시를 물려받으므로, deleted를 언급하지 않는 업데이트가 엔티티를 되살리지도, 숨기지도 않습니다.

활성(삭제되지 않은) 현재 레코드만 쿼리하려면:

SELECT * FROM ( SELECT DISTINCT ON (machinename) * FROM machineform ORDER BY machinename, tsp DESC ) latest WHERE deleted IS NULL OR deleted = false

deleted 검사는 기계별 최신 행이 선택된 다음에 실행됩니다. 이 순서가 중요합니다: 삭제된 행을 먼저 걸러내면 이전의 삭제되지 않은 행이 현재 상태로 다시 나타나게 됩니다.

보드 위젯과 SDK는 동일한 순서를 자동으로 적용합니다 — latest 토글(또는 {"latest": true})을 deleted 필터와 조합하면 정확히 이 동작이 됩니다. 양식 제출 직후 삭제된 레코드가 대시보드에서 즉시 사라지지만, 이력 및 감사 목적으로 데이터베이스에는 남아 있습니다.

보드에서 양식은 deleted 컬럼을 지정하는 Delete-Flag Column 설정을 통해 레코드를 삭제합니다: 양식은 삭제 시 true를, 저장할 때마다 false를 쓰므로 삭제된 레코드를 저장하면 복원됩니다(데이터 삭제 참조). 레코드를 나열하는 위젯은 테이블 자체를 필터링합니다. 목록을 정리하기 위한 뷰나 변환은 필요하지 않으며, 그런 것이 있다면 삭제를 다음 예정된 실행에서야 보여줄 것입니다.

다른 앱과 데이터 공유하기

데이터 백엔드는 앱 전용입니다. 프로젝트에 설치된 다른 어떤 앱도 여러분의 테이블을 볼 수 없습니다. data-template.yml의 선택적 키 두 개가 이를 바꿉니다.

다른 앱의 데이터를 읽으려면, 읽어올 앱들을 최상위 consumes: 섹션에 나열합니다. data: 안이 아니라 그 옆에 둡니다.

consumes: - app: machine-monitor reason: "모니터의 기계 상태 및 카운터 스트림에서 OEE를 계산합니다" data: tables: - tablename: oee_results columns: # ... 평소와 같이 앱 자체 테이블

app은 제공 앱의 기술 이름이거나, 프로젝트 내 모든 앱을 의미하는 "*"(따옴표 필수)입니다. reason은 동의 대화상자에서 사용자에게 표시됩니다. 선언만으로는 아무것도 허용되지 않으며 사용자의 승인이 있어야 합니다.

개별 테이블을 비공개로 두려면 private: true로 표시합니다. 정의한 모든 것은 기본적으로 공유 가능하지만, 비공개 테이블이나 트랜스폼은 다른 앱이 보는 카탈로그에 전혀 나타나지 않습니다.

data: tables: - tablename: measurements # 공유됨(기본값) columns: [ ... ] - tablename: calibration_state # 내부용 — 다른 앱에는 절대 보이지 않음 private: true columns: [ ... ]

접근 권한은 읽기 전용이며, 프로젝트별로 사용자가 부여하고 언제든지 철회할 수 있습니다. 전체 모델과 제공 앱의 이력·라이브 스트림을 읽는 SDK 호출은 다른 앱의 데이터 사용을 참고하세요.

특권 접근

앱의 컨테이너는 디바이스와 격리된 채 Docker의 기본 권한으로 실행됩니다. 어떤 앱은 그 이상이 필요합니다. 시리얼 포트나 USB 어댑터, GPIO, CAN 또는 Bluetooth 인터페이스, 호스트 네트워크 같은 것들입니다. 이것이 특권 접근입니다. 특권 접근 권한이 있는 앱은 다른 앱과 그 데이터를 포함하여 디바이스 전체를 제어할 수 있으므로, 앱은 이를 요청해야 하고 부여 여부는 사용자가 결정합니다.

요청하려면, 최상위에 reason을 포함한 privileged: 섹션을 추가합니다. data: 안이 아니라 그 옆에 둡니다.

privileged: reason: "시리얼 포트 /dev/ttyUSB0을 통해 PLC를 읽습니다" data: tables: # ... 평소와 같이 앱 자체 테이블

reason은 필수입니다. 동의 대화상자에서 사용자에게 표시되므로, 앱을 신뢰할지 결정하는 사람을 위해 작성하세요.

테이블이 없는 앱은 privileged 옆에 data: {}를 씁니다. data: 키가 없는 data-template.yml은 게시할 수 없습니다.

사용자가 허용하는 내용은 앱이 만들어진 방식에 따라 다릅니다.

  • 단일 이미지 앱(Dockerfile만 있는 앱)은 사용자가 허용하면 특권 모드로 실행되며, 모든 Linux 기능(capabilities), 디바이스의 /dev, 호스트의 D-Bus를 사용할 수 있습니다. 선언이 없으면 호스트의 디바이스, 추가 기능, 호스트의 D-Bus 없이 실행됩니다. 릴리스는 그래도 게시되며, 빌드 로그에 경고가 표시됩니다.
  • Docker Compose 앱은 docker-compose.yml이 요청하는 설정만 정확히 받으며, 동의 대화상자에 그 목록이 표시됩니다. compose 파일이 아래 설정 중 하나라도 사용하면 앱은 privileged를 선언해야 합니다. 그렇지 않으면 릴리스 게시가 실패하고, 오류 메시지에 해당 서비스와 설정이 표시됩니다. compose 파일이 아무것도 사용하지 않는데 privileged를 선언해도 실패합니다.

다음 compose 설정은 특권으로 간주됩니다.

설정특권으로 간주되는 경우
privilegedfalse를 제외한 모든 값
cap_add, devices, device_cgroup_rules항목이 있는 경우
security_optno-new-privileges 이외의 항목이 있는 경우
network_mode, pid, ipc, uts, userns_mode, cgrouphost 또는 container:…로 설정된 경우
cgroup_parent값이 설정된 경우
서비스의 volumes호스트 경로(/…, ~/…, C:\…), 앱 폴더 밖의 경로(../…) 또는 명명된 파이프를 마운트하는 경우. 읽기 전용 /etc/localtime과 /etc/timezone은 괜찮습니다.
volumes_fromcontainer:…를 지정하는 경우
최상위 volumes볼륨에 driver_opts, external 또는 고정된 name이 있는 경우
최상위 networks네트워크가 bridge 이외의 driver(macvlan, ipvlan, host 등)나 driver_opts를 설정하거나, external이거나, 고정된 name이 있는 경우
최상위 secrets, configs항목이 앱 폴더 밖의 file을 읽는 경우

명명된 볼륨, 기본 bridge 드라이버를 쓰는 네트워크, 앱 폴더 안의 바인드 마운트(./config:/config), 게시된 포트, GPU 예약(gpus, deploy.resources.reservations.devices)은 특권이 아닙니다.

이러한 설정에 ${VAR} 자리 표시자를 쓰는 것은 허용되지만 특권으로 간주되므로 역시 선언이 필요합니다. 이렇게 하면 앱 파라미터로 사용자가 시리얼 포트를 선택하게 할 수 있습니다.

services: plc: build: . devices: - "${SERIAL_PORT}:/dev/ttyS0"

include나 file을 지정한 extends를 사용하는 compose 파일은 선언 여부와 관계없이 게시할 수 없습니다. 릴리스에는 docker-compose.yml만 포함되기 때문입니다. 다른 파일은 그 안에 합치세요.

앱에 필요한 만큼만 요청하세요. 시리얼 포트 하나만 필요하다면 단일 이미지나 privileged: true 대신 서비스 하나와 devices: [/dev/ttyUSB0]만 있는 compose 파일을 작성하세요. 사용자는 앱이 무엇을 받는지 정확히 볼 수 있고, 앱은 그 이상을 할 수 없습니다.

동의와 철회

  • 설치 시 사용자는 여러분의 reason과, compose 앱의 경우 설정 목록을 보고 특권 접근을 켜거나 끕니다. 거부해도 설치가 막히지는 않습니다.
  • 업데이트 시 새 버전이 특권 접근을 요청하는데 프로젝트가 아직 허용하지 않았다면 업데이트 대화상자에서 묻습니다. 거부해도 앱은 업데이트되며, 새 버전은 특권 접근 없이 실행됩니다.
  • 그 이후 특권 접근은 프로젝트의 앱 설정에 있는 스위치가 되며, 사용자는 언제든지 이를 철회하거나 허용할 수 있습니다. 실행 중인 디바이스는 새 설정으로 앱을 다시 시작합니다.

클라우드 IDE에서의 개발 실행은 선언을 따르며 스위치가 없습니다. .ironflock/data-template.yml이 privileged를 선언하면 개발 빌드는 특권 모드로 실행되고, 선언하지 않으면 특권 접근 없이 실행됩니다. compose 파일이 선언 없이 특권 설정을 사용하면 개발 빌드는 게시할 때와 같은 오류로 실패합니다.

허용하지 않은 경우

앱은 그래도 시작됩니다. 단일 이미지 앱은 Docker 기본값으로 실행되며, /dev도, 추가 기능도, 호스트의 D-Bus도 없습니다. compose 앱의 경우 디바이스 에이전트가 앱을 시작하기 전에 compose 파일에서 특권 설정을 제거하고, 어떤 서비스에서 어떤 설정을 제거했는지 앱 로그에 기록합니다. 열 수 없는 시리얼 포트처럼 접근할 수 없는 것이 있으면 코드가 충돌하지 않고 이를 알리도록 하세요.

이전 릴리스

앱이 특권 접근을 선언해야 하기 전에 게시된 릴리스는 특권 접근을 요청한 것으로 간주되며, 기존 설치는 이를 유지하므로 이러한 앱은 이전과 같이 실행됩니다. 사용자는 다른 앱과 마찬가지로 프로젝트의 앱 설정에서 이를 철회할 수 있습니다. 다음 릴리스부터는 앱에 필요하다면 privileged를 선언하세요.

Last updated on