iOS의 제조업체별 트레잇

표준 특성에서 제공하는 기능 외에 추가 기능을 제공하는 자체 맞춤 특성을 지원할 수 있습니다. 이러한 맞춤 특성을 제조업체별 특성 또는 MS 특성이라고 합니다. MS 특성은 표준 .matter IDL 형식으로 정의된 후 앱으로 가져올 수 있는 iOS 모듈로 변환됩니다.

iOS Home APIs SDK에는 이 변환을 실행할 수 있는 코드 생성기가 포함되어 있습니다. 또한 필요한 경우 이 코드 생성기를 사용하여 임시 특성을 생성할 수도 있습니다.

기본 요건

코드 생성기를 사용하려면 다음이 필요합니다.

  • Python 3.10 이상
  • MS 특성의 정의가 포함된 .matter IDL 파일 이 파일에는 client cluster 정의만 포함되어야 합니다. 직접 만들거나 기기 펌웨어의 Matter SDK 빌드 프로세스의 일부로 생성된 것을 사용할 수 있습니다.

Matter 코드 생성 도구에는 설치해야 하는 Python 종속 항목이 있습니다. 이는 SDK의 새 버전을 통합한 후에 도구가 Xcode 샌드박스에서 실행될 수 있도록 한 번 실행해야 하는 단계입니다.

swift package plugin --allow-network-connections "all" --allow-writing-to-package-directory matter-codegen-init

IDL 형식에 관한 자세한 내용은 GitHub의 matter/idl을 참고하세요. /tests/inputs 디렉터리에는 여러 샘플 IDL 파일이 있습니다. 모든 Matter 클러스터의 전체 IDL 파일은 모든 플랫폼 (홈 API의 iOS 모듈 포함)에서 생성된 파일의 소스이며 controller-clusters.matter에서 확인할 수 있습니다.

Swift 파일 생성

코드 생성기는 SDK와 함께 번들로 제공되며 Swift Package Manager(SwiftPM)에 통합되어 있습니다. 코드 생성기는 Xcode에서 SwiftPM 플러그인으로 호출되지만 패키지 관리를 위해 프로젝트에서 SwiftPM을 사용할 필요는 없습니다.

  1. 프로젝트에 SDK를 통합합니다. 자세한 내용은 iOS SDK 설정을 참고하세요.
  2. 플러그인을 설정합니다. 플러그인은 샌드박스에서 실행되므로 몇 가지 종속 항목을 설치해야 합니다.
    swift package plugin matter-codegen-init \
     --allow-network-connections all \
     --allow-writing-to-package-directory
  3. 생성된 코드의 네임스페이스를 결정하고 IDL 파일에 pragma swift로 추가합니다. 예를 들어 MyCompany는 다음과 같습니다.

    // pragma kotlin(package=com.mycompany.matter.cluster, generate_namespace=true)
    // pragma swift(package=MyCompany, generate_namespace=true)
    
    client cluster SimpleCustom = 4294048768 {
        attribute int16u clusterAttr = 1;
    
        // Global Attributes
        readonly attribute command_id generatedCommandList[] = 65528;
        readonly attribute command_id acceptedCommandList[] = 65529;
        readonly attribute event_id eventList[] = 65530;
        readonly attribute attrib_id attributeList[] = 65531;
        readonly attribute bitmap32 featureMap = 65532;
        readonly attribute int16u clusterRevision = 65533;
    }
    
  4. 생성기를 실행합니다.

    swift package plugin matter-codegen Clusters/MyCustomCluster.matter

    생성된 .swift 파일은 앱 프로젝트에 추가하거나 별도의 모듈에 추가할 수 있습니다.

대안: 특성을 자동으로 생성

코드 생성기에는 .matter 파일 확장자를 인식하는 추가 플러그인이 포함되어 있습니다. 플러그인은 코드 생성기를 자동으로 호출하고 출력 Swift 파일을 현재 타겟에 추가합니다. 이렇게 하면 생성된 파일을 소스 제어에 커밋할 필요가 없으며 항상 번들 버전의 생성기를 사용하여 특성이 생성됩니다. 앱에서 이미 SwiftPM을 사용하고 있다면 이 플러그인을 사용할 것을 적극 권장합니다.

플러그인을 사용하려면 다음 단계를 따르세요.

  1. 앱의 타겟에 .matter 파일을 추가합니다.
  2. 해당 타겟에 다음 플러그인 스니펫을 추가합니다.

        .target(
          name: "MyAppTarget",
          plugins: [.plugin(name: "MatterCodegenPlugin")]
        ),
    

