2020-06-07 12:22:32 +02:00
|
|
|
//
|
|
|
|
|
// reader.swift
|
2021-06-04 19:37:29 +02:00
|
|
|
// Kit
|
2020-06-07 12:22:32 +02:00
|
|
|
//
|
|
|
|
|
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
|
|
|
|
// Using Swift 5.0.
|
|
|
|
|
// Running on macOS 10.15.
|
|
|
|
|
//
|
|
|
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Cocoa
|
|
|
|
|
|
|
|
|
|
public protocol Reader_p {
|
2020-07-27 20:57:16 +02:00
|
|
|
var popup: Bool { get }
|
2026-03-12 16:52:00 +01:00
|
|
|
var preview: Bool { get }
|
|
|
|
|
var sleep: Bool { get }
|
2020-06-07 12:22:32 +02:00
|
|
|
|
2021-05-22 14:58:20 +02:00
|
|
|
func setup()
|
|
|
|
|
func read()
|
|
|
|
|
func terminate()
|
2020-06-07 12:22:32 +02:00
|
|
|
|
2021-05-22 14:58:20 +02:00
|
|
|
func start()
|
|
|
|
|
func pause()
|
|
|
|
|
func stop()
|
2020-07-10 22:56:47 +02:00
|
|
|
|
2021-05-22 14:58:20 +02:00
|
|
|
func lock()
|
|
|
|
|
func unlock()
|
2020-07-27 20:57:16 +02:00
|
|
|
|
2021-05-22 14:58:20 +02:00
|
|
|
func initStoreValues(title: String)
|
|
|
|
|
func setInterval(_ value: Int)
|
2026-03-12 16:52:00 +01:00
|
|
|
func sleepMode(state: Bool)
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public protocol ReaderInternal_p {
|
|
|
|
|
associatedtype T
|
|
|
|
|
|
|
|
|
|
var value: T? { get }
|
2021-05-22 14:58:20 +02:00
|
|
|
func read()
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
|
2023-06-29 17:46:40 +02:00
|
|
|
open class Reader<T: Codable>: NSObject, ReaderInternal_p {
|
2021-06-26 13:38:45 +02:00
|
|
|
public var log: NextLog {
|
2023-06-29 17:46:40 +02:00
|
|
|
NextLog.shared.copy(category: "\(String(describing: self))")
|
2021-06-26 13:38:45 +02:00
|
|
|
}
|
2025-03-14 21:17:23 +01:00
|
|
|
private let valueQueue = DispatchQueue(label: "eu.exelban.readerActiveQueue")
|
|
|
|
|
private var _value: T?
|
|
|
|
|
public var value: T? {
|
|
|
|
|
get { self.valueQueue.sync { self._value } }
|
|
|
|
|
set { self.valueQueue.sync { self._value = newValue } }
|
|
|
|
|
}
|
2023-06-29 17:46:40 +02:00
|
|
|
public var name: String {
|
|
|
|
|
String(NSStringFromClass(type(of: self)).split(separator: ".").last ?? "unknown")
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-10 22:56:47 +02:00
|
|
|
public var interval: Double? = nil
|
2025-10-04 13:44:16 +02:00
|
|
|
public var defaultInterval: Int = 1
|
2020-06-07 12:22:32 +02:00
|
|
|
public var optional: Bool = false
|
2020-07-27 20:57:16 +02:00
|
|
|
public var popup: Bool = false
|
2026-03-12 16:52:00 +01:00
|
|
|
public var preview: Bool = false
|
|
|
|
|
public var sleep: Bool = false
|
2020-06-07 12:22:32 +02:00
|
|
|
|
2026-03-13 19:46:47 +01:00
|
|
|
public var alignToSecondBoundary: Bool = false
|
|
|
|
|
public var alignOffset: TimeInterval = 0
|
|
|
|
|
|
2024-02-08 17:35:01 +01:00
|
|
|
public var callbackHandler: (T?) -> Void
|
2020-06-07 12:22:32 +02:00
|
|
|
|
2024-02-08 17:35:01 +01:00
|
|
|
private let module: ModuleType
|
|
|
|
|
private var history: Bool
|
2020-06-07 12:22:32 +02:00
|
|
|
private var repeatTask: Repeater?
|
2020-07-27 20:57:16 +02:00
|
|
|
private var locked: Bool = true
|
2021-01-21 21:56:06 +01:00
|
|
|
private var initlizalized: Bool = false
|
2022-09-16 17:48:06 +02:00
|
|
|
|
2024-02-08 17:35:01 +01:00
|
|
|
private let activeQueue = DispatchQueue(label: "eu.exelban.readerActiveQueue")
|
2022-09-16 17:48:06 +02:00
|
|
|
private var _active: Bool = false
|
|
|
|
|
public var active: Bool {
|
2024-02-08 17:35:01 +01:00
|
|
|
get { self.activeQueue.sync { self._active } }
|
|
|
|
|
set { self.activeQueue.sync { self._active = newValue } }
|
2023-09-30 12:36:51 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-02 21:09:37 +02:00
|
|
|
private var lastDBWrite: Date? = nil
|
2024-05-21 21:02:24 +02:00
|
|
|
|
2026-03-13 19:46:47 +01:00
|
|
|
private var alignWorkItem: DispatchWorkItem?
|
|
|
|
|
private let alignQueue = DispatchQueue(label: "eu.exelban.readerAlignQueue")
|
|
|
|
|
|
2026-03-12 16:52:00 +01:00
|
|
|
public init(_ module: ModuleType, popup: Bool = false, preview: Bool = false, history: Bool = false, callback: @escaping (T?) -> Void = {_ in }) {
|
2021-03-25 22:14:20 +01:00
|
|
|
self.popup = popup
|
2026-03-12 16:52:00 +01:00
|
|
|
self.preview = preview
|
2024-02-08 17:35:01 +01:00
|
|
|
self.module = module
|
|
|
|
|
self.history = history
|
|
|
|
|
self.callbackHandler = callback
|
2020-06-07 12:22:32 +02:00
|
|
|
|
2021-08-13 11:33:04 +03:00
|
|
|
super.init()
|
2025-01-26 14:32:51 +01:00
|
|
|
DB.shared.setup(T.self, "\(module.stringValue)@\(self.name)")
|
|
|
|
|
if let lastValue = DB.shared.findOne(T.self, key: "\(module.stringValue)@\(self.name)") {
|
2024-02-08 17:35:01 +01:00
|
|
|
self.value = lastValue
|
|
|
|
|
callback(lastValue)
|
|
|
|
|
}
|
2024-09-07 17:41:10 +02:00
|
|
|
self.setup()
|
2024-02-08 17:35:01 +01:00
|
|
|
|
2021-06-26 13:38:45 +02:00
|
|
|
debug("Successfully initialize reader", log: self.log)
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-21 21:02:24 +02:00
|
|
|
deinit {
|
2025-01-26 14:32:51 +01:00
|
|
|
DB.shared.insert(key: "\(self.module.stringValue)@\(self.name)", value: self.value, ts: self.history)
|
2024-05-21 21:02:24 +02:00
|
|
|
}
|
|
|
|
|
|
2021-03-20 16:33:14 +01:00
|
|
|
public func initStoreValues(title: String) {
|
2024-02-08 17:35:01 +01:00
|
|
|
guard self.interval == nil else { return }
|
2025-10-04 13:44:16 +02:00
|
|
|
let updateInterval = Store.shared.int(key: "\(title)_updateInterval", defaultValue: self.defaultInterval)
|
|
|
|
|
self.interval = Double(updateInterval)
|
2020-07-10 22:56:47 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-07 12:22:32 +02:00
|
|
|
public func callback(_ value: T?) {
|
2025-03-22 12:59:23 +01:00
|
|
|
let moduleKey = "\(self.module.stringValue)@\(self.name)"
|
2020-06-07 12:22:32 +02:00
|
|
|
self.value = value
|
2023-06-29 17:46:40 +02:00
|
|
|
if let value {
|
|
|
|
|
self.callbackHandler(value)
|
2025-03-22 12:59:23 +01:00
|
|
|
Remote.shared.send(key: moduleKey, value: value)
|
2024-05-21 21:02:24 +02:00
|
|
|
if let ts = self.lastDBWrite, let interval = self.interval, Date().timeIntervalSince(ts) > interval * 10 {
|
2025-03-22 12:59:23 +01:00
|
|
|
DB.shared.insert(key: moduleKey, value: value, ts: self.history)
|
2024-05-21 21:02:24 +02:00
|
|
|
self.lastDBWrite = Date()
|
|
|
|
|
} else if self.lastDBWrite == nil {
|
2025-03-22 12:59:23 +01:00
|
|
|
DB.shared.insert(key: moduleKey, value: value, ts: self.history)
|
2024-05-21 21:02:24 +02:00
|
|
|
self.lastDBWrite = Date()
|
|
|
|
|
}
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open func read() {}
|
|
|
|
|
open func setup() {}
|
|
|
|
|
open func terminate() {}
|
|
|
|
|
|
|
|
|
|
open func start() {
|
2026-03-12 16:52:00 +01:00
|
|
|
if (self.popup || self.preview) && self.locked {
|
2024-02-08 17:35:01 +01:00
|
|
|
DispatchQueue.global(qos: .background).async {
|
|
|
|
|
self.read()
|
2020-07-29 18:09:52 +02:00
|
|
|
}
|
2020-07-27 20:57:16 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-21 21:56:06 +01:00
|
|
|
if !self.initlizalized {
|
2026-03-13 19:46:47 +01:00
|
|
|
if self.alignToSecondBoundary {
|
|
|
|
|
self.startAlignedRepeater()
|
|
|
|
|
} else {
|
|
|
|
|
self.startNormalRepeater()
|
|
|
|
|
DispatchQueue.global(qos: .background).async { self.read() }
|
|
|
|
|
self.repeatTask?.start()
|
2021-01-21 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
self.initlizalized = true
|
2026-03-13 19:46:47 +01:00
|
|
|
} else {
|
|
|
|
|
self.repeatTask?.start()
|
2020-07-30 17:21:40 +02:00
|
|
|
}
|
2026-03-13 19:46:47 +01:00
|
|
|
|
2020-09-05 13:03:00 +02:00
|
|
|
self.active = true
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open func pause() {
|
2026-03-13 19:46:47 +01:00
|
|
|
self.alignWorkItem?.cancel()
|
2020-07-10 22:56:47 +02:00
|
|
|
self.repeatTask?.pause()
|
2020-09-05 13:03:00 +02:00
|
|
|
self.active = false
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
open func stop() {
|
2026-03-13 19:46:47 +01:00
|
|
|
self.alignWorkItem?.cancel()
|
2022-06-30 20:39:03 +02:00
|
|
|
self.repeatTask?.pause()
|
2020-07-27 20:57:16 +02:00
|
|
|
self.repeatTask = nil
|
2020-09-05 13:03:00 +02:00
|
|
|
self.active = false
|
2021-01-21 21:56:06 +01:00
|
|
|
self.initlizalized = false
|
2020-07-10 22:56:47 +02:00
|
|
|
}
|
|
|
|
|
|
2020-10-23 15:12:05 +02:00
|
|
|
public func setInterval(_ value: Int) {
|
2026-03-13 19:46:47 +01:00
|
|
|
debug("Set update interval: \(value) sec", log: self.log)
|
2020-11-28 14:54:34 +01:00
|
|
|
self.interval = Double(value)
|
2026-03-13 19:46:47 +01:00
|
|
|
|
|
|
|
|
if self.alignToSecondBoundary {
|
|
|
|
|
self.repeatTask?.pause()
|
|
|
|
|
self.repeatTask = nil
|
|
|
|
|
self.alignWorkItem?.cancel()
|
|
|
|
|
if self.active {
|
|
|
|
|
self.startAlignedRepeater()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.repeatTask?.reset(seconds: value, restart: true)
|
|
|
|
|
}
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
2024-09-07 17:41:10 +02:00
|
|
|
|
|
|
|
|
public func save(_ value: T) {
|
2025-01-26 14:32:51 +01:00
|
|
|
DB.shared.insert(key: "\(self.module.stringValue)@\(self.name)", value: value, ts: self.history, force: true)
|
2024-09-07 17:41:10 +02:00
|
|
|
}
|
2026-03-12 16:52:00 +01:00
|
|
|
|
2026-03-13 19:46:47 +01:00
|
|
|
private func delayToNextSecondBoundary() -> TimeInterval {
|
|
|
|
|
let now = Date().addingTimeInterval(self.alignOffset)
|
|
|
|
|
let fractional = now.timeIntervalSince1970.truncatingRemainder(dividingBy: 1.0)
|
|
|
|
|
let baseDelay = (fractional == 0) ? 0.0 : (1.0 - fractional)
|
|
|
|
|
let safety: TimeInterval = 0.005 // 5ms past the boundary
|
|
|
|
|
return baseDelay + safety
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func startNormalRepeater() {
|
|
|
|
|
guard let interval = self.interval, self.repeatTask == nil else { return }
|
|
|
|
|
|
|
|
|
|
if !self.popup && !self.preview {
|
|
|
|
|
debug("Set up update interval: \(Int(interval)) sec", log: self.log)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.repeatTask = Repeater(seconds: Int(interval)) { [weak self] in
|
|
|
|
|
self?.read()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func startAlignedRepeater() {
|
|
|
|
|
guard let interval = self.interval, self.repeatTask == nil else { return }
|
|
|
|
|
|
|
|
|
|
if !self.popup && !self.preview {
|
|
|
|
|
debug("Set up update interval: \(Int(interval)) sec (aligned)", log: self.log)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let work = DispatchWorkItem { [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
|
|
|
|
|
self.read()
|
|
|
|
|
self.repeatTask = Repeater(seconds: Int(interval)) { [weak self] in
|
|
|
|
|
self?.read()
|
|
|
|
|
}
|
|
|
|
|
self.repeatTask?.start()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.alignWorkItem?.cancel()
|
|
|
|
|
self.alignWorkItem = work
|
|
|
|
|
self.alignQueue.asyncAfter(deadline: .now() + self.delayToNextSecondBoundary(), execute: work)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:52:00 +01:00
|
|
|
public func sleepMode(state: Bool) {
|
|
|
|
|
guard state != self.sleep else { return }
|
|
|
|
|
|
|
|
|
|
debug("Sleep mode: \(state ? "on" : "off")", log: self.log)
|
|
|
|
|
self.sleep = state
|
|
|
|
|
|
|
|
|
|
if state {
|
|
|
|
|
self.pause()
|
|
|
|
|
} else {
|
|
|
|
|
self.start()
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extension Reader: Reader_p {
|
2020-07-27 20:57:16 +02:00
|
|
|
public func lock() {
|
|
|
|
|
self.locked = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func unlock() {
|
|
|
|
|
self.locked = false
|
|
|
|
|
}
|
2020-06-07 12:22:32 +02:00
|
|
|
}
|