diff --git a/GroundSdk.podspec b/GroundSdk.podspec index 6a0353e..abead05 100644 --- a/GroundSdk.podspec +++ b/GroundSdk.podspec @@ -1,17 +1,17 @@ Pod::Spec.new do |s| s.name = "GroundSdk" - s.version = "7.1.0" + s.version = "7.2.0" s.summary = "Parrot Drone SDK" s.homepage = "https://developer.parrot.com" s.license = "{ :type => 'BSD 3-Clause License', :file => 'LICENSE' }" s.author = 'Parrot Drone SAS' - s.source = { :git => 'https://github.com/Parrot-Developers/pod_groundsdk.git', :tag => "7.1.0" } + s.source = { :git => 'https://github.com/Parrot-Developers/pod_groundsdk.git', :tag => "7.2.0" } s.platform = :ios s.ios.deployment_target = '12.0' s.source_files = 'GroundSdk/**/*.{swift,h,m}' s.resources = 'GroundSdk/**/*.{vsh,fsh,txt,png}' - s.dependency 'SdkCore', '7.1.0' + s.dependency 'SdkCore', '7.2.0' s.public_header_files = ["GroundSdk/GroundSdk.h"] s.swift_version = '5' s.pod_target_xcconfig = {'SWIFT_VERSION' => '5'} diff --git a/GroundSdk/Device/Instrument/Alarms.swift b/GroundSdk/Device/Instrument/Alarms.swift index 54f5e81..660a9e6 100644 --- a/GroundSdk/Device/Instrument/Alarms.swift +++ b/GroundSdk/Device/Instrument/Alarms.swift @@ -162,6 +162,11 @@ public class Alarm: NSObject { /// direction. case obstacleAvoidanceBlindMotionDirection + /// Obstacle avoidance is frozen. + /// The drone does not respond to PCMD. + /// Obstacle avoidance mode needs to be set to disabled for the drone to move again. + case obstacleAvoidanceFreeze + /// Drone inclination is to high to fly safely. case inclinationTooHigh @@ -171,6 +176,12 @@ public class Alarm: NSObject { /// Vertical geofence reached. case verticalGeofenceReached + /// Free fall detected. + case freeFallDetected + + /// Stereo camera is decalibrated. + case stereoCameraDecalibrated + /// Debug description. public var description: String { switch self { @@ -208,6 +219,9 @@ public class Alarm: NSObject { case .inclinationTooHigh: return "inclinationTooHigh" case .horizontalGeofenceReached: return "horizontalGeofenceReached" case .verticalGeofenceReached: return "verticalGeofenceReached" + case .obstacleAvoidanceFreeze: return "obstacleAvoidanceFreeze" + case .freeFallDetected: return "freeFallDetected" + case .stereoCameraDecalibrated: return "stereoCameraDecalibrated" } } @@ -222,7 +236,8 @@ public class Alarm: NSObject { .obstacleAvoidanceDisabledTooDark, .obstacleAvoidanceDisabledEstimationUnreliable, .obstacleAvoidanceDisabledCalibrationFailure, .obstacleAvoidanceStrongWind, .obstacleAvoidancePoorGps, .obstacleAvoidanceComputationalError, .obstacleAvoidanceBlindMotionDirection, - .inclinationTooHigh, .horizontalGeofenceReached, .verticalGeofenceReached] + .inclinationTooHigh, .horizontalGeofenceReached, .verticalGeofenceReached, + .obstacleAvoidanceFreeze, .freeFallDetected, .stereoCameraDecalibrated] } /// Alarm level. @@ -258,6 +273,11 @@ public class Alarm: NSObject { /// Level of the alarm. public internal(set) var level: Level + /// Delay related to this alarm. + /// Used only by Obstacle avoidance. + /// This delay indicates the time after which the obstacle avoidance is deactivated. + public internal(set) var timer: TimeInterval? + /// Constructor. /// /// - Parameters: diff --git a/GroundSdk/Device/PilotingItf/FlightPlanPilotingItf.swift b/GroundSdk/Device/PilotingItf/FlightPlanPilotingItf.swift index 9584b91..1af375b 100644 --- a/GroundSdk/Device/PilotingItf/FlightPlanPilotingItf.swift +++ b/GroundSdk/Device/PilotingItf/FlightPlanPilotingItf.swift @@ -39,7 +39,8 @@ public enum FlightPlanUnavailabilityReason: Int, CustomStringConvertible { /// No flight plan file uploaded. case missingFlightPlanFile /// Drone cannot take-off. - /// This error can happen if the flight plan piloting interface is activated while the drone cannot take off. + /// This error can happen if the flight plan piloting interface is activated while the drone + /// cannot take off. /// It can be for example if the drone is in emergency or has not enough battery to take off. case cannotTakeOff /// Drone camera is not available. @@ -117,7 +118,8 @@ public enum FlightPlanFileUploadState: Int, CustomStringConvertible { } } -/// Result of media resources clean before recovery of a flight plan execution, see `cleanBeforeRecovery`. +/// Result of media resources clean before recovery of a flight plan execution, +/// see `cleanBeforeRecovery`. public enum CleanBeforeRecoveryResult: String, CustomStringConvertible { /// Media resources clean succeeded. case success @@ -156,8 +158,8 @@ public struct RecoveryInfo: Equatable { /// - latestMissionItemExecuted: index of the latest mission item completed /// - runningTime: running time of the flightplan being executed /// - resourceId: first resource id of the latest media capture requested by the flightplan. - public init(id: String, customId: String, latestMissionItemExecuted: UInt, runningTime: TimeInterval, - resourceId: String) { + public init(id: String, customId: String, latestMissionItemExecuted: UInt, + runningTime: TimeInterval, resourceId: String) { self.id = id self.customId = customId self.latestMissionItemExecuted = latestMissionItemExecuted @@ -166,20 +168,33 @@ public struct RecoveryInfo: Equatable { } } +/// Describes the drone's behaviour upon disconnection of GroundSdk. +public enum FlightPlanDisconnectionPolicy { + /// The drone stops the current executing flight plan and performs an Return To Home. + case returnToHome + /// The drone continues the current flight plan execution until its completion. Upon reaching + /// completion if the GroundSdk is still disconnected the default disconnect behavior is + /// performed. + case `continue` +} + /// Flight Plan piloting interface for drones. /// /// Allows to make the drone execute predefined flight plans. /// A flight plan is defined using a file in Mavlink format. For further information, please refer to /// [Parrot FlightPlan Mavlink documentation](https://developer.parrot.com/docs/mavlink-flightplan). /// -/// This piloting interface remains `.unavailable` until all `FlightPlanUnavailabilityReason` have been cleared: -/// - A Flight Plan file (i.e. a mavlink file) has been uploaded to the drone (see uploadFlightPlan(filepath:)) +/// This piloting interface remains `.unavailable` until all `FlightPlanUnavailabilityReason` have +/// been cleared: +/// - A Flight Plan file (i.e. a mavlink file) has been uploaded to the drone (see +/// `uploadFlightPlan(filepath:)`) /// - The drone GPS location has been acquired /// - The drone is properly calibrated /// - The drone is in a state that allows it to take off /// -/// Then, when all those conditions hold, the interface becomes `.idle` and can be activated to begin or resume -/// Flight Plan execution, which can be paused by deactivating this piloting interface. +/// Then, when all those conditions hold, the interface becomes `.idle` and can be activated to +/// begin or resume Flight Plan execution, which can be paused by deactivating this piloting +/// interface. /// /// This piloting interface can be retrieved by: /// ``` @@ -205,7 +220,8 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// It is put back to `.none` as soon as `activate(restart:)` is called. var latestActivationError: FlightPlanActivationError { get } - /// Whether the current flight plan on the drone is the latest one that has been uploaded from the application. + /// Whether the current flight plan on the drone is the latest one that has been uploaded from + /// the application. var flightPlanFileIsKnown: Bool { get } /// Identifier of the flight plan currently loaded on the drone, `nil` if unknown. @@ -216,24 +232,33 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// This information is provided by the drone at connection and when a flight plan stops. /// It is turned to `nil` when the drone is disconnected or when `clearRecoveryInfo()` is called. /// - /// If the application lose connection to the drone during a flight plan and then flight plan stops, this - /// information will help the application to manage flight plan resume at reconnection. + /// If the application lose connection to the drone during a flight plan and then flight plan + /// stops, this information will help the application to manage flight plan resume at + /// reconnection. var recoveryInfo: RecoveryInfo? { get } /// Whether the flight plan is currently paused. /// - /// If `true`, the restart parameter of `activate(restart:)` can be set to `false` to resume the flight plan instead - /// of playing it from the beginning. If `isPaused` is `false,` this parameter will be ignored and the flight plan - /// will be played from its beginning. + /// If `true`, the restart parameter of `activate(restart:)` can be set to `false` to resume the + /// flight plan instead of playing it from the beginning. If `isPaused` is `false,` this + /// parameter will be ignored and the flight plan will be played from its beginning. /// - /// When this piloting interface is deactivated, any currently playing flight plan will be paused. + /// When this piloting interface is deactivated, any currently playing flight plan will be + /// paused. var isPaused: Bool { get } /// Whether start of a flight plan at a given mission item is supported. /// - /// When `true`, method `activate(missionItem:restart:type:)` can be used. + /// When `true`, method `activate(restart:interpreter:missionItem:)` can be used. var activateAtMissionItemSupported: Bool { get } + /// Whether start of a flight plan at a given mission item with a disconnection policy is + /// supported. + /// + /// When `true`, method `activate(restart:interpreter:missionItem:disconnectionPolicy:)` can be + /// used. + var activateAtMissionItemV2Supported: Bool { get } + /// Tells whether uploading a flight plan with an associated custom identifier is supported. /// /// When `true`, method `uploadFlightPlan(filepath:customFlightPlanId:)` can be used. @@ -241,8 +266,9 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// Uploads a Flight Plan file to the drone. /// - /// When the upload ends, if all other necessary conditions hold (GPS location acquired, drone properly calibrated), - /// then the interface becomes idle and the Flight Plan is ready to be executed. + /// When the upload ends, if all other necessary conditions hold (GPS location acquired, drone + /// properly calibrated), then the interface becomes idle and the Flight Plan is ready to be + /// executed. /// /// If any upload is on-going it is cancelled. /// @@ -251,11 +277,12 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// Uploads a Flight Plan file to the drone. /// - /// This method associates the provided identifier only if `isUploadWithCustomIdSupported` returns `true`, - /// otherwise it behaves strictly as `uploadFlightPlan(filepath:)` function. + /// This method associates the provided identifier only if `isUploadWithCustomIdSupported` + /// returns `true`, otherwise it behaves strictly as `uploadFlightPlan(filepath:)` function. /// - /// When the upload ends, if all other necessary conditions hold (GPS location acquired, drone properly calibrated), - /// then the interface becomes idle and the Flight Plan is ready to be executed. + /// When the upload ends, if all other necessary conditions hold (GPS location acquired, drone + /// properly calibrated), then the interface becomes idle and the Flight Plan is ready to be + /// executed. /// /// If any upload is on-going it is cancelled. /// @@ -276,12 +303,13 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// The flight plan is resumed if the `restart` parameter is false and `isPaused` is `true`. /// Otherwise, the flight plan is restarted from its beginning. /// - /// If successful, it deactivates the current piloting interface and activate this one. + /// If successful, it deactivates the current piloting interface and activates this one. /// /// - Parameter restart: `true` to force restarting the flight plan. /// If `isPaused` is `false`, this parameter will be ignored. /// - Returns: `true` on success, `false` if the piloting interface can't be activated - /// - Note: activate(restart:) will call activate(restart: interpreter:) with interpreter `legacy`. + /// - Note: `activate(restart:)` will call `activate(restart: interpreter:)` with interpreter + /// `legacy`. func activate(restart: Bool) -> Bool /// Activates this piloting interface and starts executing the uploaded flight plan. @@ -290,32 +318,53 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// The flight plan is resumed if the `restart` parameter is false and `isPaused` is `true`. /// Otherwise, the flight plan is restarted from its beginning. /// - /// If successful, it deactivates the current piloting interface and activate this one. + /// If successful, it deactivates the current piloting interface and activates this one. /// /// - Parameters: - /// - restart: `true` to force restarting the flight plan. - /// If `isPaused` is `false`, this parameter will be ignored. + /// - restart: `true` to force restarting the flight plan. If `isPaused` is `false`, this + /// parameter will be ignored. /// - interpreter: instructs how the flight plan must be interpreted by the drone. /// - Returns: `true` on success, `false` if the piloting interface can't be activated func activate(restart: Bool, interpreter: FlightPlanInterpreter) -> Bool - /// Activates this piloting interface and starts executing the uploaded flight plan at given mission item. + /// Activates this piloting interface and starts executing the uploaded flight plan at given + /// mission item. /// /// The interface should be `.idle` for this method to have effect. /// The flight plan is resumed if the `restart` parameter is false and `isPaused` is `true`. /// Otherwise, the flight plan is restarted from the mission item. /// This method can be used only when `activateAtMissionItemSupported` is `true`. /// - /// If successful, it deactivates the current piloting interface and activate this one. + /// If successful, it deactivates the current piloting interface and activates this one. /// /// - Parameters: - /// - restart: `true` to force restarting the flight plan. - /// If `isPaused` is `false`, this parameter will be ignored. + /// - restart: `true` to force restarting the flight plan. If `isPaused` is `false`, this + /// parameter will be ignored. /// - interpreter: instructs how the flight plan must be interpreted by the drone /// - missionItem: index of mission item where the flight plan should start /// - Returns: `true` on success, `false` if the piloting interface can't be activated func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt) -> Bool + /// Activates this piloting interface and starts executing the uploaded flight plan at given mission item. + /// + /// The interface should be `.idle` for this method to have effect. + /// The flight plan is resumed if the `restart` parameter is false and `isPaused` is `true`. + /// Otherwise, the flight plan is restarted from the mission item. + /// This method can be used only when `activateAtMissionItemSupported` is `true`. + /// + /// If successful, it deactivates the current piloting interface and activates this one. + /// + /// - Parameters: + /// - restart: `true` to force restarting the flight plan. If `isPaused` is `false`, this + /// parameter will be ignored. + /// - interpreter: instructs how the flight plan must be interpreted by the drone + /// - missionItem: index of mission item where the flight plan should start + /// - disconnectionPolicy: the behavior of the drone when a disconnection occurs + /// - Returns: `true` on success, `false` if the piloting interface can't be activated + /// - Note: This activation method is compatible with drones running on firmware at least 7.2. + func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt, + disconnectionPolicy: FlightPlanDisconnectionPolicy) -> Bool + /// Stops execution of current flight plan, if any. /// /// This method has effect only if the piloting interface is active or if `isPaused` is `true`. @@ -325,22 +374,24 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// - Returns: `true` if the stop command was sent to the drone, `false` otherwise func stop() -> Bool - /// Clears information about the latest flight plan started by the drone prior to current connection. + /// Clears information about the latest flight plan started by the drone prior to current + /// connection. /// - /// This sends a command to the drone to clear this information, and sets `recoveryInfo` to `nil`. + /// This sends a command to the drone to clear this information, and sets `recoveryInfo` to + /// `nil`. func clearRecoveryInfo() /// Cleans media resources before recovery of a flight plan execution. /// - /// When a flight plan execution is interrupted, it can be restarted later from the latest reached waypoint. - /// This function can be called before the flight plan restart to delete media resources captured during the - /// interrupted execution and after the latest reached waypoint. The aim is to not have duplicate media resources - /// captured after the latest reached waypoint. + /// When a flight plan execution is interrupted, it can be restarted later from the latest + /// reached waypoint. This function can be called before the flight plan restart to delete media + /// resources captured during the interrupted execution and after the latest reached waypoint. + /// The aim is to not have duplicate media resources captured after the latest reached waypoint. /// /// - Parameters: /// - customId: custom identifier, as provided by `recoveryInfo` - /// - resourceId: first resource identifier of media captured after the latest reached waypoint, as provided - /// by `recoveryInfo` + /// - resourceId: first resource identifier of media captured after the latest reached + /// waypoint, as provided by `recoveryInfo` /// - completion: completion callback (called on the main thread) /// - result: media resources clean result /// - Returns: a clean media resources cancelable request @@ -351,19 +402,22 @@ public protocol FlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// Flight Plan piloting interface for drones. /// /// Allows to make the drone execute predefined flight plans. -/// This piloting interface remains `.unavailable` until all `FlightPlanUnavailabilityReason` have been cleared: -/// - A Flight Plan file (i.e. a mavlink file) has been uploaded to the drone (see uploadFlightPlan(filepath:)) +/// This piloting interface remains `.unavailable` until all `FlightPlanUnavailabilityReason` have +/// been cleared: +/// - A Flight Plan file (i.e. a mavlink file) has been uploaded to the drone (see +/// `uploadFlightPlan(filepath:))` /// - The drone GPS location has been acquired /// - The drone is properly calibrated /// - The drone is in a state that allows it to take off /// -/// Then, when all those conditions hold, the interface becomes `.idle` and can be activated to begin or resume -/// Flight Plan execution, which can be paused by deactivating this piloting interface. +/// Then, when all those conditions hold, the interface becomes `.idle` and can be activated to +/// begin or resume Flight Plan execution, which can be paused by deactivating this piloting +/// interface. /// /// This piloting interface can be retrieved by: /// /// ``` -// id fplan = (id)[drone getPilotingItf:GSPilotingItfs.flightPlan]; +/// id fplan = (id)[drone getPilotingItf:GSPilotingItfs.flightPlan]; /// ``` /// - Note: This protocol is for Objective-C only. Swift must use the protocol `FlightPlanPilotingItf`. @objc @@ -382,21 +436,24 @@ public protocol GSFlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// It is put back to `.none` as soon as `activate(restart:)` is called. var latestActivationError: FlightPlanActivationError { get } - /// Whether the current flight plan on the drone is the latest one that has been uploaded from the application. + /// Whether the current flight plan on the drone is the latest one that has been uploaded from + /// the application. var flightPlanFileIsKnown: Bool { get } /// Whether the flight plan is currently paused. /// - /// If `true`, the restart parameter of `activate(restart:)` can be set to `false` to resume the flight plan instead - /// of playing it from the beginning. If `isPaused` is false, this parameter will be ignored and the flight plan - /// will be played from its beginning. + /// If `true`, the restart parameter of `activate(restart:)` can be set to `false` to resume the + /// flight plan instead of playing it from the beginning. If `isPaused` is false, this parameter + /// will be ignored and the flight plan will be played from its beginning. /// - /// When this piloting interface is deactivated, any currently playing flight plan will be paused. + /// When this piloting interface is deactivated, any currently playing flight plan will be + /// paused. var isPaused: Bool { get } /// Uploads a Flight Plan file to the drone. - /// When the upload ends, if all other necessary conditions hold (GPS location acquired, drone properly calibrated), - /// then the interface becomes idle and the Flight Plan is ready to be executed. + /// When the upload ends, if all other necessary conditions hold (GPS location acquired, drone + /// properly calibrated), then the interface becomes idle and the Flight Plan is ready to be + /// executed. /// /// - Parameter filepath: local path of the file to upload func uploadFlightPlan(filepath: String) @@ -412,7 +469,8 @@ public protocol GSFlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// - Parameter restart: `true` to force restarting the flight plan. /// If `isPaused` is false, this parameter will be ignored. /// - Returns: `true` on success, `false` if the piloting interface can't be activated - /// - Note: activate(restart:) will call activate(restart: type:), default value of type is `flightPlan` + /// - Note: `activate(restart:)` will call `activate(restart: type:)`, default value of type is + /// `flightPlan` func activate(restart: Bool) -> Bool /// Activates this piloting interface and starts executing the uploaded flight plan. @@ -430,10 +488,12 @@ public protocol GSFlightPlanPilotingItf: PilotingItf, ActivablePilotingItf { /// - Returns: `true` on success, `false` if the piloting interface can't be activated func activate(restart: Bool, interpreter: FlightPlanInterpreter) -> Bool - /// Tells whether a given reason is partly responsible of the unavailable state of this piloting interface. + /// Tells whether a given reason is partly responsible of the unavailable state of this piloting + /// interface. /// /// - Parameter reason: the reason to query - /// - Returns: `true` if the piloting interface is partly unavailable because of the given reason. + /// - Returns: `true` if the piloting interface is partly unavailable because of the given + /// reason. func hasUnavailabilityReason(_ reason: FlightPlanUnavailabilityReason) -> Bool } diff --git a/GroundSdk/GroundSdkConfig.swift b/GroundSdk/GroundSdkConfig.swift index 7ac5ad4..6d7b1aa 100644 --- a/GroundSdk/GroundSdkConfig.swift +++ b/GroundSdk/GroundSdkConfig.swift @@ -338,7 +338,7 @@ public class GroundSdkConfig: NSObject { } } - /// Maximum memroy size used by cellular log messages, in kilobytes. + /// Maximum memory size used by cellular log messages, in kilobytes. public var cellularCellularLogsKb: UInt? { willSet(newValue) { checkLocked() diff --git a/GroundSdk/Internal/Device/Instrument/AlarmsCore.swift b/GroundSdk/Internal/Device/Instrument/AlarmsCore.swift index 6b70eef..d1211bc 100644 --- a/GroundSdk/Internal/Device/Instrument/AlarmsCore.swift +++ b/GroundSdk/Internal/Device/Instrument/AlarmsCore.swift @@ -74,6 +74,26 @@ extension AlarmsCore { let alarm = getAlarm(kind: kind) if alarm.level != level { alarm.level = level + if alarm.level == .off { + alarm.timer = nil + } + markChanged() + } + + return self + } + + /// Changes the timer of a given alarm. + /// + /// - Parameters: + /// - timer: the timer of the alarm + /// - forAlarm: kind of the alarm + /// - Returns: self to allow call chaining + /// - Note: Changes are not notified until notifyUpdated() is called. + @discardableResult public func update(timer: TimeInterval, forAlarm kind: Alarm.Kind) -> AlarmsCore { + let alarm = getAlarm(kind: kind) + if alarm.timer != timer { + alarm.timer = timer markChanged() } diff --git a/GroundSdk/Device/Peripheral/FlightCameraRecorder.swift b/GroundSdk/Internal/Device/Peripheral/FlightCameraRecorder.swift similarity index 53% rename from GroundSdk/Device/Peripheral/FlightCameraRecorder.swift rename to GroundSdk/Internal/Device/Peripheral/FlightCameraRecorder.swift index a20f935..4cbad88 100644 --- a/GroundSdk/Device/Peripheral/FlightCameraRecorder.swift +++ b/GroundSdk/Internal/Device/Peripheral/FlightCameraRecorder.swift @@ -29,57 +29,13 @@ import Foundation -/// Flight camera recording setting. -public protocol FlightCameraRecorderSetting: AnyObject { +/// Setting to change the flight camera recording pipelines configuration. +public protocol FlightCameraRecorderPipelinesSetting: AnyObject { /// Tells if setting value has been changed and is waiting for change confirmation. var updating: Bool { get } - /// Supported modes. - var supportedValues: Set { get } - - /// Flight camera recorder pipelines. - var value: Set { get set } -} - -/// Type of pipeline. -public enum FlightCameraRecorderPipeline: Int, RawRepresentable, Hashable, CaseIterable, CustomStringConvertible { - /// Drone left stereo camera pipeline. - case fstcamLeftTimelapse - /// Drone right stereo camera pipeline. - case fstcamRightTimelapse - /// Drone front camera pipeline. - case fcamTimelapse - /// Drone left stereo camera last frames pipeline. - case fstcamLeftEmergency - /// Drone right stereo camera last frames pipeline. - case fstcamRightEmergency - /// Drone front camera last frames pipeline. - case fcamEmergency - /// Drone front camera follow me pipeline. - case fcamFollowme - /// Drone vertical camera precise home pipeline. - case vcamPrecisehome - /// Drone left stereo camera obstacle avoidance pipeline. - case fstcamLeftObstacleavoidance - /// Drone right stereo camera obstacle avoidance pipeline. - case fstcamRightObstacleavoidance - /// Drone vertical camera precise hovering pipeline. - case vcamPrecisehovering - /// Drone left stereo camera love calibration pipeline. - case fstcamLeftCalibration - /// Drone right stereo camera love calibration pipeline. - case fstcamRightCalibration - /// Drone right stereo camera precise hovering pipeline. - case fstcamRightPrecisehovering - /// Drone left stereo camera specific events pipeline. - case fstcamLeftEvent - /// Drone right stereo camera specific events pipeline. - case fstcamRightEvent - /// Drone front camera specific events pipeline. - case fcamEvent - - /// Debug description. - public var description: String { "(\rawValue)" } + /// Flight camera recorder pipelines configuration identifier. + var id: UInt64 { get set } } /// Flight camera recorder peripheral interface for anafi2 drones. @@ -92,10 +48,10 @@ public enum FlightCameraRecorderPipeline: Int, RawRepresentable, Hashable, CaseI /// device.getPeripheral(Peripherals.flightCameraRecorder) /// ``` public protocol FlightCameraRecorder: Peripheral { - /// Gives access to the active pipelines setting. - /// This setting allows to select which recording pipelines are active for flight camera recording. + /// Gives access to the flight camera recording pipelines configuration setting. + /// This setting allows to select current flight camera recording pipelines configuration. /// - Note: This setting remains available when the drone is not connected. - var activePipelines: FlightCameraRecorderSetting { get } + var pipelines: FlightCameraRecorderPipelinesSetting { get } } /// :nodoc: diff --git a/GroundSdk/Internal/Device/Peripheral/FlightCameraRecorderCore.swift b/GroundSdk/Internal/Device/Peripheral/FlightCameraRecorderCore.swift index a55a9b3..d4927d5 100644 --- a/GroundSdk/Internal/Device/Peripheral/FlightCameraRecorderCore.swift +++ b/GroundSdk/Internal/Device/Peripheral/FlightCameraRecorderCore.swift @@ -31,15 +31,15 @@ import Foundation /// Flight camera recorder backend part. public protocol FlightCameraRecorderBackend: AnyObject { - /// Sets active pipelines. + /// Sets flight camera recorder pipeline configuration identifier. /// - /// - Parameter activePipelines: the new set of active pipelines + /// - Parameter pipelineConfigId: the new flight camera recorder pipeline configuration identifier. /// - Returns: true if the command has been sent, false if not connected and the value has been changed immediately - func set(activePipelines: Set) -> Bool + func set(pipelineConfigId: UInt64) -> Bool } -/// Core implementation of FlightCameraRecorderSetting. -class FlightCameraRecorderSettingCore: FlightCameraRecorderSetting, CustomDebugStringConvertible { +/// Core implementation of FlightCameraRecorderPipelinesSetting. +class FlightCameraRecorderPipelinesSettingCore: FlightCameraRecorderPipelinesSetting, CustomDebugStringConvertible { /// Delegate called when the setting value is changed by setting properties private unowned let didChangeDelegate: SettingChangeDelegate @@ -51,24 +51,20 @@ class FlightCameraRecorderSettingCore: FlightCameraRecorderSetting, CustomDebugS /// Tells if the setting value has been changed and is waiting for change confirmation var updating: Bool { return timeout.isScheduled } - /// Supported pipeline values - private(set) var supportedValues: Set = [] - - /// Flight camera recorder value. - var value: Set { + /// Flight camera recorder pipeline configuration identifier. + var id: UInt64 { get { - return _value + return _id } set { - if _value != newValue { - let newSupportedValue = supportedValues.intersection(newValue) - if backend(newSupportedValue) { - let oldValue = _value + if _id != newValue { + if backend(newValue) { + let oldValue = _id // value sent to the backend, update setting value and mark it updating - _value = newSupportedValue + _id = newValue timeout.schedule { [weak self] in - if let `self` = self, self.update(activePipelines: oldValue) { + if let `self` = self, self.update(newId: oldValue) { self.didChangeDelegate.userDidChangeSetting() } } @@ -78,41 +74,29 @@ class FlightCameraRecorderSettingCore: FlightCameraRecorderSetting, CustomDebugS } } - /// Flight camera recorder set of pipelines. - private var _value: Set = [] + /// Flight camera recorder pipeline configuration identifier. + private var _id = UInt64(0) /// Closure to call to change the value - private let backend: ((Set) -> Bool) + private let backend: ((UInt64) -> Bool) /// Constructor /// /// - Parameters: /// - didChangeDelegate: delegate called when the setting value is changed by setting properties /// - backend: closure to call to change the setting value - init(didChangeDelegate: SettingChangeDelegate, backend: @escaping (Set) -> Bool) { + init(didChangeDelegate: SettingChangeDelegate, backend: @escaping (UInt64) -> Bool) { self.didChangeDelegate = didChangeDelegate self.backend = backend } - /// Changes flight camera recorder supported pipeline values. - /// - /// - Parameter supportedValues: new set of supported pipelines - /// - Returns: true if the setting has been changed, false otherwise - func update(supportedValues newSupportedValues: Set) -> Bool { - if supportedValues != newSupportedValues { - supportedValues = newSupportedValues - return true - } - return false - } - - /// Changes flight camera recorder pipelines. + /// Changes flight camera recorder pipelines configuration identifier. /// /// - Parameter activePipelines: new set of active pipelines /// - Returns: true if the setting has been changed, false otherwise - func update(activePipelines newActivePipelines: Set) -> Bool { - if updating || _value != newActivePipelines { - _value = newActivePipelines + func update(newId: UInt64) -> Bool { + if updating || _id != newId { + _id = newId timeout.cancel() return true } @@ -131,7 +115,7 @@ class FlightCameraRecorderSettingCore: FlightCameraRecorderSetting, CustomDebugS /// Debug description. var debugDescription: String { - return "\(value.description) [updating: \(updating)]" + return "\(id) [updating: \(updating)]" } } @@ -146,12 +130,12 @@ public class FlightCameraRecorderCore: PeripheralCore, FlightCameraRecorder { /// Tells if the setting value has been changed and is waiting for change confirmation var updating: Bool { return timeout.isScheduled } - public var activePipelines: FlightCameraRecorderSetting { - return _activePipelines + public var pipelines: FlightCameraRecorderPipelinesSetting { + return _pipelines } /// Internal storage for active pipelines setting. - private var _activePipelines: FlightCameraRecorderSettingCore! + private var _pipelines: FlightCameraRecorderPipelinesSettingCore! /// Implementation backend private unowned let backend: FlightCameraRecorderBackend @@ -164,35 +148,21 @@ public class FlightCameraRecorderCore: PeripheralCore, FlightCameraRecorder { public init(store: ComponentStoreCore, backend: FlightCameraRecorderBackend) { self.backend = backend super.init(desc: Peripherals.flightCameraRecorder, store: store) - _activePipelines = FlightCameraRecorderSettingCore( - didChangeDelegate: self) { [unowned self] newActivePipelines in - return self.backend.set(activePipelines: newActivePipelines) + _pipelines = FlightCameraRecorderPipelinesSettingCore( + didChangeDelegate: self) { [unowned self] id in + return self.backend.set(pipelineConfigId: id) } } } extension FlightCameraRecorderCore { - /// Called by the backend, set the supported pipeline values - /// - /// - Parameter supportedValues: new supported pipeline values - /// - Returns: self to allow call chaining - /// - Note: Changes are not notified until notifyUpdated() is called. - @discardableResult public func update(supportedValues - newSupportedValues: Set) -> FlightCameraRecorderCore { - if _activePipelines.update(supportedValues: newSupportedValues) { - markChanged() - } - return self - } - /// Called by the backend, change the setting data /// - /// - Parameter activePipelines: new set of active pipelines + /// - Parameter pipelineConfigId: new pipeline configuration identifier /// - Returns: self to allow call chaining - @discardableResult public func update(activePipelines - newActivePipelines: Set) -> FlightCameraRecorderCore { - if _activePipelines.update(activePipelines: newActivePipelines) { + @discardableResult public func update(pipelineConfigId: UInt64) -> FlightCameraRecorderCore { + if _pipelines.update(newId: pipelineConfigId) { markChanged() } return self @@ -203,7 +173,7 @@ extension FlightCameraRecorderCore { /// - Returns: self to allow call chaining /// - note: changes are not notified until notifyUpdated() is called @discardableResult public func cancelSettingsRollback() -> FlightCameraRecorderCore { - _activePipelines.cancelRollback { markChanged() } + _pipelines.cancelRollback { markChanged() } return self } } diff --git a/GroundSdk/Internal/Device/PilotingItf/FlightPlanPilotingItfCore.swift b/GroundSdk/Internal/Device/PilotingItf/FlightPlanPilotingItfCore.swift index b04ea73..1a12d26 100644 --- a/GroundSdk/Internal/Device/PilotingItf/FlightPlanPilotingItfCore.swift +++ b/GroundSdk/Internal/Device/PilotingItf/FlightPlanPilotingItfCore.swift @@ -36,9 +36,12 @@ public protocol FlightPlanPilotingItfBackend: ActivablePilotingItfBackend { /// - Parameters: /// - restart: `true` to force restarting the flight plan. /// - interpreter: instructs how the flight plan must be interpreted by the drone. - /// - missionItem: index of mission item where the flight plan should start, `nil` if should start from beginning + /// - missionItem: index of mission item where the flight plan should start, `nil` if should + /// start from beginning + /// - disconnectionPolicy: the behavior of the drone when a disconnection occurs. /// - Returns: `true` on success, false if the piloting interface can't be activated - func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt?) -> Bool + func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt?, + disconnectionPolicy: FlightPlanDisconnectionPolicy) -> Bool /// Stops execution of current flight plan. /// @@ -54,7 +57,8 @@ public protocol FlightPlanPilotingItfBackend: ActivablePilotingItfBackend { /// - customFlightPlanId: custom flight plan id func uploadFlightPlan(filepath: String, customFlightPlanId: String) - /// Clears information about the latest flight plan started by the drone prior to current connection. + /// Clears information about the latest flight plan started by the drone prior to current + /// connection. func clearRecoveryInfo() /// Cancels any on-going upload. @@ -66,8 +70,8 @@ public protocol FlightPlanPilotingItfBackend: ActivablePilotingItfBackend { /// /// - Parameters: /// - customId: custom identifier, as provided by `recoveryInfo` - /// - resourceId: first resource identifier of media captured after the latest reached waypoint, as provided - /// by `recoveryInfo` + /// - resourceId: first resource identifier of media captured after the latest reached + /// waypoint, as provided by `recoveryInfo` /// - completion: completion callback /// - result: media resources clean result /// - Returns: a clean media resources cancelable request @@ -98,6 +102,8 @@ public class FlightPlanPilotingItfCore: ActivablePilotingItfCore, FlightPlanPilo private(set) public var activateAtMissionItemSupported = false + private(set) public var activateAtMissionItemV2Supported = false + private(set) public var isUploadWithCustomIdSupported = false /// Super class backend as FlightPlanPilotingItfBackend @@ -115,24 +121,43 @@ public class FlightPlanPilotingItfCore: ActivablePilotingItfCore, FlightPlanPilo } public func activate(restart: Bool) -> Bool { - if state == .idle { - return flightPlanBackend.activate(restart: restart, interpreter: .legacy, missionItem: nil) + return commonActivate(restart: restart) + } + + public func activate(restart: Bool, interpreter: FlightPlanInterpreter) -> Bool { + return commonActivate(restart: restart, interpreter: interpreter) + } + + public func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt) -> Bool { + if activateAtMissionItemSupported { + return commonActivate(restart: restart, + interpreter: interpreter, + missionItem: missionItem) } return false } - public func activate(restart: Bool, interpreter: FlightPlanInterpreter) -> Bool { - if state == .idle { - return flightPlanBackend.activate(restart: restart, interpreter: interpreter, missionItem: nil) + public func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt, + disconnectionPolicy: FlightPlanDisconnectionPolicy) -> Bool { + if activateAtMissionItemV2Supported { + return commonActivate(restart: restart, + interpreter: interpreter, + missionItem: missionItem, + disconnectionPolicy: disconnectionPolicy) } return false } - public func activate(restart: Bool, interpreter: FlightPlanInterpreter, missionItem: UInt) -> Bool { - if state == .idle && activateAtMissionItemSupported { + /// Regroups all activation demands to the flight plan backend. + private func commonActivate(restart: Bool, + interpreter: FlightPlanInterpreter = .legacy, + missionItem: UInt? = nil, + disconnectionPolicy: FlightPlanDisconnectionPolicy = .returnToHome) -> Bool { + if state == .idle { return flightPlanBackend.activate(restart: restart, interpreter: interpreter, - missionItem: missionItem) + missionItem: missionItem, + disconnectionPolicy: disconnectionPolicy) } return false } @@ -299,7 +324,7 @@ extension FlightPlanPilotingItfCore { /// /// - Parameter isPaused: true if the flight plan is currently paused, false otherwise /// - Returns: self to allow call chaining - /// - Note: Changes are not notified until notifyUpdated() is called. + /// - Note: Changes are not notified until `notifyUpdated()` is called. @discardableResult public func update(isPaused newValue: Bool) -> FlightPlanPilotingItfCore { if isPaused != newValue { isPaused = newValue @@ -312,7 +337,7 @@ extension FlightPlanPilotingItfCore { /// /// - Parameter activateAtMissionItemSupported: `true` if supported, `false` otherwise /// - Returns: self to allow call chaining - /// - Note: Changes are not notified until notifyUpdated() is called. + /// - Note: Changes are not notified until `notifyUpdated()` is called. @discardableResult public func update(activateAtMissionItemSupported newValue: Bool) -> FlightPlanPilotingItfCore { if activateAtMissionItemSupported != newValue { @@ -322,11 +347,26 @@ extension FlightPlanPilotingItfCore { return self } + /// Updates capability to start a flight plan at a given mission item with a disconnection + /// policy. + /// + /// - Parameter activateAtMissionItemV2Supported: `true` if supported, `false` otherwise + /// - Returns: self to allow call chaining + /// - Note: Changes are not notified until `notifyUpdated()` is called. + @discardableResult + public func update(activateAtMissionItemV2Supported newValue: Bool) -> FlightPlanPilotingItfCore { + if activateAtMissionItemV2Supported != newValue { + activateAtMissionItemV2Supported = newValue + markChanged() + } + return self + } + /// Updates capability to start a flight plan at a given mission item, with custom id. /// /// - Parameter isUploadWithCustomIdSupported: `true` if supported, `false` otherwise /// - Returns: self to allow call chaining - /// - Note: Changes are not notified until notifyUpdated() is called. + /// - Note: Changes are not notified until `notifyUpdated()` is called. @discardableResult public func update(isUploadWithCustomIdSupported newValue: Bool) -> FlightPlanPilotingItfCore { if isUploadWithCustomIdSupported != newValue { diff --git a/GroundSdk/Internal/Engine/BlackBox/BlackBoxCollector.swift b/GroundSdk/Internal/Engine/BlackBox/BlackBoxCollector.swift index 799c6d4..98f9454 100644 --- a/GroundSdk/Internal/Engine/BlackBox/BlackBoxCollector.swift +++ b/GroundSdk/Internal/Engine/BlackBox/BlackBoxCollector.swift @@ -55,6 +55,9 @@ class BlackBoxCollector { /// File extension of a non finalized report fileprivate static let nonFinalizedFileExtension = "tmp" + /// Whether collection has been cancelled. + private var isCancelled: Bool = false + /// Blackbox public folder private var blackboxPublicFolder: String? = GroundSdkConfig.sharedInstance.blackboxPublicFolder @@ -104,6 +107,7 @@ class BlackBoxCollector { let reports = try? FileManager.default.contentsOfDirectory( at: dir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) reports?.forEach { report in + guard !self.isCancelled else { return } // if the report is finalized if report.isAFinalizedBlackBox { // keep the parent folder @@ -123,11 +127,18 @@ class BlackBoxCollector { } DispatchQueue.main.async { - completionCallback(toUpload) + if !self.isCancelled { + completionCallback(toUpload) + } } } } + /// Cancels black box collection. + func cancelCollection() { + isCancelled = true + } + /// Delete a black box in background. /// /// - Parameter url: url of the black box report to delete diff --git a/GroundSdk/Internal/Engine/BlackBoxEngine.swift b/GroundSdk/Internal/Engine/BlackBoxEngine.swift index b792271..02562ee 100644 --- a/GroundSdk/Internal/Engine/BlackBoxEngine.swift +++ b/GroundSdk/Internal/Engine/BlackBoxEngine.swift @@ -66,7 +66,7 @@ class BlackBoxEngine: EngineBaseCore { private var userAccountInfo: UserAccountInfoCore? /// Black box reports collector - private var collector: BlackBoxCollector! + private var collector: BlackBoxCollector? /// List of reports waiting for upload. /// @@ -102,7 +102,6 @@ class BlackBoxEngine: EngineBaseCore { super.init(enginesController: enginesController) publishUtility(BlackBoxStorageCoreImpl(engine: self)) - collector = createCollector() } public override func startEngine() { @@ -121,9 +120,11 @@ class BlackBoxEngine: EngineBaseCore { ULog.d(.myparrot, "User account change with private mode or old data upload denied -> delete all black boxes") self.stopAndDropAllBlackBoxes() + self.userAccountInfo = newInfo + } else { + self.userAccountInfo = newInfo + self.startBlackBoxUploadProcess() } - self.userAccountInfo = newInfo - self.startBlackBoxUploadProcess() }) if spaceQuotaInMb != 0 { @@ -131,7 +132,8 @@ class BlackBoxEngine: EngineBaseCore { totalMaxSizeMb: spaceQuotaInMb, includingSubfolders: true) } - collector.collectBlackBoxes { [weak self] blackBoxes in + collector = createCollector() + collector?.collectBlackBoxes { [weak self] blackBoxes in if let `self` = self, self.started { self.pendingReports.append(contentsOf: blackBoxes) self.startBlackBoxUploadProcess() @@ -160,6 +162,9 @@ class BlackBoxEngine: EngineBaseCore { blackBoxReporter.unpublish() cancelCurrentUpload() uploader = nil + collector?.cancelCollection() + collector = nil + pendingReports = [] connectivityMonitor.stop() } @@ -170,7 +175,7 @@ class BlackBoxEngine: EngineBaseCore { guard userAccountInfo?.privateMode == false else { return } - collector.archive(blackBoxData: blackBoxData) { [weak self] report in + collector?.archive(blackBoxData: blackBoxData) { [weak self] report in self?.pendingReports.append(report) ULog.d(.myparrot, "BLACKBOX append \(report)") self?.startBlackBoxUploadProcess() @@ -282,17 +287,17 @@ class BlackBoxEngine: EngineBaseCore { /// - Parameter blackBox: the black box report to delete private func deleteBlackBox(_ blackBox: BlackBox) { ULog.d(.myparrot, "BLACKBOX deleteBlackBox \(blackBox)") - if self.pendingReports.first == blackBox { - self.pendingReports.remove(at: 0) + if pendingReports.first == blackBox { + pendingReports.remove(at: 0) } else { ULog.w(.blackBoxEngineTag, "Uploaded report is not the first one of the pending") // fallback - if let index: Int = self.pendingReports.firstIndex(where: {$0 == blackBox}) { - self.pendingReports.remove(at: index) + if let index: Int = pendingReports.firstIndex(where: {$0 == blackBox}) { + pendingReports.remove(at: index) } } - self.collector.deleteBlackBox(at: blackBox.url) + collector?.deleteBlackBox(at: blackBox.url) } /// Cancel the current upload if there is one. @@ -313,7 +318,7 @@ class BlackBoxEngine: EngineBaseCore { cancelCurrentUpload() pendingReports.forEach { (blackBox) in - collector.deleteBlackBox(at: blackBox.url) + collector?.deleteBlackBox(at: blackBox.url) } // clear all pending blackBoxes diff --git a/GroundSdk/Internal/Engine/CrashReport/CrashReportCollector.swift b/GroundSdk/Internal/Engine/CrashReport/CrashReportCollector.swift index 8610ebc..65fcbce 100644 --- a/GroundSdk/Internal/Engine/CrashReport/CrashReportCollector.swift +++ b/GroundSdk/Internal/Engine/CrashReport/CrashReportCollector.swift @@ -48,6 +48,9 @@ class CrashReportCollector { /// This directory should not be scanned nor deleted because reports might be currently downloading in it. private let reportsLocalWorkDir: URL + /// Whether collection has been cancelled. + private var isCancelled: Bool = false + /// Constructor /// /// - Parameters: @@ -94,6 +97,7 @@ class CrashReportCollector { let reportDirs = try? FileManager.default.contentsOfDirectory( at: dir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) reportDirs?.forEach { reportUrl in + guard !self.isCancelled else { return } // if the report is finalized if reportUrl.isAFinalizedCrashReport { // keep the parent folder @@ -113,11 +117,18 @@ class CrashReportCollector { } DispatchQueue.main.async { - completionCallback(toUpload) + if !self.isCancelled { + completionCallback(toUpload) + } } } } + /// Cancels crash report collection. + func cancelCollection() { + isCancelled = true + } + /// Delete a crash report in background. /// /// - Parameter url: url of the crash report to delete diff --git a/GroundSdk/Internal/Engine/CrashReportEngine.swift b/GroundSdk/Internal/Engine/CrashReportEngine.swift index 485ce66..6a7f326 100644 --- a/GroundSdk/Internal/Engine/CrashReportEngine.swift +++ b/GroundSdk/Internal/Engine/CrashReportEngine.swift @@ -66,7 +66,7 @@ class CrashReportEngine: EngineBaseCore { private var userAccountInfo: UserAccountInfoCore? /// Crash reports file collector. - private var collector: CrashReportCollector! + private var collector: CrashReportCollector? /// List of reports waiting for upload. /// @@ -102,7 +102,6 @@ class CrashReportEngine: EngineBaseCore { super.init(enginesController: enginesController) publishUtility(CrashReportStorageCoreImpl(engine: self)) - collector = createCollector() } public override func startEngine() { @@ -125,9 +124,11 @@ class CrashReportEngine: EngineBaseCore { ULog.d(.myparrot, "User account change with private mode or old data upload denied -> delete all reports") self.dropReports() + self.userAccountInfo = newInfo + } else { + self.userAccountInfo = newInfo + self.startReportUploadProcess() } - self.userAccountInfo = newInfo - self.startReportUploadProcess() }) if spaceQuotaInMb != 0 { @@ -135,7 +136,8 @@ class CrashReportEngine: EngineBaseCore { totalMaxSizeMb: spaceQuotaInMb, includingSubfolders: true) } - collector.collectCrashReports { [weak self] crashReports in + collector = createCollector() + collector?.collectCrashReports { [weak self] crashReports in if let `self` = self, self.started { self.pendingReportUrls.append(contentsOf: crashReports) self.startReportUploadProcess() @@ -163,6 +165,9 @@ class CrashReportEngine: EngineBaseCore { crashReporter.unpublish() cancelCurrentUpload() uploader = nil + collector?.cancelCollection() + collector = nil + pendingReportUrls = [] connectivityMonitor.stop() } @@ -277,23 +282,23 @@ class CrashReportEngine: EngineBaseCore { /// /// - Parameter report: the crash report to delete private func deleteCrashReport(at reportUrl: URL) { - if self.pendingReportUrls.first == reportUrl { - self.pendingReportUrls.remove(at: 0) + if pendingReportUrls.first == reportUrl { + pendingReportUrls.remove(at: 0) } else { ULog.w(.crashReportEngineTag, "Uploaded report is not the first one of the pending") // fallback - if let index: Int = self.pendingReportUrls.firstIndex(where: {$0 == reportUrl}) { - self.pendingReportUrls.remove(at: index) + if let index: Int = pendingReportUrls.firstIndex(where: {$0 == reportUrl}) { + pendingReportUrls.remove(at: index) } } - self.collector.deleteCrashReport(at: reportUrl) + collector?.deleteCrashReport(at: reportUrl) if reportUrl.pathExtension == "gz" { let urlLight = URL(fileURLWithPath: reportUrl.path + ".anon") - if let index: Int = self.pendingReportUrls.firstIndex(where: {$0 == urlLight}) { - self.pendingReportUrls.remove(at: index) - self.collector.deleteCrashReport(at: urlLight) + if let index: Int = pendingReportUrls.firstIndex(where: {$0 == urlLight}) { + pendingReportUrls.remove(at: index) + collector?.deleteCrashReport(at: urlLight) } } } @@ -315,7 +320,7 @@ class CrashReportEngine: EngineBaseCore { cancelCurrentUpload() pendingReportUrls.forEach { (reportUrl) in - collector.deleteCrashReport(at: reportUrl) + collector?.deleteCrashReport(at: reportUrl) } // clear all pending reports diff --git a/GroundSdk/Internal/Engine/FlightCameraRecord/FlightCameraRecordCollector.swift b/GroundSdk/Internal/Engine/FlightCameraRecord/FlightCameraRecordCollector.swift index 5cf89e1..904ef61 100644 --- a/GroundSdk/Internal/Engine/FlightCameraRecord/FlightCameraRecordCollector.swift +++ b/GroundSdk/Internal/Engine/FlightCameraRecord/FlightCameraRecordCollector.swift @@ -48,6 +48,9 @@ class FlightCameraRecordCollector { /// This directory should not be scanned nor deleted because records might be currently downloading in it. private let workDir: URL + /// Whether collection has been cancelled. + private var isCancelled: Bool = false + /// Constructor /// /// - Parameters: @@ -95,6 +98,7 @@ class FlightCameraRecordCollector { let recordUrls = try? FileManager.default.contentsOfDirectory( at: dir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) recordUrls?.forEach { recordUrl in + guard !self.isCancelled else { return } if recordUrl.isProcessing { FlightLogEngineBase.recover(file: recordUrl) GroundSdkCore.logEvent( @@ -116,11 +120,18 @@ class FlightCameraRecordCollector { } DispatchQueue.main.async { - completionCallback(toUpload) + if !self.isCancelled { + completionCallback(toUpload) + } } } } + /// Cancels flight camera record collection. + func cancelCollection() { + isCancelled = true + } + /// Delete a flight camera record in background. /// /// - Parameter url: url of the flight camera record to delete diff --git a/GroundSdk/Internal/Engine/FlightCameraRecordEngine.swift b/GroundSdk/Internal/Engine/FlightCameraRecordEngine.swift index 95a0e02..8443e77 100644 --- a/GroundSdk/Internal/Engine/FlightCameraRecordEngine.swift +++ b/GroundSdk/Internal/Engine/FlightCameraRecordEngine.swift @@ -138,7 +138,7 @@ class FlightCameraRecordEngine: EngineBaseCore { private var userAccountInfo: UserAccountInfoCore? /// Flight camera records file collector. - private var collector: FlightCameraRecordCollector! + private var collector: FlightCameraRecordCollector? /// List of flight camera records waiting for upload. /// @@ -184,7 +184,6 @@ class FlightCameraRecordEngine: EngineBaseCore { super.init(enginesController: enginesController) publishUtility(FlightCameraRecordStorageCoreImpl(engine: self)) - collector = createCollector() } /// Flight camera anonymizer class init. @@ -232,9 +231,11 @@ class FlightCameraRecordEngine: EngineBaseCore { ULog.d(.myparrot, "User account change with private mode or old data upload denied -> delete all records") self.dropFlightCameraRecords() + self.userAccountInfo = newInfo + } else { + self.userAccountInfo = newInfo + self.startFlightCameraRecordUploadProcess() } - self.userAccountInfo = newInfo - self.startFlightCameraRecordUploadProcess() }) if spaceQuotaInMb != 0 { @@ -244,7 +245,8 @@ class FlightCameraRecordEngine: EngineBaseCore { totalMaxSizeMb: spaceQuotaInMb, includingSubfolders: true) } - collector.collectFlightCameraRecords { [weak self] flightCameraRecords in + collector = createCollector() + collector?.collectFlightCameraRecords { [weak self] flightCameraRecords in if let `self` = self, self.started { ULog.d(.parrotCloudFcrTag, "Records locally collected: \(flightCameraRecords)") self.pendingFlightCameraRecordUrls.append(contentsOf: flightCameraRecords) @@ -276,9 +278,10 @@ class FlightCameraRecordEngine: EngineBaseCore { flightCameraRecordReporter.unpublish() cancelCurrentUpload() uploader = nil - if connectivityMonitor != nil { - connectivityMonitor.stop() - } + collector?.cancelCollection() + collector = nil + pendingFlightCameraRecordUrls = [] + connectivityMonitor?.stop() } /// Adds a flightCameraRecord to the flight camera records to be uploaded. @@ -541,7 +544,7 @@ class FlightCameraRecordEngine: EngineBaseCore { ULog.w(.parrotCloudFcrTag, "FCR not found in the pending list") } } - collector.deleteFlightCameraRecord(at: fcrUrl) + collector?.deleteFlightCameraRecord(at: fcrUrl) GroundSdkCore.logEvent(message: "EVT:LOGS;event='delete';reason='\(reason)';file='\(fcrUrl.lastPathComponent)'") } @@ -553,8 +556,8 @@ class FlightCameraRecordEngine: EngineBaseCore { /// - fcr: the record to delete /// - reason: the reason why the record should be deleted private func deleteFlightCameraRecordFile(at fcr: FlightCameraRecordFile, reason: String) { - collector.deleteFlightCameraRecord(at: fcr.blurFile) - collector.deleteFlightCameraRecord(at: fcr.jsonFile) + collector?.deleteFlightCameraRecord(at: fcr.blurFile) + collector?.deleteFlightCameraRecord(at: fcr.jsonFile) deleteFlightCameraRecord(at: fcr.originalFile, reason: reason) } @@ -575,7 +578,7 @@ class FlightCameraRecordEngine: EngineBaseCore { cancelCurrentUpload() pendingFlightCameraRecordUrls.forEach { url in - collector.deleteFlightCameraRecord(at: url) + collector?.deleteFlightCameraRecord(at: url) GroundSdkCore.logEvent(message: "EVT:LOGS;event='delete';reason='denied';file='\(url.lastPathComponent)'") } diff --git a/GroundSdk/Internal/Engine/FlightLog/FlightLogCollector.swift b/GroundSdk/Internal/Engine/FlightLog/FlightLogCollector.swift index 99e1910..cf0cf25 100644 --- a/GroundSdk/Internal/Engine/FlightLog/FlightLogCollector.swift +++ b/GroundSdk/Internal/Engine/FlightLog/FlightLogCollector.swift @@ -48,6 +48,9 @@ class FlightLogCollector { /// This directory should not be scanned nor deleted because reports might be currently downloading in it. private let flightLogsLocalWorkDir: URL + /// Whether collection has been cancelled. + private var isCancelled: Bool = false + /// Constructor /// /// - Parameters: @@ -95,6 +98,7 @@ class FlightLogCollector { let logUrls = try? FileManager.default.contentsOfDirectory( at: dir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) logUrls?.forEach { logUrl in + guard !self.isCancelled else { return } if logUrl.isConverting { // if the report crashed while converting to gutma. FlightLogEngineBase.recover(file: logUrl) GroundSdkCore.logEvent( @@ -122,11 +126,18 @@ class FlightLogCollector { } DispatchQueue.main.async { - completionCallback(toUpload) + if !self.isCancelled { + completionCallback(toUpload) + } } } } + /// Cancels flightLog collection. + func cancelCollection() { + isCancelled = true + } + /// Delete a flightLog report in background. /// /// - Parameter url: url of the flightLog report to delete diff --git a/GroundSdk/Internal/Engine/FlightLogEngine.swift b/GroundSdk/Internal/Engine/FlightLogEngine.swift index 5f848ba..8cdeea4 100644 --- a/GroundSdk/Internal/Engine/FlightLogEngine.swift +++ b/GroundSdk/Internal/Engine/FlightLogEngine.swift @@ -87,9 +87,11 @@ class FlightLogEngine: FlightLogEngineBase { ULog.d(.myparrot, "User account change with private mode or old data upload denied -> delete all flight logs") self.dropFlightLogs() + self.userAccountInfo = newInfo + } else { + self.userAccountInfo = newInfo + self.startFlightLogUploadProcess() } - self.userAccountInfo = newInfo - self.startFlightLogUploadProcess() }) if spaceQuotaInMb != 0 { @@ -324,7 +326,7 @@ class FlightLogEngine: FlightLogEngineBase { var flightLogUrlReal = flightLogUrl if flightLogUrl.pathExtension == "anon" { /// remove anon flightLog - self.collector.deleteFlightLog(at: flightLogUrl) + collector?.deleteFlightLog(at: flightLogUrl) flightLogUrlReal = URL(fileURLWithPath: flightLogUrl.path).deletingPathExtension() } @@ -339,7 +341,7 @@ class FlightLogEngine: FlightLogEngineBase { self.pendingFlightLogUrls.remove(at: index) } } - self.collector.deleteFlightLog(at: flightLogUrlReal) + collector?.deleteFlightLog(at: flightLogUrlReal) GroundSdkCore.logEvent(message: "EVT:LOGS;event='delete';reason='\(reason)';" + "file='\(flightLogUrlReal.lastPathComponent)'") } @@ -354,7 +356,7 @@ class FlightLogEngine: FlightLogEngineBase { cancelCurrentUpload() pendingFlightLogUrls.forEach { (flightLogUrl) in - collector.deleteFlightLog(at: flightLogUrl) + collector?.deleteFlightLog(at: flightLogUrl) GroundSdkCore.logEvent(message: "EVT:LOGS;event='delete';reason='denied';" + "file='\(flightLogUrl.lastPathComponent)'") } diff --git a/GroundSdk/Internal/Engine/FlightLogEngineBase.swift b/GroundSdk/Internal/Engine/FlightLogEngineBase.swift index 9a7d52c..763964d 100644 --- a/GroundSdk/Internal/Engine/FlightLogEngineBase.swift +++ b/GroundSdk/Internal/Engine/FlightLogEngineBase.swift @@ -64,7 +64,7 @@ class FlightLogEngineBase: EngineBaseCore { public var currentUploadRequest: CancelableCore? /// Flight logs collector. - public var collector: FlightLogCollector! + public var collector: FlightLogCollector? /// Constructor /// @@ -76,7 +76,6 @@ class FlightLogEngineBase: EngineBaseCore { engineDir = cacheDirUrl.appendingPathComponent(engineDirName, isDirectory: true) workDir = engineDir.appendingPathComponent(UUID().uuidString, isDirectory: true) super.init(enginesController: enginesController) - collector = createCollector() } /// Required Constructor @@ -89,11 +88,11 @@ class FlightLogEngineBase: EngineBaseCore { engineDir = cacheDirUrl.appendingPathComponent("default", isDirectory: true) workDir = engineDir.appendingPathComponent(UUID().uuidString, isDirectory: true) super.init(enginesController: enginesController) - collector = createCollector() } public override func startEngine() { - collector.collectFlightLogs { [weak self] flightLogs in + collector = createCollector() + collector?.collectFlightLogs { [weak self] flightLogs in if let `self` = self, self.started { self.pendingFlightLogUrls.append(contentsOf: flightLogs) self.queueForProcessing() @@ -103,6 +102,9 @@ class FlightLogEngineBase: EngineBaseCore { public override func stopEngine() { cancelCurrentUpload() + collector?.cancelCollection() + collector = nil + pendingFlightLogUrls = [] } /// Cancel the current upload if there is one. diff --git a/GroundSdk/Internal/Facility/BlackBoxReporterCore.swift b/GroundSdk/Internal/Facility/BlackBoxReporterCore.swift index 5769b9a..8e26c10 100644 --- a/GroundSdk/Internal/Facility/BlackBoxReporterCore.swift +++ b/GroundSdk/Internal/Facility/BlackBoxReporterCore.swift @@ -42,6 +42,11 @@ class BlackBoxReporterCore: FacilityCore, BlackBoxReporter { init(store: ComponentStoreCore) { super.init(desc: Facilities.blackBoxReporter, store: store) } + + override func reset() { + pendingCount = 0 + isUploading = false + } } /// Backend callback methods diff --git a/GroundSdk/Internal/Facility/CrashReporterCore.swift b/GroundSdk/Internal/Facility/CrashReporterCore.swift index a1d6208..512a220 100644 --- a/GroundSdk/Internal/Facility/CrashReporterCore.swift +++ b/GroundSdk/Internal/Facility/CrashReporterCore.swift @@ -42,6 +42,11 @@ class CrashReporterCore: FacilityCore, CrashReporter { init(store: ComponentStoreCore) { super.init(desc: Facilities.crashReporter, store: store) } + + override func reset() { + pendingCount = 0 + isUploading = false + } } /// Backend callback methods diff --git a/GroundSdk/Internal/Facility/FlightCameraRecordReporterCore.swift b/GroundSdk/Internal/Facility/FlightCameraRecordReporterCore.swift index a5a56f6..925d42a 100644 --- a/GroundSdk/Internal/Facility/FlightCameraRecordReporterCore.swift +++ b/GroundSdk/Internal/Facility/FlightCameraRecordReporterCore.swift @@ -42,6 +42,11 @@ class FlightCameraRecordReporterCore: FacilityCore, FlightCameraRecordReporter { init(store: ComponentStoreCore) { super.init(desc: Facilities.flightCameraRecordReporter, store: store) } + + override func reset() { + pendingCount = 0 + isUploading = false + } } /// Backend callback methods diff --git a/GroundSdk/Internal/Facility/FlightLogReporterCore.swift b/GroundSdk/Internal/Facility/FlightLogReporterCore.swift index 3ea3697..b5bc95f 100644 --- a/GroundSdk/Internal/Facility/FlightLogReporterCore.swift +++ b/GroundSdk/Internal/Facility/FlightLogReporterCore.swift @@ -42,6 +42,11 @@ class FlightLogReporterCore: FacilityCore, FlightLogReporter { init(store: ComponentStoreCore) { super.init(desc: Facilities.flightLogReporter, store: store) } + + override func reset() { + pendingCount = 0 + isUploading = false + } } /// Backend callback methods