모듈 사용

생성된 출력을 사용하려면 파일을 Xcode 프로젝트에 복사합니다. 이 경우 파일은 MyCompany.swiftMyCustom.swift입니다.

특성에 별도의 프레임워크를 사용하는 경우 import 문을 사용하여 해당 모듈을 가져옵니다.

이제 MS 특성이 Matter 펌웨어에 정의되어 있는 한 표준 Matter 특성과 동일한 방식으로 Home API를 통해 MS 특성을 사용할 수 있습니다. 표준 특성 이름을 MS 특성 이름으로 대체하기만 하면 됩니다.

예를 들어 MS 특성의 이름이 MyCustomTrait인 경우 다음 호출은 MyCustomTrait의 모든 속성을 반환합니다.

let myCustomTrait = deviceType.traits[MyCompany.MyCustomTrait.self]

IDL 형식에 익숙하지 않다면 matter/idl/tests/inputs 디렉터리에서 샘플 파일을 참고하세요.

IDL 입력

매우 간단한 MS 특성은 다음과 같이 IDL에서 정의할 수 있습니다.

// mycustom.matter
// pragma kotlin(package=com.mycompany.matter.cluster, generate_namespace=true)
// pragma swift(package=MyCompany, generate_namespace=true)

client cluster MyCustom = 4294048768 {
    attribute int16u clusterAttr = 1;

    // Global Attributes
    readonly attribute command_id generatedCommandList[] = 65528;
    readonly attribute command_id acceptedCommandList[] = 65529;
    readonly attribute event_id eventList[] = 65530;
    readonly attribute attrib_id attributeList[] = 65531;
    readonly attribute bitmap32 featureMap = 65532;
    readonly attribute int16u clusterRevision = 65533;
}

이 예에서 4294048768의 특성 ID는 16진수 0xFFF1FC00에 해당합니다. 여기서 0xFFF1 접두사는 테스트 공급업체 ID를 나타내고 0xFC00 접미사는 제조업체별 특성을 위해 예약된 값입니다. 자세한 내용은 Matter 사양의 제조업체 확장 가능 식별자 (MEI) 섹션을 참고하세요. IDL 파일의 각 MS 특성에 적절한 10진수 특성 ID를 사용해야 합니다.

오늘 기기에서 MS 특성을 사용하는 경우 이미 이 형식으로 정의되어 있을 것입니다.

Swift 출력

특성 이름을 딴 MyCustom.swift와 네임스페이스 이름을 딴 MyCompany.swift라는 두 개의 Swift 파일이 지정된 출력 디렉터리에 있습니다. 이러한 파일은 Home API와 함께 사용하도록 특별히 포맷되어 있습니다.

파일을 사용할 수 있게 되면 (예: 앱의 Xcode 프로젝트에서) 모듈 사용에 설명된 대로 파일을 사용할 수 있습니다.

MyCustom.swift

클릭하여 `MyCustom.swift`를 확인합니다.

// This file contains machine-generated code.

public import Foundation
@_spi(GoogleHomeInternal) import GoogleHomeSDK

/*
 * This file was machine generated via the code generator
 * in `codegen.clusters.swift.CustomGenerator`
 *
 */

extension MyCompany {
/// :nodoc:
  public struct MyCustomTrait: MatterTrait {

    /// No supported events for `MyCustomTrait`.
    public static let supportedEventTypes: [Event.Type] = []

    /// No supported commands for `MyCustomTrait`.
    public static let supportedCommandTypes: [Command.Type] = []

    public static let identifier = MyCompany.MyCustomTrait.makeTraitID(for: 4294048768)

    public let metadata: TraitMetadata

    /// List of attributes for the `MyCustomTrait`.
    public let attributes: MyCompany.MyCustomTrait.Attributes

    private let interactionProxy: InteractionProxy

    public init(decoder: TraitDecoder, interactionProxy: InteractionProxy?, metadata: TraitMetadata) throws {
      guard let interactionProxy = interactionProxy else {
        throw HomeError.invalidArgument("InteractionProxy parameter required.")
      }
      let unwrappedDecoder = try decoder.unwrapPayload(namespace: Self.identifier.namespace)
      self.interactionProxy = interactionProxy
      self.attributes = try Attributes(decoder: unwrappedDecoder)
      self.metadata = metadata
    }

