מאפיינים ספציפיים ליצרן ב-iOS

אתם יכולים להוסיף תמיכה בתכונות מותאמות אישית משלכם, שמספקות יכולות נוספות מעבר לאלה שמסופקות על ידי התכונות הרגילות. מאפיינים מותאמים אישית כמו זה נקראים מאפיינים ספציפיים ליצרן או מאפייני MS. מאפייני MS מוגדרים בפורמט .matter IDL הרגיל, ואז מומרים למודול iOS שאפשר לייבא לאפליקציה.

ערכת ה-SDK של iOS Home APIs כוללת מחולל קוד שיכול לבצע את ההמרה הזו. בנוסף, אם צריך, אפשר גם ליצור תכונות זמניות באמצעות מחולל הקוד הזה.

דרישות מוקדמות

כדי להשתמש בכלי ליצירת קוד, צריך:

  • ‫Python 3.10 ואילך.
  • קובץ .matter IDL עם ההגדרה של מאפייני ה-MS. הקובץ הזה צריך להכיל רק את ההגדרות של client cluster. אפשר ליצור אחד באופן ידני, או להשתמש באחד שנוצר כחלק מתהליך הבנייה של MatterSDK עבור קושחת המכשיר.

לכלי Matter codegen יש כמה תלויות ב-Python שצריך להתקין. זהו שלב חד-פעמי שצריך לבצע אחרי שמשלבים גרסה חדשה של ערכת ה-SDK, כדי לוודא שאפשר להריץ את הכלי בארגז החול של Xcode.

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

מידע נוסף על פורמט IDL זמין ב-matter/idl ב-GitHub. בספרייה /tests/inputs יש כמה קובצי IDL לדוגמה. קובץ ה-IDL המלא לכל אשכולי Matter, שהוא המקור לקבצים שנוצרו בכל הפלטפורמות (כולל מודולי iOS עבור ממשקי ה-API של Home), נמצא בכתובת 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. בוחרים מרחב שמות לקוד שנוצר ומוסיפים אותו כ-pragma swift לקובץ ה-IDL. לדוגמה, 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.swift ו-MyCustom.swift.

אם אתם משתמשים במסגרת נפרדת למאפיינים, אתם צריכים להשתמש בהצהרה import כדי לייבא את המודול הרלוונטי.

אחרי כן, תכונות MS אמורות להיות זמינות דרך ממשקי ה-API של Home באותו אופן שבו תכונות Matter רגילות זמינות, כל עוד תכונות MS האלה מוגדרות בקושחה של Matter. פשוט מחליפים שם מאפיין סטנדרטי בשם המאפיין שלכם ב-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 תואם לערך 0xFFF1FC00 בפורמט הקסדצימלי, שבו הקידומת 0xFFF1 מייצגת מזהה ספק לבדיקה והסיומת 0xFC00 היא ערך ששמור למאפיינים ספציפיים ליצרן. מידע נוסף זמין בקטע Manufacturer Extensible Identifier (MEI) במפרט Matter. חשוב להקפיד להשתמש במזהה תכונה עשרוני מתאים לכל תכונת MS בקובץ ה-IDL.

אם התכונה MS traits נמצאת בשימוש במכשיר שלכם היום, סביר להניח שהיא כבר מוגדרת בפורמט הזה.

פלט Swift

אפשר למצוא שני קובצי Swift, ‏ MyCustom.swift (שנקרא על שם המאפיין) ו-MyCompany.swift (שנקרא על שם מרחב השמות), בספריית הפלט שצוינה. הקבצים האלה מפורמטים במיוחד לשימוש בממשקי ה-API של Home.

אחרי שהקבצים יהיו זמינים (למשל בפרויקט 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 { }