    // Internal for testing.
    internal init(attributes: MyCompany.MyCustomTrait.Attributes = .init(), interactionProxy: InteractionProxy?, metadata: TraitMetadata = .init()) throws {
      guard let interactionProxy = interactionProxy else {
        throw HomeError.invalidArgument("InteractionProxy parameter required.")
      }
      self.interactionProxy = interactionProxy
      self.attributes = attributes
      self.metadata = metadata
    }

    public func encode(with encoder: TraitEncoder) throws {
      encoder.wrapPayload(namespace: Self.identifier.namespace)
      try self.attributes.encode(with: encoder)
    }

    public func update(_ block: @Sendable (MutableAttributes) -> Void) async throws -> Self {
      let mutable = MutableAttributes(attributes: self.attributes)
      block(mutable)
      if self.interactionProxy.strictOperationValidation {
        guard self.attributes.$clusterAttr.isSupported || !mutable.clusterAttrIsSet else {
          throw HomeError.invalidArgument("clusterAttr is not supported.")
        }
      }
      let updatedTrait = try MyCompany.MyCustomTrait(attributes: self.attributes.apply(mutable), interactionProxy: self.interactionProxy, metadata: self.metadata)
      try await self.interactionProxy.update(trait: mutable, useTimedInteraction: false)
      return updatedTrait
    }
  }
}

// MARK: - ForceReadableTrait

extension MyCompany.MyCustomTrait: ForceReadableTrait {
  public func forceRead() async throws {
    try await self.interactionProxy.forceRead(traitID: Self.identifier)
  }
}

// MARK: - Attributes

extension MyCompany.MyCustomTrait {

  /// Attributes for the `MyCustomTrait`.
  public struct Attributes: Sendable {
    // Attributes required at runtime.
    /** A list of the attribute IDs of the attributes supported by the cluster instance. */
    /// Nullable: false.
    @TraitAttribute public var attributeList: [UInt32]?

    /// Nullable: false.
    @TraitAttribute public var clusterAttr: UInt16?
    /** A list of server-generated commands (server to client) which are supported by this
    cluster server instance. */
    /// Nullable: false.
    @TraitAttribute public var generatedCommandList: [UInt32]?
    /** A list of client-generated commands which are supported by this cluster server instance.
    */
    /// Nullable: false.
    @TraitAttribute public var acceptedCommandList: [UInt32]?
    /**  Whether the server supports zero or more optional cluster features. A cluster feature
    is a set of cluster elements that are mandatory or optional for a defined feature of the
    cluster. If a cluster feature is supported by the cluster instance, then the corresponding
    bit is set to 1, otherwise the bit is set to 0 (zero). */
    /// Nullable: false.
    @TraitAttribute public var featureMap: UInt32?
    /** The revision of the server cluster specification supported by the cluster instance. */
    /// Nullable: false.
    @TraitAttribute public var clusterRevision: UInt16?

    internal init(
      clusterAttr: UInt16? = nil,
      generatedCommandList: [UInt32]? = nil,
      acceptedCommandList: [UInt32]? = nil,
      attributeList: [UInt32]? = nil,
      featureMap: UInt32? = nil,
      clusterRevision: UInt16? = nil
    ) {
      self._clusterAttr = .init(
        wrappedValue: clusterAttr,
        isSupported: attributeList?.contains(0x01) ?? false,
        isNullable: false
      )
      self._generatedCommandList = .init(
        wrappedValue: generatedCommandList,
        isSupported: attributeList?.contains(0x0FFF8) ?? false,
        isNullable: false
      )
      self._acceptedCommandList = .init(
        wrappedValue: acceptedCommandList,
        isSupported: attributeList?.contains(0x0FFF9) ?? false,
        isNullable: false
      )
      self._attributeList = .init(
        wrappedValue: attributeList,
        isSupported: attributeList?.contains(0x0FFFB) ?? false,
        isNullable: false
      )
      self._featureMap = .init(
        wrappedValue: featureMap,
        isSupported: attributeList?.contains(0x0FFFC) ?? false,
        isNullable: false
      )
      self._clusterRevision = .init(
        wrappedValue: clusterRevision,
        isSupported: attributeList?.contains(0x0FFFD) ?? false,
        isNullable: false
      )
    }

    fileprivate init(decoder: TraitDecoder) throws {
      let decodedAttributeList: [UInt32] = try decoder.decodeOptionalArray(tag: 0x0FFFB) ?? []
      var generatedAttributeList = [UInt32]()
      generatedAttributeList.append(0x0FFFB)

      let clusterAttrValue: UInt16? = try decoder.decodeOptional(tag: 0x01)
      let clusterAttrIsSupported = clusterAttrValue != nil
      if clusterAttrIsSupported {
        generatedAttributeList.append(0x01)
      }
      self._clusterAttr = .init(
        wrappedValue: clusterAttrIsSupported ? clusterAttrValue : nil,
        isSupported: clusterAttrIsSupported,
        isNullable: false
      )

      let generatedCommandListValue: [UInt32]? = try decoder.decodeOptionalArray(tag: 0x0FFF8)
      let generatedCommandListIsSupported = generatedCommandListValue != nil
      if generatedCommandListIsSupported {
        generatedAttributeList.append(0x0FFF8)
      }
      self._generatedCommandList = .init(
        wrappedValue: generatedCommandListIsSupported ? generatedCommandListValue : nil,
        isSupported: generatedCommandListIsSupported,
        isNullable: false
      )

      let acceptedCommandListValue: [UInt32]? = try decoder.decodeOptionalArray(tag: 0x0FFF9)
      let acceptedCommandListIsSupported = acceptedCommandListValue != nil
      if acceptedCommandListIsSupported {
        generatedAttributeList.append(0x0FFF9)
      }
      self._acceptedCommandList = .init(
        wrappedValue: acceptedCommandListIsSupported ? acceptedCommandListValue : nil,
        isSupported: acceptedCommandListIsSupported,
        isNullable: false
      )

      let featureMapValue: UInt32? = try decoder.decodeOptional(tag: 0x0FFFC)
      let featureMapIsSupported = featureMapValue != nil
      if featureMapIsSupported {
        generatedAttributeList.append(0x0FFFC)
      }
      self._featureMap = .init(
        wrappedValue: featureMapIsSupported ? featureMapValue : nil,
        isSupported: featureMapIsSupported,
        isNullable: false
      )

      let clusterRevisionValue: UInt16? = try decoder.decodeOptional(tag: 0x0FFFD)
      let clusterRevisionIsSupported = clusterRevisionValue != nil
      if clusterRevisionIsSupported {
        generatedAttributeList.append(0x0FFFD)
      }
      self._clusterRevision = .init(
        wrappedValue: clusterRevisionIsSupported ? clusterRevisionValue : nil,
        isSupported: clusterRevisionIsSupported,
        isNullable: false
      )

      self._attributeList = .init(
        wrappedValue: generatedAttributeList,
        isSupported: true,
        isNullable: false
      )
    }

    fileprivate func apply(_ update: MyCompany.MyCustomTrait.MutableAttributes) -> Self {
      let clusterAttrValue = update.clusterAttrIsSet ? update.clusterAttr : self.clusterAttr
      let generatedCommandListValue = self.generatedCommandList
      let acceptedCommandListValue = self.acceptedCommandList
      let attributeListValue = self.attributeList
      let featureMapValue = self.featureMap
      let clusterRevisionValue = self.clusterRevision
      return MyCompany.MyCustomTrait.Attributes(
        clusterAttr: clusterAttrValue,
        generatedCommandList: generatedCommandListValue,
        acceptedCommandList: acceptedCommandListValue,
        attributeList: attributeListValue,
        featureMap: featureMapValue,
        clusterRevision: clusterRevisionValue
      )
    }

  }
}

extension MyCompany.MyCustomTrait.Attributes: TraitEncodable {
  public static var identifier: String { MyCompany.MyCustomTrait.identifier }

  public func encode(with encoder: TraitEncoder) throws {
    try encoder.encode(tag: 0x01, value: self.clusterAttr)
    try encoder.encode(tag: 0x0FFF8, value: self.generatedCommandList)
    try encoder.encode(tag: 0x0FFF9, value: self.acceptedCommandList)
    try encoder.encode(tag: 0x0FFFB, value: self.attributeList)
    try encoder.encode(tag: 0x0FFFC, value: self.featureMap)
    try encoder.encode(tag: 0x0FFFD, value: self.clusterRevision)
  }
}

// MARK: - Hashable & Equatable

extension MyCompany.MyCustomTrait: Hashable {
  public static func ==(lhs: MyCompany.MyCustomTrait, rhs: MyCompany.MyCustomTrait) -> Bool {
    return lhs.identifier == rhs.identifier
      && lhs.attributes == rhs.attributes
      && lhs.metadata == rhs.metadata
  }

  public func hash(into hasher: inout Hasher) {
    hasher.combine(identifier)
    hasher.combine(attributes)
    hasher.combine(metadata)
  }
}

extension MyCompany.MyCustomTrait.Attributes: Hashable {
  public static func ==(lhs: MyCompany.MyCustomTrait.Attributes, rhs: MyCompany.MyCustomTrait.Attributes) -> Bool {
    var result = true
    result = lhs.clusterAttr == rhs.clusterAttr && result
    result = lhs.generatedCommandList == rhs.generatedCommandList && result
    result = lhs.acceptedCommandList == rhs.acceptedCommandList && result
    result = lhs.attributeList == rhs.attributeList && result
    result = lhs.featureMap == rhs.featureMap && result
    result = lhs.clusterRevision == rhs.clusterRevision && result
    return result
  }

  public func hash(into hasher: inout Hasher) {
    hasher.combine(self.clusterAttr)
    hasher.combine(self.generatedCommandList)
    hasher.combine(self.acceptedCommandList)
    hasher.combine(self.attributeList)
    hasher.combine(self.featureMap)
    hasher.combine(self.clusterRevision)
  }
}

// MARK: - MutableAttributes

extension MyCompany.MyCustomTrait {

  public final class MutableAttributes: TraitEncodable {
    public static let identifier: String = MyCompany.MyCustomTrait.identifier
    private let baseAttributes: Attributes

    fileprivate var clusterAttr: UInt16?
    private(set) public var clusterAttrIsSet = false
    public func setClusterAttr(_ value: UInt16) {
      self.clusterAttr = value
      self.clusterAttrIsSet = true
    }
    public func clearClusterAttr() {
      self.clusterAttr = nil
      self.clusterAttrIsSet = false
    }

    internal init(attributes: MyCompany.MyCustomTrait.Attributes) {
      self.baseAttributes = attributes
    }

    public func encode(with encoder: TraitEncoder) throws {
      // MutableAttributes is encoded individually, e.g. through update(...),
      // therefore uddm wrapping needs to be applied.
      encoder.wrapPayload(namespace: Self.identifier.namespace)
      if self.clusterAttrIsSet {
        try encoder.encode(tag: 0x01, value: self.clusterAttr)
      }
    }
  }
}

// MARK: - Attributes definitions

extension MyCompany.MyCustomTrait {
  public enum Attribute: UInt32, Field {
    case clusterAttr = 1
    case generatedCommandList = 65528
    case acceptedCommandList = 65529
    case attributeList = 65531
    case featureMap = 65532
    case clusterRevision = 65533

    public var id: UInt32 {
      self.rawValue
    }

    public var type: FieldType {
      switch self {
        case .clusterAttr:
          return .uint16
        case .generatedCommandList:
          return .uint32
        case .acceptedCommandList:
          return .uint32
        case .attributeList:
          return .uint32
        case .featureMap:
          return .uint32
        case .clusterRevision:
          return .uint16
      }
    }
  }

  public static func attribute(id: UInt32) -> (any Field)? {
    return Attribute(rawValue: id)
  }
}

// MARK: - Attribute fieldSelect definitions

extension TypedReference where T == MyCompany.MyCustomTrait {
  public var clusterAttr: TypedExpression<UInt16> {
    fieldSelect(from: self, selectedField: T.Attribute.clusterAttr)
  }
  public var generatedCommandList: TypedExpression<[UInt32]> {
    fieldSelect(from: self, selectedField: T.Attribute.generatedCommandList)
  }
  public var acceptedCommandList: TypedExpression<[UInt32]> {
    fieldSelect(from: self, selectedField: T.Attribute.acceptedCommandList)
  }
  public var attributeList: TypedExpression<[UInt32]> {
    fieldSelect(from: self, selectedField: T.Attribute.attributeList)
  }
  public var featureMap: TypedExpression<UInt32> {
    fieldSelect(from: self, selectedField: T.Attribute.featureMap)
  }
  public var clusterRevision: TypedExpression<UInt16> {
    fieldSelect(from: self, selectedField: T.Attribute.clusterRevision)
  }
}

extension Updater where T == MyCompany.MyCustomTrait {
  public func setClusterAttr(_ value: UInt16) {
    self.set(Parameter(field: T.Attribute.clusterAttr, value: value))
  }
}


// MARK: - Struct Fields definitions


  

MyCompany.swift

/// Namespace for all MyCompany Traits and DeviceTypes.
public enum MyCompany { }