Когда мы интегрируем наше iOS приложение в с различными сервисами нам рано или поздно приходится обрабатывать строковое представление даты в свои структуры. И при особых ситуациях может произойти так, что DateFormatter просто не может получить дату, учитывая что формат единый. Такими ситуациями может служить то, что у пользователя указан другой календарь или же отличный формат представления времени (24/12 часовой), даже если вы жестко указали, что ожидатете 24-часовой формат.
Повторить такую проблему можно выставим другой регион на телефоне, например UK, и поставив 12-часовой формат отображения данных.
Решение такой проблемы достаточно простое. Допустим мы имеем такую инициализацию:
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd-yyyy HH:mm"Решение достаточно простое
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
Для пущей надежности можно даже указать григорианский календарь
formatter.calendar = Calendar(identifier: .gregorian)Но проблему решить удается и простым указанием локали и часвого пояса. Однако стоит поправить часовой пояс на нужный, если с сервера приходят даты не по UTC.
Почему же локаль en_US_POSIX? В принципе можно использовать и локаль сервера, однако документацией это подается как наиболее универсальный вариант, который поможет в "большинстве" случаев. При чем сам формат является статичным и даже при условии, если в штатах произойдут какие то изменения в форматах дат, то для POSIX (С локаль) ничего не поменяется. Тем самым мы ничем не рискуем используя эту локаль.
Так же хотелось бы напомнить, что с iOS 10.0 нам доступен отдельный форматтер для ISO8601 - ISO8601DateFormatter. Так что если вы используете этот формат, то можно избавится от пары строк кода.
]]>Когда речь заходит о создании превью, то первым делом на ум приходит Open Graph. Особый протокол который с помощью тегов в head секции страницы помогает там вытащить необходимые данные для создания превью. Однако, не всегда все так просто и зачастую подход к каждому сайту становится более индивидуальным. В рамках этого поста мы разберем несколько примеров и выработаем основной подход к этой задаче.
Если у вам не хочется вручную разбирать html код страницы, то всегда есть различные сервисы. Достаточно погуглить и в выдаче можно найти несколько API. Тут мы их разбирать не будем, так как такие сервисы конечно же не бесплатны.
Для начала необходимо определится со списком ресурсов на которые желательно было бы вывести. Для этого можно месяц-полтора пособирать аналитику. По TOP-10 же уже начать тестирование.
Это наиболее универсальный способ получения мета данных страницы. Тут у нас есть и заголовок и типа и картинка превью. Впервые получив html можно просто пройтись по необходимым тегам.
og:title - заголовок страницыog:description - описание og:image - картинка для превьюog:site_name - имя сайта (параметр от которого стоит отталкиваться в случае когда необходимо выделить дополнительные параметры)С другими тегами можножно ознакомится на сайте протокола - https://ogp.me/
Все прекрасно, но зачастую бывает недостаточно. Сайты просто игнорируют реализацию таких тегов. Тут на помощь приходят теги твиттера. Они называются в документации твиттера как Cards.
og:site_name в случае карт будет twitter:site. По использовав этот параметр как id можно уже идти дальше к более детальному получению информации по каждому сайту.
Случается так, что страницы отказываются давать нам достаточно информации, поэтому при запросе желательно добавить User-Agent к которому сервер более охотно отдаст ответ. Например User-Agent: Googlebot или User-Agent: Twitterbot.
Для того чтобы получить какие либо дополнительные данные из твиттера нам необходимо использовать их API, а следовательно требуется получить токен для работы с ним. Но для того чтобы сделать запрос к API надо знать что именно мы хотим от него получить. Для этого самым простым путем будет распарсить путь ссылки и взять последний его элемент. В большой долей вероятности если это число - то это ссылка на твит, иначе - ссылка на юзера.
Получить твит можно по endpoint-у https://api.twitter.com/2/tweets/<id-твита> с параметрами. Например,
"tweet.fields" = "id,text""expansions" = "attachments.media_keys,author_id""media.fields" = "preview_image_url"Таким запросом мы сможем получить ссылку на аватар автора твита, его юзернейм и имя. Подробнее про этот запрос можно почитать тут.
Для пользователя же необходимо обратиться к https://api.twitter.com/2/users/by/username/<username>, с параметрами
"user.fields" = "name,username,profile_image_url"В ответ мы получим все те же что и вариантом с постом параметры. Документация к запросу доступна по ссылке.
Для Youtube нам необходимо общаться с API гугла, однако прежде всего нам необходимо вытащить id видео. Оно доступно в параметрах запроса с id - v. К сожалению, без этого id мы мало что можем сделать, правда если только парсить html дальше, однако вариант не особо надежен.
Получаем информацию о видео по запросу на https://youtube.googleapis.com/youtube/v3/videos где в параметры добавим
"part" = "snippet,statistics"id = id видеоВ ответ получим информацию о количестве комментариев и просмотров, а так же пачку превью разных размеров. Документация к запросу.
Однако, на этом можно не останавливаться, ведь Youtube пришлет нам еще и id канала. По id канала можно забрать еще больше данных. Получить информацию о канале можно получить по запросу на https://youtube.googleapis.com/youtube/v3/channels, где добавим параметры
"part" = "snippet"id = id каналаТеперь нам доступно не только название канала, но и еще ссылка на его картинку. Документация.
Для того чтобы производить какие либо действия необходимо разобрать путь ссылки. Если компонент все один, то вероятно это ссылка на профиль и можно предполагать, что последний этот компонент является id пользователя на которого сделана ссылка. Иначе это вероятнее всего является ссылкой на пост или на рилс, что серьезно усложнит получение дополнительной информации. Так как дальнейшие запросы потребуют от нас авторизации для получения токена к API.
В случае же с id юзера все проще. Есть достаточно популярный запрос к приватному API - https://www.instagram.com/<username>?__a=1. Он выдаст нам как минимум ссылку на аватар профиля и его имя.
К сожалению, нам не всегда доступны какие либо API, а мета данные не до конца удовлетворяют нашим запросам. В этом случае приходится прибегать к менее надежным вариантам. А именно к парсингу самого html. Здесь нет универсальных путей и потребуется переодически мониторить правильно ли мы достали эти данные. Но с правильно настроеной системой сбора аналитики и сбоев даже такой подход не будет является большой проблемой.
]]>Во многих проектах, где существует интеграция с собственными сервисами, встает вопрос о сборе аналитики доступности этого сервиса. Это можно реализовать несколькими путями. Наиболее доступным и быстром среди них будет использовать Firebase с его Performance Monitoring. Подключив модуль в проект ваши запроса станут автоматически трекаться, плюс появится возможность добавить свои события время затраты на которые вам необходимо затрекать. Так что если вашей целью является просто мониторить задержку с различных регионов, то можно закрывать эту статью и идти прямиком в Firebase. Однако, если у вас есть необходимость получать какую то дополнительную информацию, например, чтобы трекать падения парсера с клиентской стороны или мониторинг ответов от сервера, то вполне возможно, что вариант описанный в этом статье, вам подойдет.
InfluxDB - база данных которая собирает события на основе временных данных. Есть два варианта - Облако и open source решение. Так как для наших задач нам необходимо изолироваться он своих собственных серверов, то можно использовать облако. Бесплатная квота позволит нам держать данные месяц и смотреть на них в режиме онлайне, а так же строить графики, анализировать данные, и отправлять уже более постоянные хранилища, если это требуется.
Процесс регистрации простой, нам необходимо зайти на сервиc https://www.influxdata.com/ создать bucket и сгенерировать для него токен.

Сохраняем полученный токен, ID бакета и идем в проект. ID бакета можно посмотреть здесь:

Интеграцию будем реализовывать в iOS на примере обычной сессии. На этом этапе важно продумать, какие данные мы собираемся слать и какие данные будут тегами и какие значениями. Так как InfluxDB мы можем строить запросы только на основе одного значения, то предлагаю поместить слеедующие значения: время, потраченное на запрос, или причину, почему этот запрос не прошел.
Структура отправки запроса в БД выглядит так
measurementName,tagKey=tagValue fieldKey="fieldValue" 1465839830100400200
--------------- --------------- --------------------- -------------------
| | | |
Measurement Tag set Field set TimestampПервым идет Measurement - это будет названием так называемой "таблицы", затем через запятую идут теги, потом значения и timestamp. Все части, кроме тегов, являются обязательными.
К тегам предлагаю отнести такие значения: названия модуля; откуда происходил запрос; хост; путь; локация пользователя (если она доступна) и пр.
Поле значений может быть нескольких типов:
i , но это не обзательно. Например, 12485903iu аналогично с обычным integer-омt/f, T/F, true/false, True/False, TRUE/FALSEОсобенности форматирования значения подразумевают, что нам потребуется написать свой форматер под каждый типа данных. Например,
private extension Dictionary where Key == String {
func toInfluxLine(wrapString: Bool = true) -> String? {
var fields: [String] = []
for (key, value) in self {
let converted: String
switch value {
case let float as Float: converted = "\(float)"
case let double as Double: converted = "\(double)"
case let integer as Int: converted = "\(integer)i"
case let uInteger as UInt: converted = "\(uInteger)u"
case let string as String: converted = wrapString ? "\"\(string)\"" : string
case let bool as Bool: converted = bool ? "true" : "false"
default: continue
}
fields.append("\(key)=\(converted)")
}
guard !fields.isEmpty else { return nil }
return fields.joined(separator: ",")
}
}Когда мы определились с содержимым наших запросов, давайте попробуем набросать пример как будет выглядеть сбор этих значений:
final class NetworkManager: NSObject, URLSessionDataDelegate {
let session = URLSession(configuration: .default)
func makeRequest() async throws {
let url = URL(string: "https://www.google.com/")!
let _ = try await URLSession.shared.data(from: url, delegate: self)
}
func trace(url: URL?, duration: TimeInterval) {
guard let url = url else { return }
print("Host:", url.host ?? "-", "Path:", url.relativePath, "Duration:", duration)
}
// MARK: - URLSessionDataDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
trace(url: task.currentRequest?.url, duration: metrics.taskInterval.duration)
}
}Тестовый запрос может выглядеть так:
let manager = NetworkManager()
do {
try await manager.makeRequest()
} catch {
print("Error:", error.localizedDescription)
}Здесь мы видим, что при каждом запросе мы прикрепляем делегат для сбора данных о процессе выполнения задачи. В других фреймворках это, конечно, будет выглядеть по другому. Например, метрики о запросе в Alamofire можно получить из финального кложура. К сожалению, все варианты в рамках поста охватить будет сложно.
Итак, у нас есть необходимые данные для отправки. Пора приступать к самой отправке. Для этого нам потребуется адрес облака который вы выбрали для хранения, токен который вы получили прежде и ID бакета. Отправку можно сделать двумя способами, отправка текстом или gzip. Здесь я покажу как отправляется текстом, запаковать далее, если данных много будет несложно. Напишем отдельный класс, который будет отвечать за отправку.
final class InfluxDbService {
let host: String
let bucketId: String
let session: URLSession
init(host: String, bucketId: String, token: String) {
self.host = host
self.bucketId = bucketId
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Content-Type": "text/plain; charset=utf-8",
"Accept": "application/json",
"Authorization": "Token \(token)"
]
session = URLSession(configuration: configuration)
}
public func addMetrics(url: URL, duration: TimeInterval) {
let measurement = "APIHealth"
var tags: [String: Any] = [:]
tags["host"] = url.host
tags["path"] = url.relativePath
var fields: [String: Any] = [:]
fields["duration"] = duration
guard !fields.isEmpty else { return }
var message = measurement
if let line = tags.toInfluxLine(wrapString: false) { message += "," + line }
if let line = fields.toInfluxLine() { message += " " + line }
let timestamp = Int(1000 * Date().timeIntervalSince1970)
message += " \(timestamp)"
let url = URL(string: "\(host)/api/v1/write?bucket=\(bucketId)&precision=ms")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = message.data(using: .utf8)
session.dataTask(with: request).resume()
}
}Здесь мы можем видеть отправку нашей сформированной строки на адрес (host)/api/v1/write?bucket=(bucketId)&precision=ms . Где
На данном этапе было бы нецелесообразно отправлять метрики каждый раз, как совершился запрос, но более эффективно было бы собрать их вместе и отправлять раз в определенный промежуток времени, например раз в пять секунд. Поэтому, предлагаю расширить функицонал класса и добавить Timer, который будет работать в debounce-режиме: т.е. каждый раз сбрасываться до тех пор пока не пройдет таймаут.
Для этого переделаем момент отправки запроса на добавление сообщения в массив, который будет вызывать создание таймера
var metrics: [String] = [] { didSet { debounceTimer() } }
var timer: Timer?
func addMetrics(url: URL, duration: TimeInterval) {
...
metrics.append(message)
}Ниже представлена сама инициализация таймера. Интервал установлен в 5 секунд, который будет вызывать отправку метрик.
private func debounceTimer() {
DispatchQueue.main.async { [metrics, weak self] in
self?.timer?.invalidate()
guard !metrics.isEmpty else { return }
self?.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in self?.submitMetrics() }
}
}Соберем метрики в единое сообщение и отправим.
private func submitMetrics() {
guard !metrics.isEmpty else { return }
let message = metrics.joined(separator: "\n")
metrics = []
let url = URL(string: "\(host)/api/v1/write?bucket=\(bucketId)&precision=ms")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = message.data(using: .utf8)
session.dataTask(with: request).resume()
}Ну и, конечно, не стоит забывать момент свертки приложения. Можно добавить подписку на это событие в самый init нашего сервиса.
NotificationCenter.default.addObserver(
forName: UIScene.willDeactivateNotification,
object: nil,
queue: nil) { [weak self] _ in self?.submitMetrics() }Весь код класса получается таким:
final class InfluxDbService2 {
let host: String
let bucketId: String
let session: URLSession
var metrics: [String] = [] { didSet { debounceTimer() } }
var timer: Timer?
init(host: String, bucketId: String, token: String) {
self.host = host
self.bucketId = bucketId
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"Content-Type": "text/plain; charset=utf-8",
"Accept": "application/json",
"Authorization": "Token \(token)"
]
session = URLSession(configuration: configuration)
NotificationCenter.default.addObserver(
forName: UIScene.willDeactivateNotification,
object: nil,
queue: nil) { [weak self] _ in self?.submitMetrics() }
}
func addMetrics(url: URL, duration: TimeInterval) {
let measurement = "APIHealth"
var tags: [String: Any] = [:]
tags["host"] = url.host
tags["path"] = url.relativePath
var fields: [String: Any] = [:]
fields["duration"] = duration
guard !fields.isEmpty else { return }
var message = measurement
if let line = tags.toInfluxLine(wrapString: false) { message += "," + line }
if let line = fields.toInfluxLine() { message += " " + line }
let timestamp = Int(1000 * Date().timeIntervalSince1970)
message += " \(timestamp)"
metrics.append(message)
}
private func debounceTimer() {
DispatchQueue.main.async { [metrics, weak self] in
self?.timer?.invalidate()
guard !metrics.isEmpty else { return }
self?.timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in self?.submitMetrics() }
}
}
private func submitMetrics() {
guard !metrics.isEmpty else { return }
let message = metrics.joined(separator: "\n")
metrics = []
let url = URL(string: "\(host)/api/v1/write?bucket=\(bucketId)&precision=ms")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = message.data(using: .utf8)
session.dataTask(with: request).resume()
}
}Теперь у нас есть сбор аналитики и их отправка. Можно приступать к составлению дашбордов и графиков. Вернемся в панель InfluxDb и попробуем создать дашборд.
Этот этап дает нам много возможностей. Например, для того чтобы построить график по хостам, нам нужно будет задать примерно такой запрос:
from(bucket: "BucketName")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "APIHealth" and r["_field"] == "duration")
|> group(columns: ["host"])
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|> yield(name: "mean")Экспериментируйте с разными вариантами отображения, которые больше подойдут для ваших нужд.
В результате исследования различных вариантов ведения аналитики, модно сделать вывод, что influx подошел лучше всего, ведь с его помощью удалось найти узкие места в работе API, и решить проблемы в реализации клиентской части. В будущем можно создать систему оповещений о неполадках в доступе из определенной страны, или определить какие части API можно закешировать заранее, чтобы ускорить доступ клиентов к данным.
]]>Tuist это инструмент для генерации проектов XCode. Это поз
]]>
Tuist это инструмент для генерации проектов XCode. Это позволяет организовать структуру проекта и автоматизировать конфигурацию зависимостей более удобным способом. Такое решение отлично подходит для тех у кого есть множество проектов которые зависят от одной кодовой базы или для тех кому необходимо собирать один и тот же проект с различными конфигурациями (Test, Stage, Prod и т.п.). Так же это избавляет от разрешения конфликтов при мерже xcproget-ов, так как по большей части их больше нет, они каждый раз будут сгенерированы по новому для каждого разработчика отдельно.
Как всегда есть и минусы в подобном решении, так как проект с открытым исходным кодом, владельцы проекта не всегда идут на встречу и решают проблемы сообщества. Однако, на то он и open source, и если какое то решение необходимо реализовать, то это можно решить своими силами. Проект написан swift, а большинству кто будет пользоваться инструментом этот язык хорошо знаком.
Так же Tuist не поддерживает и не будет поддерживать CocoaPods. Это не такая критичная проблема так как перенос всех зависимостей на SPM или Carthage не составит труда.
Сперва необходимо установить сам tuist, если вы все еще это не сделали
curl -Ls https://install.tuist.io | bashДалее уже можно приступать к самой подготовке. Нам нужно сохранить конфигурации ваших проектов и их таргетов. Для этого я сделал папку Configs, куда сохранил все конфигурации
tuist migration settings-to-xcconfig -p Project.xcodeproj -t MyApp -x Configs/MyApp.xcconfig
tuist migration settings-to-xcconfig -p Project.xcodeproj -x Configs/MyAppProject.xcconfig
На этом шаге мы почти готовы удалить проекты, но для начала необходимо перенести зависимости. Создаем папку Tuist и в нем файл Dependencies.swift. В нем мы поместим все зависимости. Здесь небольшой пример содержимого файла, на примере части конфига в нашем проекте
import ProjectDescription
let dependencies = Dependencies(
carthage: [
.github(path: "Alamofire/Alamofire", requirement: .exact("5.0.4")),
],
swiftPackageManager: [
.remote(url: "https://github.com/amplitude/Amplitude-iOS.git", requirement: .upToNextMajor(from: "8.0.0")),
.remote(url: "https://github.com/firebase/firebase-ios-sdk.git", requirement: .upToNextMajor(from: "9.0.0")),
.remote(url: "https://github.com/facebook/facebook-ios-sdk", requirement: .upToNextMajor(from: "15.0.0")),
.remote(url: "https://github.com/onevcat/Kingfisher.git", requirement: .upToNextMajor(from: "7.0.0")),
.remote(url: "https://github.com/SnapKit/SnapKit.git", requirement: .upToNextMajor(from: "5.0.0")),
],
platforms: [.iOS]
)
Если в вашем проекте используется Realm, то существует проблема https://github.com/tuist/tuist/issues/3928 которая не позволяет нам добавить его ни через carthage ни через SPM. К сожалению, единственное решение которое я нашел, это импорт xcframework realm-а уже напрямую.
Теперь мы готовы приступить к самой конфигурации проектов. Создаем файл Projects.swift в корне проекта и начинаем добавляет проекты. В нем будет указаны конфигурации самих проектов и их взаимоотношения между друг другом. А так же добавим скачанные зависимости в необходимые таргеты.
import ProjectDescription
let deploymentTarget: DeploymentTarget = .iOS(targetVersion: "13.0", devices: [.iphone, .ipad])
let project = Project(
name: "Company Apps",
organizationName: "Our company name",
targets: [
Target(
name: "MyApp1",
platform: .iOS,
product: .app,
bundleId: "com.company.myapp1",
deploymentTarget: deploymentTarget,
infoPlist: "Targets/MyApp1/Info.plist",
sources: ["Targets/MyApp1/**"],
resources: [
"Targets/MyApp1/Resources/**",
"Targets/MyApp1/GoogleService-Info.plist",
],
dependencies: [
.target(name: "MyFramework"),
],
settings: .settings(
configurations: [
.debug(name: .debug, xcconfig: Path("Configs/MyApp1.xcconfig")),
.release(name: .release, xcconfig: Path("Configs/MyApp1.xcconfig")),
]
)
),
Target(
name: "MyApp2",
platform: .iOS,
product: .app,
bundleId: "com.company.myapp2",
deploymentTarget: deploymentTarget,
infoPlist: "Targets/MyApp2/Info.plist",
sources: ["Targets/MyApp2/**"],
resources: [
"Targets/MyApp2/Resources/**",
"Targets/MyApp2/GoogleService-Info.plist",
],
dependencies: [
.target(name: "MyFramework"),
],
settings: .settings(
configurations: [
.debug(name: .debug, xcconfig: Path("Configs/MyApp2.xcconfig")),
.release(name: .release, xcconfig: Path("Configs/MyApp2.xcconfig")),
]
)
),
Target(
name: "MyFramework",
platform: .iOS,
product: .framework,
bundleId: "com.company.MyFramework",
deploymentTarget: deploymentTarget,
sources: ["Targets/MyFramework/**"],
resources: ["Targets/MyFramework/Resources/**"],
headers: .headers(
public: [
"Targets/MyFramework/Thirdparity/SomeCLibrary/**",
"Targets/MyFramework/Headers/**",
]
),
dependencies: [
.external(name: "Alamofire"),
.external(name: "Amplitude"),
.external(name: "ApphudSDK"),
.external(name: "FacebookCore"),
.external(name: "FirebaseAnalytics"), .external(name: "FirebaseRemoteConfig"), .external(name: "FirebaseCrashlytics"),
.external(name: "Kingfisher"),
.external(name: "SnapKit"),
.xcframework(path: "Frameworks/RealmSwift.xcframework"),
.xcframework(path: "Frameworks/Realm.xcframework"),
],
settings: .settings(
configurations: [
.debug(name: .debug, xcconfig: Path("Configs/MyFramework.xcconfig")),
.release(name: .release, xcconfig: Path("Configs/MyFramework.xcconfig")),
]
)
),
]
)
В этом примере я постарался охватить максимальное количество ситуаций которое может возникнуть при переносе существующего проекта на tuist. У нас есть два проекта которые имеют один общий фреймворк который забирает все зависимости проекта и даже имеет C код при себе. Так как фреймворки не могут иметь bridging хедеров, то импорт такого кода происходит через импорт самодельного хедера с нужным набором импортов. Важно сложить все ресурсы которые не являются swift кодом или исходниками на другом языке в отдельную папку, иначе придется указывать локацию таких файлов отдельно как, например, я совершил импорт файла конфигурации от firebase.
Разберем текущий инциализатор по пунктам
Так как это swift файлик, то генерацию каждого таргета можно оптимизировать путем расширения Target и упрощения инициализатора.
Запуск скриптов до или после сборки можно реализовать через конфигурацию таргета. Создадим в корне проекта папку с нужными нам скриптами. Например, для firebase.
scripts/firebase.sh:
if [ "${CONFIGURATION}" != "Debug" ]; then
"Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Crashlytics/run" \
"Tuist/Dependencies/SwiftPackageManager/.build/checkouts/firebase-ios-sdk/Crashlytics/upload-symbols" \
-gsp ${SCRIPT_INPUT_FILE_0} \
-p ios ${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
fiДалее добавим скрипт в конфигурацию
...
let project = Project(
...
targets: [
Target(
...
resources: [
"Targets/MyApp1/Resources/**",
"Targets/MyApp1/GoogleService-Info.plist",
],
scripts: [
.post(
path: "./scripts/firebase.sh",
name: "Firebase Crashlystics",
inputPaths: ["Targets/MyApp1/GoogleService-Info.plist"]
),
]
dependencies: [
.target(name: "MyFramework"),
],
...
),
...
],
...
}Tuist позволяет нам реализовать расширения классов где мы можем оптимизировать инициализацию таргетов, фреймворков и пр., что значительно разгрузит конфигурацию и улучшит ее читаемость.
Для этого создадим папку ProjectDescriptionHelpers в папке Tuist и наполним ее расширениями классов которые хотим упроситить. Например, инициализация таргета Target+Apps.swift:
import ProjectDescription
public extension Target {
static func app(
name: String,
bundleId: String,
id: String,
scripts: [TargetScript]
) -> Target {
Target(
name: name,
platform: .iOS,
product: .app,
bundleId: bundleId,
deploymentTarget: .default,
infoPlist: "Targets/\(id)/Info.plist",
sources: ["Targets/\(id)/**"],
resources: [
"Targets/\(id)/Resources/**",
"Targets/\(id)/GoogleService-Info.plist",
],
scripts: scripts,
dependencies: [
.target(name: "MyFramework"),
],
settings: .settings(
configurations: [
.debug(name: .debug, xcconfig: Path("Configs/\(id).xcconfig")),
.release(name: .release, xcconfig: Path("Configs/\(id).xcconfig")),
]
)
)
}
}
extension DeploymentTarget {
static let `default`: DeploymentTarget = .iOS(targetVersion: "13.0", devices: [.iphone, .ipad])
}
Теперь можно обновить список проектов
...
let project = Project(
...
targets: [
.app(name: "MyApp1",
bundleId: "com.company.myapp1",
id: "MyApp1",
scripts: [
.firebase(plist: "Targets/MyApp1/GoogleService-Info.plist"),
]),
.app(name: "MyApp2",
bundleId: "com.company.myapp2",
id: "MyApp2",
scripts: [
.firebase(plist: "Targets/MyApp2/GoogleService-Info.plist"),
]),
...
]
)
Здесь так же присуствует расширение для скриптов. Оно реализовано следующим образом TargetScript+Upload.swift:
import ProjectDescription
extension TargetScript {
public static func firebase(plist: Path) -> TargetScript {
.post(
path: "./scripts/firebase.sh",
name: "Firebase Crashlystics",
inputPaths: [plist]
)
}
}На этом вся необходимая конфигурация готова. Остается только попробовать запустить генерацию. Но предварительно необходимо сделать загрузку зависимостей. Это делается командой
tuist fetch
А потом уже можно сделать генерацию самого проекта
tuist generate
Если все пройдет хорошо, что конечно скорее всего не так, то нам откроется наш workspace с проектами. Так же есть удобная функция позволяющая собрать проект чисто под одно приложение. Например:
tuist generate MyApp1
Если вам понадобится вновь производить какие то изменения в конфигурацию можно пользоваться командой
tuist edit
Будет создан проект манифеста, после изменения которого нужно будет нажать ctrl+c что бы tuist почистил лишнее.
Советую занести все сгенеренные tuist-ом файлы в gitignore чтобы избежать конфликтов между различными версиями. Необходимо привыкнуть, что самому проект лучше не открывать, а запускать через tuist generate.
Если вы обновите набор зависимостей, то загрузить обновления необходимо через флаг u:
tuist fetch -u
Так же может произойти так, что после обновления каких либо конфигураций, проект перестает генерироваться, то можно попробовать почистить кеш и прочие файлы которые этому препятствуют.
tuist clean
Так же можно сгенерировать проект без использования кеша
tuist generate --no-cache
]]>После того как мы завели наш датчик и начали делиться его показаниями с HomeKit, то хорошо бы отправить эти данные куда либо во внешний интернет, дабы любоваться ими не только в пределах домашней сети. Для этой цели идеально подойдет дашборд метрик Grafana.
Для того чтобы начать отправлять данные, нам не обязательно иметь настроенный Homebridge, необходим только MQTT брокер и сервер с Docker-ом.
Данные мы будем хранить в InfluxDB, она специально была создана для подобных задач. Сервер БД можно завести и в домашней сети, но не советую это делать на RaspberryPi, большое количество чтение/записи может быстро израсходовать лимит вашей SD-карты.
Начнем с того, что зайдем на наш сервер и заведем папку где будем хранить файлы БД, например в /var/influxdb
sudo mkdir /var/influxdbИ создадим базу данных. Это можно сделать командой:
docker run --rm \
-e INFLUXDB_DB=<имя базы данных> \
-e INFLUXDB_ADMIN_ENABLED=true \
-e INFLUXDB_ADMIN_USER=admin \
-e INFLUXDB_ADMIN_PASSWORD=<Пароль администратора> \
-e INFLUXDB_USER=<Имя пользователя> \
-e INFLUXDB_USER_PASSWORD=<Пароль пользователя> \
-v /var/influxdb:/var/lib/influxdb \
influxdb /init-influxdb.shТеперь создадим папку для хранения файлов grafana. Например, /var/grafana
sudo mkdir /var/grafanaТ.к. в докере grafana не работает от root, то она не будет иметь прав на запись в папках которые ей по сути не принадлежат, права можно выдать через chmod, но можно сменить владельца и не выдавать лишних прав другим пользователям. Для этого необходимо зайти в терминал через контейнер grafana
docker run -ti --user root --volume /var/grafana:/var/lib/grafana --entrypoint bash grafana/grafanaИ поменять владельца
chown -R root:root /etc/grafana && \
chmod -R a+r /etc/grafana && \
chown -R grafana:grafana /var/lib/grafana && \
chown -R grafana:grafana /usr/share/grafanaИсчерпывающую информацию к более детальной настройки Grafana можно найти в документации https://grafana.com/docs/grafana/latest/installation/docker/
Далее можно запускать DB и Grafana.
docker run -d \
-p 8086:8086 \
-v /var/influxdb:/var/lib/influxdb \
--name influxdb \
influxdb
docker run -d \
-p 3000:3000 \
-e GF_SERVER_ROOT_URL="<адрес хоста>" \
-e GF_SECURITY_ADMIN_PASSWORD="<пароль администратора>" \
-v /var/grafana:/var/lib/grafana \
--name grafana \
grafana/grafanaВсе что нам осталось, так это зайти на сервер по порту 3000, авторизоваться с указанным вами паролем и выполнять инструкции по настройке указанные в сервисе.
Теперь как же отправить данные на InfluxDB с нашего домашнего сервера? Для этого необходимо маршрутизировать данные с MQTT брокера на сервер. Предлагаю воспользоваться уже готовым решением https://github.com/Kichix/WlanThermoMeterGrafana/tree/master/02-bridge
Для конфигурации которая была описана в предыдущем посте необходимо слегка видоизменить main.py:
#!/usr/bin/env python3
"""A MQTT to InfluxDB Bridge
This script receives MQTT data and saves those to InfluxDB.
"""
import re
from typing import NamedTuple
import paho.mqtt.client as mqtt
from influxdb import InfluxDBClient
INFLUXDB_ADDRESS = '<Адрес хоста с БД>'
INFLUXDB_USER = '<Имя пользователя от БД>'
INFLUXDB_PASSWORD = '<Пароль от БД>'
INFLUXDB_DATABASE = '<Имя базы данных>'
MQTT_ADDRESS = '<Адрес MQTT брокера>'
MQTT_TOPIC = '+/+' # [temperature|humidity|battery|status]/[id]
MQTT_REGEX = '([^/]+)/([^/]+)'
MQTT_CLIENT_ID = 'MQTTInfluxDBBridge'
influxdb_client = InfluxDBClient(INFLUXDB_ADDRESS, 8086, INFLUXDB_USER, INFLUXDB_PASSWORD, None)
class SensorData(NamedTuple):
location: str
measurement: str
value: float
def on_connect(client, userdata, flags, rc):
""" The callback for when the client receives a CONNACK response from the server."""
print('Connected with result code ' + str(rc))
client.subscribe(MQTT_TOPIC)
def on_message(client, userdata, msg):
"""The callback for when a PUBLISH message is received from the server."""
print(msg.topic + ' ' + str(msg.payload))
sensor_data = _parse_mqtt_message(msg.topic, msg.payload.decode('utf-8'))
if sensor_data is not None:
_send_sensor_data_to_influxdb(sensor_data)
def _parse_mqtt_message(topic, payload):
match = re.match(MQTT_REGEX, topic)
if match:
measurement = match.group(1)
location = match.group(2)
if measurement == 'status':
return None
return SensorData(location, measurement, float(payload))
else:
return None
def _send_sensor_data_to_influxdb(sensor_data):
json_body = [
{
'measurement': sensor_data.measurement,
'tags': {
'location': sensor_data.location
},
'fields': {
'value': sensor_data.value
}
}
]
influxdb_client.write_points(json_body)
def _init_influxdb_database():
databases = influxdb_client.get_list_database()
if len(list(filter(lambda x: x['name'] == INFLUXDB_DATABASE, databases))) == 0:
influxdb_client.create_database(INFLUXDB_DATABASE)
influxdb_client.switch_database(INFLUXDB_DATABASE)
def main():
_init_influxdb_database()
mqtt_client = mqtt.Client(MQTT_CLIENT_ID)
#mqtt_client.username_pw_set(MQTT_USER, MQTT_PASSWORD)
mqtt_client.on_connect = on_connect
mqtt_client.on_message = on_message
mqtt_client.connect(MQTT_ADDRESS, 1883)
mqtt_client.loop_forever()
if __name__ == '__main__':
print('MQTT to InfluxDB bridge')В оригинальном коде происходила авторизация на брокер по логин/пароль, возможно это и более безопасно, но почему то для своей сети я решил этим не заморачиваться и было изменено определение источника и типа измерения. Автор кода принимал сначала id источника, а потом идентификатор измерения, в моем же варианте наоборот. Так же автор предлагает запускать скрипт в докере, но память RaspberryPi не резиновая и устанавливать на него Docker мне лично кажется неразумным. Поэтому необходимо будет написать конфигурацию сервиса чтобы настроить автозапуск скрипта при старте RaspberryPi. Создадим файл mqtt-influxdb-bridge.service:
[Unit]
Description=MQTT InfluxDB Bridge
After=syslog.target network-online.target
[Service]
Type=simple
User=pi
ExecStart=/usr/bin/python3 /home/pi/mqtt-influxdb-bridge/main.py
Restart=on-failure
RestartSec=10
KillMode=process
[Install]
WantedBy=multi-user.targetСоздадим папку для хранения бриджа на RaspberryPi
mkdir ~/mqtt-influxdb-bridge И отправим requirements.txt, модифицированый main.py и mqtt-influxdb-bridge.service на наш RaspberryPi
scp main.py pi@raspberrypi:~/mqtt-influxdb-bridge/.
scp requirements.txt pi@raspberrypi:~/mqtt-influxdb-bridge/.
scp mqtt-influxdb-bridge.service pi@raspberrypi:~/mqtt-influxdb-bridge/.Установите python3 если он еще не установлен на ваш RaspberryPi и начните забирать зависимости из папки ~/mqtt-influxdb-bridge
pip3 install -r requirements.txtДалее скопируем конфигурацию сервиса в /lib/systemd/system/
sudo cp mqtt-influxdb-bridge.service /lib/systemd/system/.Не забудьте исправить полный путь ExecStart в mqtt-influxdb-bridge.service к файлу скрипта main.py если он отличается от настоящего.
Наконец то можно запускать скрипт
systemctl daemon-reload
systemctl enable mqtt-influxdb-bridge
systemctl start mqtt-influxdb-bridgeДанные должны начать отправляться на вашу БД. Можете начать настраивать индикаторы в Grafana и наслаждаться плодами трудов.
]]>Данный пост основан на результатах работы предыдущего поста где мы отправляли данные c BMP085 на MQTT брокер Mosquitto. В этом посте мы используем сервер Homebridge для того чтобы маршрутизировать данные получаемые с датчиков на Apple девайсы. Homebridge имеет множество плагинов, а также предоставляет достаточно подробную документацию о том как написать свой. Однако же для нашей задачи нам не нужно ничего писать. Для маршрутизации данных с брокера в Homebrige нам понадобиться homebridge-mqttthing.
Для начала давайте установим сам Homebrige
sudo npm install -g homebridgeИ плагин
sudo npm install -g homebridge-mqttthingКонфигурация происходит по адресу ~/.homebridge/config.json
{
"bridge": {
"name": "Homebridge",
"username": "username",
"port": 51826,
"pin": "pin"
},
"description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.",
"ports": {
"start": 52100,
"end": 52150,
"comment": "This section is used to control the range of ports that separate accessory (like camera or television) should be bind to."
},
"accessories": [
{
"accessory": "mqttthing",
"type": "temperatureSensor",
"name": "Temperature",
"logMqtt": "true",
"topics":
{
"getCurrentTemperature": {
"topic": "temperature/device_id"
}
},
"history": "true"
},
{
"accessory": "mqttthing",
"type": "airPressureSensor",
"name": "Pressure",
"logMqtt": "true",
"topics":
{
"getAirPressure": {
"topic": "pressure/device_id"
}
},
"history": "true"
}
]
}После установки у вас уже должен быть этот файл в наличии, не хватает только содержимого массива accessories. В приведенном выше примере уже дописана конфигурация двух показаний которые есть у меня. Подробное описание опций можно найти в репозитории к homebridge-mqttthing: датчик температуры и датчик давления. Топики можно уточнить посмотрев эфир с помощью какого либо клиента MQTT, либо посмотрев в прошивку.
Как можно заметить, в моем конфиге не указан адрес брокера MQTT. Если брокер находится на той же машине, что и homebridge, то в подобном нет необходимости.
После того как конфигурация завершена, пробуем запустить homebrige
homebridgeДанных на выводе будет много и в конце концов мы увидим как начинают приниматься данные с MQTT брокера. Далее в дело вступает телефон, который должен быть в одной локальной сети с запущенным Homebridge. В начале лога вы получите QR код и Pin в рамке. Открываем приложение Home в телефоне и добавляем устройство, наведя камеру на одно из этих изображений. В моем случае добавить через QR код у меня не получилось, но успешно добавлено через Pin. Еще наблюдалась очень длительная пауза между добавлением пина и собственно фактическим добавлением устройства в приложение.
Стоковое приложение iOS не дает нам много информации, только лишь температуру и то округленную до половины градуса. Отображение давления на момент написания поста не поддерживалось, но оно приходит, в чем можно убедиться с помощью сторонних приложений. Лично я удовлетворился использованием Eve. Девайсы у них неоправданно дорогие, но приложение бесплатно и дает значительно большую информацию нежели приложение Home.
И так, мы наконец можем любоваться результатами работы Homebridge, но сам homebridge запущен вручную, а хотелось бы чтобы запуск приложения происходил автоматически. Для этого необходимо зарегистрировать сервис для запуска сервера. В этом нам снова помогут готовые решения: Systemd Service for homebridge.
Скачиваем файлы homebridge и homebridge.service в систему мы запускаем homebridge, уточняем расположение бинарных файлов сервера
whereis homebridgeЭтот адрес необходимо указать в поле ExecStart в файле homebridge.service. Возможно, там уже указан правильный адрес, в этом случае можно ничего не менять.
Инструкция к файлам советует создать пользователя, но возможно это не обязательно, стоит лишь изменить пользователя в поле User. Однако же я все же создал такого пользователя. Файл homebridge нужно поместить по адресу /etc/default/homebridge, homebridge.service в /etc/systemd/system/homebridge.service. Обратите внимание, что в файле homebridge указывается место, где находятся файлы конфигурации сервера. Так что если у вас есть желание хранить данные сервера в другом месте, то это тот самый файл, где новые данные следует указать.
Теперь настало время поставить homebridge в автозапуск и запустить сервис
systemctl daemon-reload
systemctl enable homebridge
systemctl start homebridgeЛоги сервиса можно смотреть командой
journalctl -u homebridgeВ результате получаем что то вроде

Сегодня я хочу рассказать как можно настроить передачу данных с цифрового датчика давления BMP085 по сети. Для этого нам понадобится сам датчик и модуль NodeMCU (в моем случае это клон от WeMos). В качестве протокола передачи мы будем использовать MQTT, так как сам по себе протокол достаточно распространен для интеграции с другими сервисами.
Прежде чем заняться пайкой и самой прошивкой NodeMCU, необходимо озаботиться о сервере. В качестве принимающей стороны больше всего распространен Mosquitto его можно поставить на что угодно, в моем случае это был RaspberryPi, так как дальше локальной сети передавать телеметрию у меня не планировалось.
Порядок его установки довольно типичный и найти его можно на официальном сайте. Для RaspberryPi это так:
wget http://repo.mosquitto.org/debian/mosquitto-repo.gpg.key
sudo apt-key add mosquitto-repo.gpg.key
# Then make the repository available to apt:
cd /etc/apt/sources.list.d/
# Then one of the following, depending on which version of debian you are using:
sudo wget http://repo.mosquitto.org/debian/mosquitto-stretch.list
# Then update apt information:
apt-get update
apt-get install mosquittoДалее необходимо добавить его в автозапуск и собственно включить
sudo systemctl start mosquitto
sudo systemctl enable mosquittoПроверить работоспособность сервера можно с помощью многочисленных инструментов. Для Mac OS мне показался удобным MQTT.fx. Он бесплатный и выполняет все необходимые функции.
Теперь, когда наш брокер MQTT готов, можно заняться самим девайсом. Для начала припаяем BMP085 к нашему WiFi-модулю

В качестве протокола общения с BMP мы используем I2C. К счастью, нам не придется полностью реализовывать процесс общения с модулем, все уже сделано за нас. У нас есть два варианта. Первый - библиотека написанная на C, для этого будет необходимо включить соответствующий модуль и пересобрать прошивку. Либо использовать lua реализацию.
Если есть желание использовать C модуль, то это делается в по адресу app/include/user_modules.h
#define LUA_USE_MODULES_BMP085Дальнейшую сборку можно обеспечить с помощью Docker выполнив команду из корня проекта
docker run --rm -ti -v `pwd`:/opt/nodemcu-firmware marcelstoer/nodemcu-build buildОднако спешу заметить, что есть вероятность столкнуться с такой проблемой, что данные с датчика будут приходить довольно таки дикие. Вот тут issue с проблемой с которой столкнуться лично я https://github.com/nodemcu/nodemcu-firmware/issues/1471.
Использование Lua реализации не требует пересборки проекта и ее можно взять в репозитории прошивки NodeMCU по адресу `lua_modules/bmp085/bmp085.lua`. В моем случае у меня не корректно работало форматирование температуры, да и по какой то причине модуль был удален из ветки master, поэтому я залил его отдельным файлом с исправлениями:
--------------------------------------------------------------------------------
-- BMP085 I2C module for NODEMCU
-- NODEMCU TEAM
-- LICENCE: http://opensource.org/licenses/MIT
-- Christee <[email protected]>
--------------------------------------------------------------------------------
local moduleName = ...
local M = {}
_G[moduleName] = M
--default value for i2c communication
local id=0
--default oversampling setting
local oss = 0
--CO: calibration coefficients table.
local CO = {}
-- read reg for 1 byte
local function read_reg(dev_addr, reg_addr)
i2c.start(id)
i2c.address(id, dev_addr ,i2c.TRANSMITTER)
i2c.write(id,reg_addr)
i2c.stop(id)
i2c.start(id)
i2c.address(id, dev_addr,i2c.RECEIVER)
local c=i2c.read(id,1)
i2c.stop(id)
return c
end
--write reg for 1 byte
local function write_reg(dev_addr, reg_addr, reg_val)
i2c.start(id)
i2c.address(id, dev_addr, i2c.TRANSMITTER)
i2c.write(id, reg_addr)
i2c.write(id, reg_val)
i2c.stop(id)
end
--get signed or unsigned 16
--parameters:
--reg_addr: start address of short
--signed: if true, return signed16
local function getShort(reg_addr, signed)
local tH = string.byte(read_reg(0x77, reg_addr))
local tL = string.byte(read_reg(0x77, (reg_addr + 1)))
local temp = tH*256 + tL
if (temp > 32767) and (signed == true) then
temp = temp - 65536
end
return temp
end
-- initialize i2c
--parameters:
--d: sda
--l: scl
function M.init(d, l)
if (d ~= nil) and (l ~= nil) and (d >= 0) and (d <= 11) and (l >= 0) and ( l <= 11) and (d ~= l) then
sda = d
scl = l
else
print("iic config failed!") return nil
end
print("init done")
i2c.setup(id, sda, scl, i2c.SLOW)
--get calibration coefficients.
CO.AC1 = getShort(0xAA, true)
CO.AC2 = getShort(0xAC, true)
CO.AC3 = getShort(0xAE, true)
CO.AC4 = getShort(0xB0)
CO.AC5 = getShort(0xB2)
CO.AC6 = getShort(0xB4)
CO.B1 = getShort(0xB6, true)
CO.B2 = getShort(0xB8, true)
CO.MB = getShort(0xBA, true)
CO.MC = getShort(0xBC, true)
CO.MD = getShort(0xBE, true)
end
--get temperature from bmp085
--parameters:
--num_10x: bool value, if true, return number of 0.1 centi-degree
-- default value is false, which return a string , eg: 16.7
function M.getUT(num_10x)
write_reg(0x77, 0xF4, 0x2E);
tmr.delay(10000);
local temp = getShort(0xF6)
local X1 = (temp - CO.AC6) * CO.AC5 / 32768
local X2 = CO.MC * 2048/(X1 + CO.MD)
local r = (X2 + X1 + 8)/16
if(num_10x == true) then
return r
else
return (r/10)
end
end
--get raw data of pressure from bmp085
--parameters:
--oss: over sampling setting, which is 0,1,2,3. Default value is 0
function M.getUP_raw(oss)
local os = 0
if ((oss == 0) or (oss == 1) or (oss == 2) or (oss == 3)) and (oss ~= nil) then
os = oss
end
local ov = os * 64
write_reg(0x77, 0xF4, (0x34 + ov));
tmr.delay(30000);
--delay 30ms, according to bmp085 document, wait time are:
-- 4.5ms 7.5ms 13.5ms 25.5ms respectively according to oss 0,1,2,3
local MSB = string.byte(read_reg(0x77, 0xF6))
local LSB = string.byte(read_reg(0x77, 0xF7))
local XLSB = string.byte(read_reg(0x77, 0xF8))
local up_raw = (MSB*65536 + LSB *256 + XLSB)/2^(8 - os)
return up_raw
end
--get calibrated data of pressure from bmp085
--parameters:
--oss: over sampling setting, which is 0,1,2,3. Default value is 0
function M.getUP(oss)
local os = 0
if ((oss == 0) or (oss == 1) or (oss == 2) or (oss == 3)) and (oss ~= nil) then
os = oss
end
local raw = M.getUP_raw(os)
local B5 = M.getUT(true) * 16 - 8;
local B6 = B5 - 4000
local X1 = CO.B2 * (B6 * B6 /4096)/2048
local X2 = CO.AC2 * B6 / 2048
local X3 = X1 + X2
local B3 = ((CO.AC1*4 + X3)*2^os + 2)/4
X1 = CO.AC3 * B6 /8192
X2 = (CO.B1 * (B6 * B6 / 4096))/65536
X3 = (X1 + X2 + 2)/4
local B4 = CO.AC4 * (X3 + 32768) / 32768
local B7 = (raw -B3) * (50000/2^os)
local p = B7/B4 * 2
X1 = (p/256)^2
X1 = (X1 *3038)/65536
X2 = (-7357 *p)/65536
p = p +(X1 + X2 + 3791)/16
return p
end
--get estimated data of altitude from bmp085
--parameters:
--oss: over sampling setting, which is 0,1,2,3. Default value is 0
function M.getAL(oss)
--Altitudi can be calculated by pressure refer to sea level pressure, which is 101325
--pressure changes 100pa corresponds to 8.43m at sea level
return (M.getUP(oss) - 101325)*843/10000
end
return MДалее нам необходимо задать файл init.lua который будет запускаться при запуске самой NodeMCU. Его можно взять с официального сайта https://nodemcu.readthedocs.io/en/master/upload/#initlua. В принципе он выполняет все необходимые операции по подключению к WIFI. Необходимо только лишь добавить credentials.lua со всеми необходимыми данными WIFI. Туда же мы и добавим остальную конфигурацию:
-- Параметры WIFI
local PASSWORD = "Пароль WIFI"
local SSID = "SSID WIFI"
-- Параметры MQTT брокера
local MQTT_BROKER = "IP адрес MQTT брокера"
local MQTT_DEVICE_ID = "ID текущего устройства. На ваш выбор"
-- Порты куда мы припаяли BMP085
local I2C_SDA = 2
local I2C_SCL = 1После того как WIFI был успешно подключен, управление программой передается в файл application.lua. В нем мы опишем процесс подключения к MQTT брокеру, чтение данных с BMP085 и их последующую отправку.
dofile("credentials.lua")
function configureBMP()
print ("Configure BMP085 Module")
local bmp = require("bmp")
bmp.init(I2C_SDA, I2C_SCL)
end
function getPressure()
return bmp.getUP(3)
end
function getTemperature()
return bmp.getUT(false)
end
function publish()
print("Start publish timer")
timer = tmr.create()
-- Период отправки 30 секунд
timer:register(30000, tmr.ALARM_AUTO, function()
print("Sending data...")
local t = getTemperature()
m:publish("temperature/" .. MQTT_DEVICE_ID ,t,0,0)
local p = getPressure()/100
m:publish("pressure/" .. MQTT_DEVICE_ID,p,0,0)
end)
timer:start()
end
function mqttConnect()
print ("Connecting to MQTT Broker to", MQTT_BROKER, " with id ", MQTT_CLIENT_ID)
m = mqtt.Client(MQTT_CLIENT_ID, 120)
m:on("offline", function(T)
print ("MQTT Broker is offline")
end)
m:connect(MQTT_BROKER,1883,0,1, function(T)
print ("MQTT Broker is online")
publish()
end)
end
configureBMP()
mqttConnect()Здесь мы вызываем подключение к MQTT брокеру и в случае успеха запускаем отправку данных раз в 30 секунд.
Для отправки файлов lua на NodeMCU можно воспользоваться программой nodemcu-uploader. Она доступна на GitHub https://github.com/kmpm/nodemcu-uploader.
nodemcu-uploader --port /dev/cu.usbserial-1450 upload credentials.lua
nodemcu-uploader --port /dev/cu.usbserial-1450 upload bmp.lua
nodemcu-uploader --port /dev/cu.usbserial-1450 upload application.lua
nodemcu-uploader --port /dev/cu.usbserial-1450 upload init.luaАдрес USB порта скорее всего будет другой. Для MacOS его можно посмотреть командой
ls /dev/cu.usbserial-*Так как у вас скорее всего ничего другого кроме NodeMCU на данный момент не подключено, то выбор между адресами будет не велик.
Когда мы успешно записали все 4 файла на NodeMCU, то можно нажимать на кнопку Reset на модуле или же просто отключить/подключить питание. Следить за процессом подключения в модуле можно по UART на том же порту через любой serial терминал. Например, screen
screen /dev/cu.usbserial-1430Если вы все сделали правильно, то в MQTT клиенте можно наблюдать данные с модуля


Как вы будете чувствовать себя когда вы спросили кого либо в магазине про товар, а они просто зависли? Вы вероятно разочаруетесь и пойдете дальше. Пользователи чувствуют подобное когда видят спиннер на экране долгое время
Спиннер не сообщает пользователю как много времени займет процесс загрузки. Если вы используете его для длительных процессов, то будет похоже, что с приложением что то не так. Отсутсвие обратной связи вызывает состояние неопределенности, которое заставляет пользователей предполагать худшее.
Они будут предполагать, что процесс займет много времени и нетерпение вынудит пользователя нажать кнопку возврата или выйти из приложения.

Если вы хотите удержать пользователя в своем приложении, не используйте спиннеры для процессов которые занимают более 4-х секунд. Проведенные исследования показали, что большинство пользователей терпеливо ожидают около 4-х секунд. Это означает, что их поведение изменится после 4-х секунд.
Пользователи ожидают, что приложение будет отвечать немедленно. Немедленный ответ считается ответ пришедший за срок менее одной секунды. Если они не получат визуальный фидбек после одной секунды, то это может стать поводом для волнений.
Если у вас есть процесс который займет время более одной секунды, вы должны показать спиннер. Это скажет пользователю, что загрузка идет и не стоит волноваться

Если процесс занимает более четырех секунд, вы должны использовать прогресс бар. Пользователи становятся более терпеливыми во время должного ожидания, если видят прогресс бар.

Шкала прогресс бара позволяет пользователю увидеть как походит прогресс и дает представление о том, сколько осталось ждать. Если они увидят спиннер, то они не смогут видеть процесс и не смогут даже узнать, что их запрос был обработан. Это не дает стимула ждать.
Ваш прогресс бар должен анимироваться слева направо. Если анимация зависнет на одном месте слишком на долго, то пользователи могут подумать, что он застрял и они не захотят больше ждать

Вы можете также добавить числовое описание процесса. Если процесс длится менее одной минуты, то покажите процент выполнения или количество загруженных элементов. Проинформируйте тем самым, что приложение работает.
Если же процесс занимает более одной минуты, вы можете показать оставшееся время. Это даст понять пользователю, что процесс может занять долгое время. Отображение чисел в минутах позволит им вернуться в экран после ухода.
Многие дизайнеры имеют привычку использовать спиннеры на всех процессах, но когда вы используете спиннеры для долгих процессов, вы разочаровываете пользователя. Чтобы избежать этого, используйте прогресс бары.
Прогресс бар делает длительные процессы допустимыми. Пользователь не против подождать, если они знают, что приложение работает, но если процесс занимает больше времени чем ожидалось, то они нуждаются в визуальном отображении процесса.
]]>
В мире форм существует два типа данных основанных на текстовых полях. Это поля без формата (только текст) и поля имеющие формат (текст и символы). Вы должны четко разделять эти типы.
Многие дизайнеры возлагают на пользователя форматирование данных. Этот вызывает серьезные проблемы при заполнении форм.
Когда пользователи вводят свои данные они обращают внимание, что они не форматированы. Это заставит их задуматься, должны ли они вводить какие либо дополнительные символы или нет, что может ввести пользователя в состояние неопределенности и отобьет желание заполнять форму.
Пользователи которые решат ввести данные в формате будут вынуждены нажимать дополнительные клавиши для ввода символов. Более того, это увеличивает вероятность ошибки при вводе данных. Это может замедлить процесс заполнения формы и привести к отказу от заполнения формы.

Кроме того, пользователю придется решить в каком виде вводить данные. Например, телефон можно ввести через дефис или же использовать проблемы. При вводе даты можно использовать слеш или же дефисы.
Эта неопределенность вызовет лишнюю умственную работу для пользователя, а так же заставляет задуматься, пройдут ли форматированные данные валидацию или нет.
Существуют пользователи, которые предпочитают не добавлять какое либо форматирование при вводе данных, что в последующем затрудняет поиск ошибок и их исправление.

Например, при вводе номера кредитной карты без разделения его на группы цифр, будет сложно проверить введенный номер, так как номер будет слишком длинный (более чем 19 цифр). После проверки очередной группы цифр придется каждый раз искать место где пользователь остановился. Вместо этого можно было бы проверять небольшие группы цифр, что заметно бы ускорило процесс.
Лучшим способом для улучшения удобства заполнения формы является использования масок. Маски будут автоматически корректировать ввод данных по мере заполнения данных пользователем. Они не должны вводить какие либо символы.

Больше нет необходимости волноваться о том какой формат использовать и видя данные в форматированном виде пользователю будет проще найти и исправить ошибку.
Вы должны применять маски в тех полях, которые имеют особый формат. Например, телефонные номера, даты, время, серийные номера или номера кредитных карт.
Большенство текстовых полей требуют только ввод символов. Но не стоит игнорировать форматирование полей там где это необходимо. Возьмите на себя ответственность за форматирование введенных данных.
Оригинал - Why Formatted Data Fields Always Need Input Masks
]]>Очень часто приходится сталкиваться с различными нестандартными элементами интерфейса, когда имплементация от уже готовых элементов не удобна да и затратила бы много времени и сил, нагромождая проект огромным количеством костылей, приходится прибегать к реализации собственных компонентов.
Разберем задачу на примере радио-кнопки. В изначальной поставке у нас есть похожий компонент UISwitch, но давайте создадим свой собственный компонент. Для этого необходимо создать новый класс от UIControl с поддержкой использования его в Interface Builder.
Анимацию нашей радио-кнопки мы будем рисовать в QuartzCode. Этот инструмент позволяет с помощью примитивов и изображений быстро набросать картинку и анимаировать ее средствами CoreAnimation, а после экспортировать все это в Objective-C или Swift код уже готовый для внедрения в проект. Не будем углубляться в тонкости работы с этим инструментом, а уже перейдем к самому коду.
#import <UIKit/UIKit.h>
IB_DESIGNABLE
@interface TLCheckBox : UIView
- (void)addCheckAnimation;
- (void)addCheckAnimationCompletionBlock:(void (^)(BOOL finished))completionBlock;
- (void)addCheckAnimationReverse:(BOOL)reverseAnimation totalDuration:(CFTimeInterval)totalDuration completionBlock:(void (^)(BOOL finished))completionBlock;
- (void)removeAnimationsForAnimationId:(NSString *)identifier;
- (void)removeAllAnimations;
@end
Мы получили код в виде класса UIView с методами для анимаций. Если вы поставили галочку на Reverse Animation при экспортировании проекта в код, то у вас будут доступна возможность обратить анимацию, которую мы используем при переходе радио-кнопки в состояние unchecked.
Макрос IB_DESIGNABLE говорит о том, что этот объект будет отрисовываться в Interface Builder. Метод отрисовки вызываемый Interface Builder называется drawRect:. Перенесем код инициализации из initWithFrame: и initWithCoder: в этот метод.
- (void)drawRect:(CGRect)rect {
[self setupProperties];
[self setupLayers];
};
Запустив проект и добавив UIView с классом нашего компонента, то мы сможем увидеть начальное состояние картинки. Теперь необходимо поле, которое будет отвечать за сохранение состояния кнопки и реализуем setter для нее.
@property (nonatomic) IBInspectable BOOL isChecked;
- (void) setIsChecked:(BOOL)isChecked {
_isChecked = isChecked;
if (isChecked) {
[self addCheckAnimationReverse:NO
totalDuration:0.5f
completionBlock:nil];
} else {
[self addCheckAnimationReverse:YES
totalDuration:0.5f
completionBlock:nil];
}
}
IBInspectable говорит о том, что это поле может редактироваться из Interface Builder. Но проблема вот проблема, как бы мы не тыкали это свойство в настройках UIView, ничего не изменяется. Для этого необходимо добавить небольшой код в drawRect::
- (void)drawRect:(CGRect)rect {
[self setupProperties];
[self setupLayers];
#if TARGET_INTERFACE_BUILDER
if (!self.isChecked) {
((CAShapeLayer *) self.layers[@"oval2"]).fillColor = [UIColor clearColor].CGColor;
}
#else
self.isChecked = _isChecked;
#endif
};
Такая конструкция нам позволяет сделать выполнить отдельный код, если исполнение кода происходит для Interface Builder. Здесь мы просто заливаем центральный кружок прозрачным цветом.
Теперь с отображением все хорошо, давайте чутка улучшим сгенерированный QuartzCode код для большей гибкости. Создадим переменные для определения размеров внутреннего и внешнего круга и напишем для них getter-ы:
@property (nonatomic) CGFloat radioSize;
@property (nonatomic) CGFloat centerSize;
@property (nonatomic) CGFloat strokeWidth;
- (CGFloat)radioSize {
return self.frame.size.width;
}
- (CGFloat)centerSize {
return self.radioSize * 7/11;
}
- (CGFloat)strokeWidth {
return 2.0f;
}
Далее заменим цифры в setupLayers:
- (void)setupLayers{
CAShapeLayer * oval = [CAShapeLayer layer];
oval.frame = CGRectMake(0, 0, self.radioSize-self.strokeWidth, self.radioSize-self.strokeWidth);
oval.path = [self ovalPath].CGPath;
[self.layer addSublayer:oval];
self.layers[@"oval"] = oval;
CAShapeLayer * oval2 = [CAShapeLayer layer];
CGFloat frameX = (self.radioSize - self.centerSize)/2;
oval2.frame = CGRectMake(frameX, frameX, self.centerSize, self.centerSize);
oval2.path = [self oval2Path].CGPath;
[self.layer addSublayer:oval2];
self.layers[@"oval2"] = oval2;
[self resetLayerPropertiesForLayerIdentifiers:nil];
}
Теперь добавим возможность изменять цвет радио-кнопки. Для этого необязательно добавлять еще одно поле с цветом. Тут мы можем использовать tint у UIView. Добавим это в resetLayerPropertiesForLayerIdentifiers:
- (void)resetLayerPropertiesForLayerIdentifiers:(NSArray *)layerIds{
[CATransaction begin];
[CATransaction setDisableActions:YES];
if(!layerIds || [layerIds containsObject:@"oval"]){
CAShapeLayer * oval = self.layers[@"oval"];
oval.fillColor = nil;
oval.strokeColor = self.tintColor.CGColor;
oval.lineWidth = self.strokeWidth;
}
if(!layerIds || [layerIds containsObject:@"oval2"]){
CAShapeLayer * oval2 = self.layers[@"oval2"];
oval2.fillColor = self.tintColor.CGColor;
oval2.lineWidth = 0;
}
[CATransaction commit];
}
Не забываем, что анимации все еще привязаны к старым размерам и их необходимо также отредактировать. Как и методы Bezier Path в самом низу кода.
Так же давайте добавим возможность изменять скорость работы анимации. Добавим поле и интегрируем в проект.
@property (nonatomic) IBInspectable CGFloat animationDuration;
- (void) setIsChecked:(BOOL)isChecked {
_isChecked = isChecked;
if (isChecked) {
[self addCheckAnimationReverse:NO
totalDuration:self.animationDuration/1000.0f
completionBlock:nil];
} else {
[self addCheckAnimationReverse:YES
totalDuration:self.animationDuration/1000.0f
completionBlock:nil];
}
}
Изменение скорости задается в миллисекундах. Теперь необхоимо задать значения по умолчанию. Хорошей практикой было бы вынести все задания этих значений в метод inspectableDefaults:
- (void) inspectableDefaults {
self.animationDuration = 500.0f;
self.isChecked = false;
}
Необходимо вставить в этот метод в блоки инициализации UIView.
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self inspectableDefaults];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self inspectableDefaults];
}
return self;
}
Теперь давайте изменим родительский класс с UIView на UIControl и реализуем функционал нажатия. Основные методы которые советуют перепределить это:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event- (void)cancelTrackingWithEvent:(UIEvent *)eventТак как радио-кнопка не имеет каких то сложных обработок нажатий то мы наделим логикой только endTrackingWithTouch:withEvent::
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
[super endTrackingWithTouch:touch withEvent:event];
[self toggleCheckBox];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
Здесь у нас используется метод toggleCheckBox для того чтобы инвентировать состояние кнопки и отправку евента о том, что произошло изменение состояния кнопки.
Весь код вместе с тестовым проектом можно посмотреть на Github: Ссылка
]]>Данная статья является вольным переводом из официальной д
]]>Данная статья является вольным переводом из официальной документации. В случае каких то неясностей прошу обратиться к первоисточнику: App Programming Guide for iOS
Когда пользователь не активно пользуется вашим приложением, система перемещает его в состояние background. Для многих приложений, это состояние просто короткая остановка на пути к полному выключению suspended. Приложения в состоянии suspended сохраняют зарят батарей и позволяют системе освободить важные для системы ресурсы для новый приложений требуемых внимания пользователя.
Большинство приложений может быть перенесено в “растянутое” состояние достаточно просто, но есть так же законные основания для продолжения работы в фоновом режиме. Туристическое приложение, возможно, захочет отслеживать позицию человека в течении долгого времени, так что оно может отображать курс накладывая его поверх туристической карты. Музыкальное приложение может нужнаться в продолжении проигрывания музыки даже при блокировке экрана. Другие приложения могут хотеть скачивать данные в фоне, чтобы минимизировать задержку в отображении данных пользователю. Когда вам понадобиться держать ваше приложение запущенным в фоне, iOS поможет вам эффективно сделать это и без “съедания” ресурсов системы и аккумулятора. Методы предлагаемые iOS деляться на три категории:
Всегда пытайтеть избегать делать какую либо фоновую работу если это не улучшает UX. Приложение может быть перемещено в фон, потому что пользователь запустил другое приложение или потому что пользователь заблокировал устройство и не использовать его в данный моментю В основных ситуациях, пользователь оповещает ваше приложение о том, что не нужно выполнять какую либо работу сейчас. Продолжая работать в таких случаях ваше приложение будеть только разряжать аккумулятор устройства и может вынудить пользователя бросить пользоваться им в целом. Так помня о работе которую вы делаете в фоновом режиме, старайтесь ее избегать когда это возможно.
Приложения перемещенные в фон, как ожидается, должны выйти из этого состояния как можно быстрее, так как они могут быть остановлены системой. Если ваше приложение находиться в середине задачи и вам нужно немного дополнительного времени для ее завершения, то вы можете вызвать методы beginBackgroundTaskWithName:expirationHandler: или beginBackgroundTaskWithExpirationHandler:](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIApplication_Class/index.html#//apple_ref/occ/instm/UIApplication/beginBackgroundTaskWithExpirationHandler:) объекта UIApplication запросив дополнительное время. Вызов любого из этих методов отстрочит ненадолго закрытие вашего приложения, дав немного дополнительного времени для завершения работы. После завершения этой работы ваше приложение должно вызвать метод endBackgroundTask: дав системе понять, что задача завершена и приложение может быть закрыто.
Каждый вызов методов beginBackgroundTaskWithName:expirationHandler: или beginBackgroundTaskWithExpirationHandler: генерирует уникальный токен для каждой задачи. Когда приложение завершит задачу, он должен вызвать метод endBackgroundTask: с соответствующим токеном дав системе понять, что именно эта задача была завершена. Отказ от вызова этого метода для фоновой задачи приведет к прекращению работы вашего приложения. Если вы предоставили обработчик когда запускали задачу, система вызовет этот обработчик дав вам последний шанс на то, чтобы закончить задачу и избежать выключения.
У вас нет необходимости ждать пока приложение будет перенесено в состояние фоновой работы для обозначения фоновых задач. Более полезным методом разработки является вызов beginBackgroundTaskWithName:expirationHandler: или beginBackgroundTaskWithExpirationHandler: до запуска задачи и вызов метода endBackgroundTask: после завершения. Вы можете следить за этим паттерном пока ваше приложение работает на переднем плане.
Листинг 3-1 показывает как можно запустить долго-работающую задачу когда ваше приложение перемещено в фон. В этом примере, происходит запрос к фоновой задаче включая обработчик в случае если задача выполняется слишком долго. Сама задача ставится в очереднь для асинхронного запуска, так что метод applicationDidEnterBackground: может вернутся нормально. Используйте упрощающие блоки кода необходимые для поддержания ссылок на любые важные перменные, таких как идентификатор фоновой задачи. bgTask это переменная класса хранящая ссылку на данную фоновую задачу и инициализируемая в этом методе.
Листинг 3-1 Запуск фоновой задачи в момент выхода
- (void)applicationDidEnterBackground:(UIApplication *)application
{
bgTask = [application beginBackgroundTaskWithName:@"MyTask" expirationHandler:^{
// Clean up any unfinished task business by marking where you
// stopped or ending the task outright.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do the work associated with the task, preferably in chunks.
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
});
}
Обратите внимание: Всегда предоставляйте перехватчик завершения во время запуска задачи, но если вы хотите знать как много времени займет ваша задача от запуск, возьмите значение поля backgroundTimeRemaining из UIApplication.
В ваших собственных обработчиков завершения, вы можете вставить дополнительный код чтобы закрыть вашу задачу. Тем не менее, любой код вставленый там не должен выполняться слишком долго, потому что как только обработчик вызывается у вас остается не так много времени до завершения работы приложения. По этой причине, выполните лишь минимальную очистку вашего состояния и завершите задачу.
Когда загружаются данные, приложения должны использовать объект NSURLSession для запуска загрузки, так система сможет взять управление процессом загрузки на себя в случае если приложение было выключено. Когда вы настраиваете этот объект для передачи в фоне, система управляет этой передачей в отдельном процессе и оповещает о статусе работы вашему приложениею в обычном случае. Если ваше приложение было выключено, пока продолжалась передача, система продолжит передачу в фоне и запустить ваше приложение (если это уместно) когда передача завершится или одна или более задач требует внимания вашего приложения.
Для поддержки фоновых передач, вы должны настроить ваш объект. Настраивая сессию, вы должны сначала создать объект NSURLSessionConfiguration и выбрать несколько настроек. Вы отправляете конфигурацию в объект в момент инициализации объекта NSURLSession.
Процесс создания объекта конфигурации, который поддерживает фоновые загрузки выглядит следующим образом:
Единожды сконфигурированный, ваш объект NSURLSession сможет использоваться для задач выгрузки и скачивания в системе в нужное время. Если задачи выполнены пока приложение все еще запущено (или на переднем плане или в фоне), объект сессии будет уведомлен в обычном порядке. Если задачи не завершены и система выключива ваше приложение, система автоматический продолжит управлять задачей в фоне. Если пользователь зщавершил задачу, система завершит люую выполняемую задачу.
Когда все задачи происходящие в фоновой сесси были завершены, система перезапускает выключеное приложение (предпологается, что свойство sessionSendsLaunchEvents установлено в YES и пользователь не завершил работу приложения самостоятельно) и вызывает приложение с помощью метода application:handleEventsForBackgroundURLSession:completionHandler:. (Система может также перезапустить приложение для выполнения задач аутентификкации и других событий, связанные с вниманием вашего приложения.) В вашем приложении реализация этого метода, происходит с созданием нового NSURLSessionConfiguration и NSURLSession объекта с похожей конфигурацией что и ранее. Система переподключает ваши новый объект сессии на предыдущие задачи и оповещает об их статусе в новый объект сессии.
Для задач которые требующют более расширеное время для работы, вы должны запросить специальные права для запуска их в фоне без их выключения. В iOS, только определенные типы приложений могут выполняться в фоне:
Приложения реализующее эти сервисы, должны объявить эту поддержку и использовать системный фреймворк для реализации аспектов этого сервиса. Объявление сервисов позволяет системе узнать, какие сервисы вы используете, но в некоторых случаях это оповещает фреймворк, что ваше приложение должно не должно быть выключено.
Поддержка некоторых типов фоновых работ должна быть объявлена в вашим приложении. В Xcode 5 и позних версиях, вы объявляете о поддержке фоновой работы во вкладке Capabilities в насройках проекта. Включание опции фонового режима добавляеет ключ UIBackgroundModes в вашем файле Info.plist. Выберете один или более чекбоксов добавив необходимые опции фоновой работы. Таблица 3-1 описывает фоновые режимы которые вы можете выбрать в Xcode.
Таблица 3-1 Фоновые режимы приложения.
|
Xcode фоновой режим |
UIBackgroundModes значение |
Описание |
|
Аудио и AirPlay |
audio |
Приложение играет аудио данные пользователя или записывает данные в фоне. (Эти данные включают потокове аудио или видео данные используя AirPlay.) Пользователь должен разрешить приложению использовать пикрафон во время первого использвания; для большей информации читайте Supporting User Privacy. |
|
Обновление локации |
location |
Приложение информирует пользователя о его положении пока находиться в фоне |
|
IP-телефония |
voip |
Приложение позволяет пользователю совершать звонки через интернет. |
|
Загрзки Newsstand |
newsstand-content |
Приложение является приложением Newsstand, которое производить загрузки и обработку журналов или газет в фоне. |
|
Общение с внешними аксессуарами |
external-accessory |
Приложение работает с аппаратными аксессуарами необходимых в обновлении по расписанию через фреймворк External Accessor. |
|
Использование Bluetooth LE аксессуаров |
bluetooth-central |
Приложение работает с Bluetooth аксессуарами необходимые в обновлении по расписанию через фреймворк Core Bluetooth. |
|
Дейтвия с Bluetooth LE аксессуарами |
bluetooth-peripheral |
Приложние поддерживает общение через Bluetooth в режиме переферии через фреймворк Core Bluetooth. Этот режим используется для запросов пользовательской аутонтификации; для большей информации читайте Supporting User Privacy. |
|
Фоновые приемы |
fetch |
Приложение регулярно загружает и обрабатывает маленькие размеры информации из интернета. |
|
Удаленные уведомления |
remote-notification |
Приложение хочет загружать данные из push-уведомлений. Используйте эти уведомления чтобы минимизировать задержку для отображения данных из push-уведомлений. |
Каждый из объявленных методов дает системе знать, что ваше приложение должно быть пробуждено или запущено в нужное время чтобы ответить на пришедшие события. Например, приложение начало играть музыку и переходить в фон все еще продолжать работу заполняя выходные буферы аудио. Включение режима Audio говорит системному фреймворку, что он должен принимать callback функции приложения с соответствующими интервалы. Если приложение не выбрало этот режим, любое аудио начавшее играть или записывать будет завершено, когда приложение перейдет в фоновой режим.
Есть несколько путей для трекинга локации пользователя в фоне, большинтво из которых на самом деле не требуют неприрывной работы вашего приложения в фоновом режиме:
Сервис значительного изменения положения наиболее рекомендуется для приложений которым не нужно высокоточные данные о положении. С этим сервисом, изменения локации генерируются только когда местоположение пользователя существенно изменится; Таким образом это идеально для социальных приложений или приложений, что предостовляют пользователю уведомления полезные для данной местности. Если ваше приложение выключено, когда произошло жто события, система пробутит его в фон чтобы передать изменившееся состояние. Если приложение запустило этот сервис и было выключено, система перезапустит приложение автоматически, когда придут новые данные о локации. Этот сервис доступен с 4-й версии iOS, и она доступна только на устройствах содержащих модули сотовой связи.
В режиме работы только в переднем плане и фоновом режиме серсисы локации в обеих случаях используют стандартный серсис Core Location для получшения данных о метсоположении. Разница лишь в том, что сервисы работы только в переднем плане останавливаются когда приложение было выключено, что скорее всего произойдет если приложение не поддерживает фоновую работу или задачи. Сервис локации только в переднем плане предназначены для приложений, которые нуждаются получать локацию пока они находяться на переднем плане.
Вы включаете поддержку локации из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение location в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Включение этого режима не спасут приложение от возможности быть выключеными, но это говорит системе, что приложение должно быть пробуждено для того чтобы принять новые данные о локации. Таким образом, этот ключ позволяет приложению запускаться в фоне для обработки обновленных данных локации, когда они приходят.
Важно: Вам предлогается использовать стандартные сервисы экономно и использовать сервис значительного изменения локации. Службы локации требуют активного использования радиоаппаратуры iOS устройства. Запуск этого оборудования может непрерывно потреблять значительное количество энергии. Если ваше приложение не нуждается в точном предосталвении положения и непрерывного оповещения, предлогается свести это использование к минимуму.
Для информаии о том как использовать различные сервисы локации обратитесь к Location and Maps Programming Guide.
Приложение проигрывающее или записывающее аудио продолжительно (когда приложение запущено или находиться в фоне) может регистрировать эти задачи для выполенения в фоне. Вы можете включить поддержку аудио из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение audio в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Приложение, что играет аудиоданные в фоне должны играть играть звуковой контент, а не тишину.
Обычными примерами приложений играющих в фоне является:
Когда в ключе UIBackgroundModes выбрано значение audio, системный медиа фреймворк атоматически предотвращает закрытие приложения когда оно находится в фоновом режиме. Пока он играет аудио или видео контент или записывает звук, приложение будет запущено в фоне. Тем не менее, если запись или проигрывание остановиться, система выключит приложение.
Вы можете использовать любой из системных аудио фреймоврков для работы со звуком в фоне, и обрабатывать для использования в рамках фреймворка. (Для видео проигрывания через AirPlay, вы можете использовать Media Player или фреймворк AV Foundation для отображения видео.) Поскольку ваше приложение не может быть приостановлено пока играют медиафайлы, обратная связь работает нормально пока ваше приложение находиться в фоновом режиме. В вашей обратной связи вы должны выполнять работу только два управлением проигрывания. Например, приложение для потокового аудио должно качать данные о музыкальном потоке с сервера и помещать верные куски аудио в воспроизведенеи. Приложение не должны выполнять какие либо посторонние задачи, которые не связаны с воспроизведением.
Потому что более чем одно приложение может поддерживать аудио, система определяет, каким приложениям разрешено играть или записывать аудио в любой момент. Приложение на переднем плане всегда имеет наивысший преоритет для аудио операций. Вполне возможно, что более чем одно фоновое приложение может быть доступно к проиграванию аудио и такие определения основаны на конфигурации каждого объекта аудио сессии приложения. Вы всегда должны конфигурировать ваш объект аудио сессии нашлежащим образом и работать осторожно с фреймворком системы для обработки прерываний и других типов уведомлений связанные с аудио. Для большей информации о конфигурации объекта аудио сессий для работы в фоне читайте в Audio Session Programming Guide.
Приложение VoIP позволяет пользователю совершать телефонные звоки посредством интернет подключения через сотовую службу устройства. Такое приложение должно постоянно поддерживать интернет соединение с соответсвующей службой так что он может принимать входящие выховы и другие соответствующие данные. Вместо того, чтобы держать VoIP приложение запущеным все время, система позволяет ему быть приостановленным и предоставлять средства для мониторинга сокета для него. Когда взодящий трафик будет определен, система “разбудит” VoIP приложение и вернет ему контроль на сокетом.
Чтобы настроить VoIP приложение, вы должны сделать следующее:
Содержание значения voip в ключе UIBackgroundModes дает системе знать, что вашему приложению позволена работа в фоне и необходима работа с сетевыми сокетами. Приложение с этим ключем также перезапускается в фон немедленно после загрузки системы, что позволяет быть сервисам VoIP быть все время доступными.
Большинство приложений VoIP должны быть сконфигурированы как аудио приложение. Теким образом вы должны включить оба значения audio и voip в ключ UIBackgroundModes. Если вы не сделали это, ваше приложение не сможет играть и записывать звук в фоне. Для большей информации о ключе UIBackgroundModes, читайте Information Property List Key Reference.
Для более расширеной информации о шагах реализации VoIP приложения, читайте в Tips for Developing a VoIP App.
Приложения, которые должны иногда проверять о содержании новых данных, могут просить систему переодически буить их для инициирования получения этих данных. Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение fetch в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Включение этого режима не гарантирует, что система даст вашему приложению время чтобы получить данные в фоне. Система должна балансировать нужды приложений в получении контента и системы. После оценки этой информации, система дает время приложению, когда на это есть возможность.
Когда возникает хорошая возможность, ситема пробуждает или запускает ваше приложение из фона и вызывает объявленый метод application:performFetchWithCompletionHandler:. Используйте этот метод чтобы проверить о присутствии новых данных и инициирования операции загрузки контента. Как вы только закончите загрузку новых данных, вы должны вызвать предоставленый обработчик, передавая результат, говоря о том, что данные были приняты. выполнение этого блока говорит системе о том, что он может переместить ваше приложение обратно в приостановленое состояяние и оценить это энергопотребление. Приложение, которогое скачивает малые куски данных быстро и точно отвечает когда они были загружены, скорее всего получит время для выполения в будущем, чем те приложения которые занимают много времени, чтобы загрузить содержимое или получают проблемы с доступом и не загружают ничего.
При загрузке любых данных, рекомендуется использовать класс NSURLSession для инициализации и управлением вашими загрузками. Для большей информации о том, как использовать класс управления задачами выгрузки и загрузки, смотрите URL Loading System Programming Guide.
Если ваши сервера отправили push-уведомление устройству пользователя о том, что новые данные доступны для вашего приложения, вы можете запросить у системы запуск вашего приложения в фоне где вы сможете запустить загрузку новых данных. Этот запуск в фоне минимизирует время между тем когда пользователь увидит push-уведомление и когда ваше приложение начнет отображать соответсвующие данные. Приложения, как правила, просыпаются примерно в тоже время когда пользователь увидел уведомление, но это это все еще дает вам немного времени.
Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение remote-notification в ключ UIBackgroundModes в файле Info.plist вашего приложения.)
Чтобы включить операцию загрузки для push-уведомлений, уведомление должно содержать значение 1 в ключе content-available. Кгда ключ предосталвен, система “будит” приложение для работы в фоне (или запускает в фон) и вызывает объявленный метод application:didReceiveRemoteNotification:fetchCompletionHandler:. Ваша реализация этого метода должна загружать удаленные данные и интегрировать их в приложение.
При загрузке любых данных, рекомендуется использовать класс NSURLSession для инициализации и управлением вашими загрузками. Для большей информации о том, как использовать класс управления задачами выгрузки и загрузки, смотрите URL Loading System Programming Guide.
Приложение Newsstand, которое загружает новый журнал или газету может зарегестрировать загрузку в фоне. Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение newsstand-content в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Когда этот ключ предоставлен, система запускает ваше приложение, если оно все еще не запущено, чтобы запустить загрузку нового выпуска.
Когда вы используйте Newsstand Kit для старта скачивания, система принимает процесс загрузки на себя для вашего приложения. Система продолжить процесс загруски когда ваше приложение будет приостановлено или выключено. Когда процесс загрузки завершится, система передаст файл в sandbox приложения и оповестит ваше приложение. Если ваше приложение не запущено, это уведомление “разбудит” его и даст шанс обработать свежескаченный файл. Если произойдут какие либо ошибки в процессе загрузки, ваше приложение будет запущено для их обработки.
Для большей информации о том как использовать загруку данных с помощью фрейворка Newsstand Kit, читайте NewsstandKit Framework Reference.
Приложения работающие вс внешнему аксессуарами могут запросить свое “пробужение” если аксессуар поддерживает обновление, когда приложение приостановлено. Эта поддержка важна для некоторых типов аксессуаров, что обеспечивает данными через постоянные интервалы, такие как мониторы сердечного ритма. Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение external-accessory в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Когда этот режим включен, фреймворк внешних аксессуаров не закрывает активные сессии с аксессуаром. (В iOS 4 и ранее, эти сессии закрываются автоматически если приложение было приостановлено.) Когда новые данные приходят из аксессуара, фремворк открывает ваше приложение чтобы оно обработало полученые данные. Система так же может “пробудить” ваше приложение для обработки подключения и отключения аксессуара.
Любое приложение, поддерживающее фоновую обработку обновлений аксессуара должны следовать нескольким основным указаниям:
Приложения которые работают с Bluetooth переферии могут запросить “пробудить” себя если переферия поставит обновления когда ваше приложение приостановлено. Эта поддержка важна для Bluetooth-LE аксессурах, которые постовляет данные через постоянные промежутки времени, такие как Bluetooth мониторы сердцебиения. Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение bluetooth-central в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Когда вы включите этот режим, фрейворк Core Bluetooth будет оставлять активной сессию для общения с переферией. Ко всему прочему, новые данные пришедшие из переферии вынудят систему запустить ваше приложение в фоновом режиме для обработки принимаемых данных. Система так же “разбудит” ваше приложение для обработки ситуаций подключения и отключения аксессуара.
В iOS 6 приложение также может работать в переферийном режиме с Bluetooth аксессуарами. Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение bluetooth-peripheral в ключ UIBackgroundModes в файле Info.plist вашего приложения.) Включение этого режима заставит фремворк Core Bluetooth “пробуждать” ваше приложение в фоне, чтобы обработать принимаемые запросы. Приложение включаемое для этих событий должно обработать данные как можно быстрее и вернуться в в приостановленное состояние опять.
Любое приложение, поддерживающее фоновую обработку обновлений Bluetooth аксессуара должны следовать нескольким основным указаниям:
Уведомления это вариант для приложения, находящимся в приостановленом состояни, фоновом или выключеным, привлечь внимание пользователя. Приложение может использовать локальный уведомления для всполывающих окон, играния звуков, бейдеж на иконке приложения или комбинации всех. Например, приложение будильника может использовать локальные уведомления для проигрывания звонка будильника и отображать всплывающее окно для выключения будильника. Когда уведомление досталвено мользователю, пользователь может вернуть приложение на передний план. (Если приложение уже запущено в переднем плане, локальные уведомления доставляются тихо приложению, а не пользователю.)
Расписание может отправлять локальные уведомления, создав объект класса UILocalNotification, настроить параметры уведомления и запланировать его используя методы класса UIApplication. Объект локального уведомления содержит информацию о типе уведомления (звук, окно или бейдж) и времени (когда совершить). Методы класса UIApplication предоставляют опцию для поставки уведомления немедленно или в определенное время.
Листинг 3-2 показывает пример который планирует единичный будильник используя дату и время указанную пользователем. Этот пример настраивает только один будильник на время и отменяет его после создания нового будильника. (Ваше собственное приложение может создать не более 128 уведомлений в любое время, любое может быть настроено и повторяться с определенным интервалом.) Бульник содержит окно уведомления и звуковой файл, что играет если приложение не запущено или находиться в фоне. Если приложение активно и запущено в переднем плане, приложение объявляет метод application:didReceiveLocalNotification: вызываемым внутри.
Листинг 3-2 Планирование уведомления будильника
- (void)scheduleAlarmForDate:(NSDate*)theDate
{
UIApplication* app = [UIApplication sharedApplication];
NSArray* oldNotifications = [app scheduledLocalNotifications];
// Clear out the old notification before scheduling a new one.
if ([oldNotifications count] > 0)
[app cancelAllLocalNotifications];
// Create a new notification.
UILocalNotification* alarm = [[UILocalNotification alloc] init];
if (alarm)
{
alarm.fireDate = theDate;
alarm.timeZone = [NSTimeZone defaultTimeZone];
alarm.repeatInterval = 0;
alarm.soundName = @"alarmsound.caf";
alarm.alertBody = @"Time to wake up!";
[app scheduleLocalNotification:alarm];
}
}
Звуковой файл используется с локальными уведомления имеет такие же требования, что и push-уведомления. Пользовательские звукове файлы должны находиться в основном пакете приложения и поддерживать один из следующих фарматов: Linear PCM, MA4, µ-Law, или a-Law. Вы можете так же определеить костанта UILocalNotificationDefaultSoundName для проигрывание указанных пользователем звука уведомления на устройстве. Когда уведомление отправлено и играет звук, система может включить вибрацию на устройстве, если оно ее поддерживает.
Вы можете отменить запланированное уведомление или получить списко уведомлений используя методы класса UIApplication. Для большей информации о этих методах, смотрите UIApplication Class Reference. Для большей информации о конфигурации локальных уведомлений, смотрите Local and Remote Notification Programming Guide.
Приложение поддерживающий фоновое выполнение может быть перезапущена системой для обработки событий. Если приложение выключено для какой либо другой причины, кроме завершения инициированным пользователем, система может запустить приложение в следующих случаях:
Для навигационных приложений:
Для Bluetooth приложений:
Для приложений качающих данные в фоне:
В большинстве случаев, система не перезапускает приложение выключеное пользователем. Одним исключением является приложения навигации, которые в iOS 8 и позже перезапускают приложение после выключения пользователем. В други случаях, пользователь должен запустить приложение или перезапустить устройство для того чтобы приложение могло запуститься автоматически в фон системой.
Приложение на переднем плане всегда имеет преоритет над фоновым приложением, когда до использования системных ресурсов и аппаратных средств. Приложение запущеное в фоне должно быть готово для этого несоответствия и корректировать свое поведение когда оно работает в фоне. В частности, приложения перемещаемые в фон должны следовать следующим правилам:
Если вы реализовываете аудио приложение или любое другой тип приложения, которому разрешено запускататься в фоне, ваше приложение отвечает на пришедшие запросы в обчном порядке. Другими словами, система может уведомить ваше приложение о недостатке памяти, когда они происходят. И в ситуации, когда сисема должна выключить приложение и сонободить еще больше памяти, приложение вызывает объявленный метод applicationWillTerminate: чтобы выполнить какую либо финальную работу перед выключением.
Если вы не хотите запускать ваше приложение в фоне, то можете добавить ключ UIApplicationExitsOnSuspend (со значением YES) в вашем файле Info.plist. Когда приложение работает в этом режиме, то его циклы not-running, inactive, и active состояния никогда не попадают в состояния фона или приостановки. Когда пользователь нажимает на кнопку Home, приложение выключается. вызывая метод applicationWillTerminate: объявленный в вашем приложении давая примерно 5 секунд на очистку и выход, прежде чем завершится и перевестись в состояние not-running.
Опция отказа от работы в фоне может быть предпочтителен в некоторых условиях. В частности, если работа в фоне значительно усложняет ваше приложение. Кроме того, если ваше приложение потребляет большие объемы памяти и не может легко освободить ее, то система убьет ваше приложение быстро в любом случае, чтобы освободить место для других приложений. Таким образом может пренести пользу, чтобы сохранить время разработки.
Для большей информации о ключах содержащихся в файле Info.plist, читайте Information Property List Key Reference.
]]>Данная статья является вольным переводом из официальной д
]]>Данная статья является вольным переводом из официальной документации. В случае каких то неясностей прошу обратиться к первоисточнику: App Programming Guide for iOS
Приложение тесно общается между кодом разработчика и фреймворком системы. Фреймвок предоставляет базовую инфраструктуру, которая необходима приложениям для запуска, а вы предоставляете код необходимый для настройки этой инфраструктуры для того чтобы получить то приложение, которое вы хотите. Это поможет понять инфраструктуру iOS и то, как система работает.
Фреймворки iOS полагаются на паттерн MVC и делегируют эту реализацию. Понимание этих паттернов разработки это ключ к успешной разработки приложений. Это так же помогает в знакомстве с Objective-C и его возможностями. Если вы новичок в iOS программировании, то предлагаем почитать Start Developing iOS Apps Today для введения в iOS приложения и язык Objective-C.
Входной точкой в каждом приложении основанном на C является функция main в iOS приложениях это правильно так же соблюдается. Единственное чем отличается, это то, что в iOS прилоежении вы не должны писать в функцию main самостоятельно. Xcode создает эту функцию как часть основы для вашего проекта. Листинг 2-1 показывает пример этой функции. За исключением нескольких случаев, вы никогда не должны изменять реализацию функции main предоставляемой Xcode.
Листинг 2-1 Функция main приложения iOS
#import
#import "AppDelegate.h"
int main(int argc, char * argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
Единственное что можно сказать о функции main это то, что работа передается под управление фреймворка UIKit. Функция UIApplicationMain передает этот процесс созданием объекта ядра вашего приложения, загрузкой пользовательского интерфейса вашего приложения из файлов storyboard, вызовом вашего кода, что дает вам шанс настроить что то во время запуска, и вложение вашего приложения в цикл. Единственной вещью, что вам необходимо предоставить, это файлы storyboard и ваш инициализационный код.
Во время запуска, функция UIApplicationMain выбирает несколько ключевых объектов и запускает приложение. В сердце каждого iOS приложения лежит объект UIApplication, который способствует взаимодействию между системой и другими объектами приложения. Рисунок 2-1 показывает основные объекты которые можно найти в большинстве приложений, Таблица 2-1 описывает роли которые играет каждый объект. Обратите внимание, что iOS приложение использует MVC архитектуру. Этот паттерн отделяет данные приложение от бизнес-логики и визуальным представлением данных. Эта архитектура ключевая в создании приложения, которое способно запускаться на различных устройствах с различными размерами экрана.
Рисунок 2-1 Ключевые объекты iOS приложения
Таблица 2-1 Роли объектов в iOS приложении
|
Объект |
Описание |
|
UIApplication |
Объект UIApplication управляет циклом событий и другими высокоуровневыми поведением приложения. Он так же сообщает ключ транзакций приложения и некоторые специальные события (такие как приход push-уведомлений) для их делигирования, каждому пользовательскому объекту который вы укажите. Использует объект UIApplication без наследования. |
|
App delegate |
App delegate это сердце вашего кода. Этот объект работает в тандеме с объектом UIApplication во время инициализации, передач, и различных выскоуровневых событиях приложения. Этот объект так же единственный который гарантированно присутствует в каждом приложении, поэтому он часто используется чтобы установить начальные структуры данных приложения. |
|
Documents и data model |
Data model хранит данные вашего приложения и является уникальным для вашего приложения. Например, в банковском приложении может храниться база финансовых операций, в то время как приложение для рисования может хранить изображение объекта или даже историю команд рисования, которые привели к созданию этого изображения. (В последнем случае объект изображения по прежнему объект данных, потому что Data model всего лишь контейнер для данных изображения) Приложение так же может использовать объекты document (Наследованный класс от UIDocument) управляющий некоторыми или всеми его объектами Data model. Объекты Document не обязательны, но предлагают удобный способ для группировки данных, которые принадлежат одному файлу или пакету файлов. Для большей информации читайте Document-Based App Programming Guide for iOS. |
|
View controller |
View controller управляет отображением данных вашего приложения на экране. View controller управляет одним единственным View и его набором view. Когда происходит отображение, View controller делает view видимыми и устанавливает их в окне приложения. Класс UIViewController является основным классом для всех объектов view controller. Это обеспечивает функциональность по умолчанию для загрузки view, их отображения, поворота во время поворота устройства, и некоторые другие поведения системы. UIKit и другие фреймворки определяют дополнительный класс view controller реализующий стандартный системный интерфейс такой как выбор изображения, интерфейс с вкладками и навигационный интерфейс. Для более детальной информации о view controller, смотрите в View Controller Programming Guide for iOS. |
|
UIWindow |
Объект UIWindow координирует показом одного или более views на экране. Большинство приложений имеет один объект window, который отображает данные основного экрана, но приложения могут иметь дополнительные window для отображения данных на дополнительных экранах. Чтобы изменить данные вашего приложения, используйте view controller для изменения отображаемых данный в соответствующем window. Никогда не перемещается сам window. В дополнение в hosting view, window работает с объектом UIApplication доставляющее события в ваши view и view controller-ы. |
|
View, control, и layer объекты |
View и control-ы предоставляют визуальное представление данных вашего приложения. view это объект рисующий данные в разрабатываемой квадратной зоне и возвращает события из этой зоны. Control-ы это специализированный тип view ответственный за реализацию привычных объектов интерфейса таких как кнопки, текстовые поля или переключатели. Фреймворк UIKit предоставляет стандартные view для отображения различных типов данных. Вы можете переопределить свои собственные view путем наследования непосредственно от класса UIView (или от его потомков). В дополнение включению view и control, приложение может также включать слои Core Animation со своими иерархиями view и control. Объекты Layer на самом деле являются объектами data для отображения визуальных данных. View используют объекты layer в фоне прежде чем отобразить. Вы так же можете добавить свой собственный объект layer для реализации сложной анимации вашего интерфейса или други типов визуальных эффектов. |
Чем отлично одно iOS приложение от другого, это управление данными (и соответствующая бизнес-логика) и то как они представляются пользователю. Большинство взаимодействий объектов UIKit не определяет ваше приложение, но помогает вам усовершенствовать его поведение. Например, методы вашего приложения делегируются когда приложение изменение состояние, что вы определяет в своем коде чтобы отреагировать соответствующим образом.
Основной цикл работы приложения обрабатывает все пользовательские события. Объект UIApplication запускает основной цикл с момента запуска и использует его для обработки события и обработки изменений в интерфейсе. Как следует из названия, основной цикл выполняется в основном потоке приложения. Такое поведение гарантирует, что события связные с пользователем будут обрабатываться последовательно в том порядке в котором они были получены.
На рисунке 2-2 изображена архитектура основного цикла и то как совершается обработка пользовательских событий после их получения в вашем приложении. Когда пользователь взаимодействует с устройством, события, связанные с этим взаимодействием генерируются системой через специальный порт созданный UIKit. События помещаются в очередь внутри приложения и направляются один за другим в основной цикл для исполнения. Объект UIApplication это первый объект который принимает события и принимает решение о том, что с ним должно быть сделано. События касания (touch) обычно направляется на объект главного окна, которое в свою очередь пересылает это сообщение в view, в котором произошло это касание. Другие события могут быть приняты немного другим путями через различные объекты приложения.
Рисунок 2-2 Обработка событий в основном цикле
Много типов событий может поставляться в приложение iOS. Основная их часть отображена в таблице 2-2. Много этих типов событий поставляются используя основной цикл, но некоторые нет. Некоторые события отсылаются непосредственно в объект или отправлены в блокировку что вы предоставите. Для информации о основных типах событий, включая касания, удаленное управление, движение, акселерометр и гироскоп - смотрите в Event Handling Guide for iOS.
Таблица 2-2 Основные типы событий в приложении iOS
|
Тип события |
Поставляется в... |
Замечания |
|
Касание |
Объект view на который было совершено нажатие |
View принимающий объект. Любое событие связанное с касанием не обрабатываются View а отправляются дальше по цепочке для обработки. |
|
Удаленное управление Событие встряхивания |
Первый ответивший объект |
События удаленного управления существуют для управлением проигрыванием медиа и генерируемые наушниками или другими аксессуарами. |
|
Акселерометр Магнитометр Гироскоп |
Объект который вы указали |
События созданные акселерометром, магнитометром и гироскопом отправляются в объект который вы указали. |
|
Локация |
Объект который вы указали |
Вы регистрируете прием событий локации используя фреймворк Core Location. Для большей информации о использовании Core Location, смотрите Location and Maps Programming Guide. |
|
Перерисовать |
View который необходимо перерисовать |
События перерисовки не вызываются объектами событий, но могу быть вызваны самим View для перерисовки самого себя. Архитектура рисования iOS описана в Drawing and Printing Guide for iOS. |
Некоторые события, такие как касание или удаленное управление, могут приниматься ответившими объектами (responder objects). Такие объекты везде в вашем приложении. (Объект UIApplication, ваш view объект, и ваш view controller объекты, все примеры ответивших объектов.) Основными целями событий являются ответившие объекты, но могут быть переданы другими ответившими объектами (через цепочку ответивших (responder chain)) если вам нужно обработать события. Например, view не имеет перехватчика, событие может передать событие через superview или view controller.
Событие касания происходящие в controls (таких как кнопки) и обрабатываются иначе, чем в события касания происходящие на многих других типах view. Как правило, только на ограниченное число взаимодействий возможно с control, и эти события упакованы в action сообщения и отправляются в соответсвующий целевой объект. Этот паттерн, называемый target-action упрощает использование control-ов для включение вашего кода в приложении.
В любой момент, ваше приложение находиться в одном из стадий указаных в таблице 2-3 Система перемещает приложение от стадии к стадии в соответствии с тем, что происходит в системе. Например, когда пользователь нажал на кнопку Home, телефон принимает выходов, или любое другое прерывание из ряда, запущенное приложение может изменить состояние в ответ. На рисунке 2-3 показаны пути которые приложение принимает при переходите от стадии к стадии.
Таблица 2-3 Стадии приложения
|
Стадия |
Описание |
|
Not running |
Приложение не было запущено или было выключено системой. |
|
Inactive |
Приложение запущено, но сейчас не принимает события. (Это может произойти из за запуска другого кода.) Приложение обычной остается в этом состоянии только только на короткий промежуток времени, для перехода между состояниями. |
|
Active |
Приложение запущено и принимает события. Это нормальный режим для запущенных приложений. |
|
Background |
Приложение находиться фоне и обрабатывает код. Большинство приложений попав в это состояние ненадолго прежде чем попасть в состояние suspended. Тем не менее, приложение, которое запрашивает дополнительное время, может оставаться в течении определенного периода времени в этом состоянии. Кроме того, приложение запущенное непосредственно из этого состояния переходит в состояние inactive. Для информации о том как работает код в этом состоянии читайте Background Execution. |
|
Suspended |
Приложение в фоне, но не выполняющее код. Система перемещает приложение в это состояние автоматически и не оповещает его об этом. Когда приложение в suspended, приложение остается в памяти, но не выполняет какой либо код. Когда возникают события с нехваткой памяти, система может удалить приложения не оповестив его, для того чтобы освободить больше места для открываемых приложений. |
Рисунок 2-3 Изменение состояний приложения iOS
Основные перемещения между состояния вызывают определенные методы указанные в объекте приложения. Эти методы дают вам возможность обработать изменение состояний. Эти методы описаны ниже вместе с тем как вы можете использовать их
application:willFinishLaunchingWithOptions:— Этот метод вашего приложения позволяет вам выполнять код во время запуска приложения.
application:didFinishLaunchingWithOptions: — Этот метод позволяет вам перед окончанием запуска выполнить код прежде чем показать ваше приложение пользователю.
applicationDidBecomeActive: — Дает вам знать, что оно становятся foreground приложением. Используйте этот метод для последних приготовлений.
applicationWillResignActive: — Дает вам значит, что приложение уходит из состояния foreground. Используйте этот метод для помещения вашего приложения в режим покоя.
applicationDidEnterBackground: — Дает вам знать, что приложение запущено в фоне и может быть выключено в любое время.
applicationWillEnterForeground: — Дает вам знать, что ваше приложение перемещено из фона обратно в foreground, но то, что оно еще не активно.
applicationWillTerminate: — Дает вам знать, что приложение было выключено. Этот метод не вызывается, если приложение было в состоянии suspended.
Приложение должно быть подготовлено к выключение в любое время и не должно ждать сохранения пользовательских данных или подготовкой других критических задач. Выключение инициализируемое системой это нормальная часть жизненного цикла приложения. Система обычно выключает приложения, для очищения памяти и подготовки к запуску других приложений, которые запущены пользователем, но система так же может выключить приложения , которые некорректно или не отвечающим на события своевременно.
Suspended приложения не получают уведомления о завершении. Система убивает процесс и восстанавливает соотвествующую память. Если приложение запущено в фоне и не отвечает, система вызовет applicationWillTerminate: чтобы приложение подготовилось к выключаению. Система не вызывает метод когда устройство перезагружается.
В дополнение, система выключает выше приложение, когда пользователь выключил его с помощью интерфейса мультизадачности. Выключение вызванное пользователем вызывает такой же эффект как при выключении suspended приложений. Процевв приложения удаляется без предупреждения отправляемый приложению.
Система создает приложение в основном потоке и вы можете создавать отдельные потоки, если вам это необходимо, для решения каких либо задач. Для приложений iOS, предпочтительным методом является использование Grand Central Dispatch (GCD), оперирущим с объектами, и другиим интерфейсами асинхронного программирования не создавая и управляя потоками собственоручно. Такие технологии как GCD позволяют определить работу, которую вы хотите сделать и в каком порядке вы хотите ее сделать, но пусть система решает как лучше выполнить эту работу для CPU. Когда система управляет вашими потоками вам становиться легче писать кот, обспечивается большая корректность кода, а так же увеличивает общую производительность.
Когда мы думает о потоках и парралельной работе, необхоимо учитывать следующее:
Работа с включающими view, Core Animation, и многими другими классами UIKit которые обычно встречаются в основном потоке приложения. Есть некоторые исключения из этих правилах, например манипулции связанные с изображениями могут встретится в фоновых потоках, но предположим, что работа должна выполняться в основном потоке.
Долго выполняющиеся задачи (или потенциально долгие задачи) должны всегда выполняться в фоновых потоках. Любые задачи связанные с доступом в интернет, к файловой системе или большие обработки данных, все должны выполняться асинхронно с использованием GCD или оперирующими объектами.
Во время запуска, перемещайте задачи из основного потока, когда это возможно. Во время запуска, ваше приложение должно использовать имеющееся время для запуска пользовательского интерфейса на столько быстро на сколько это возможно. Только задачи, которые вносят вклад в создание пользовательского интерфейса должны быть выполнены в основном потоке. Все другие задачи должны выполняться асинхронно, отправляющие результат работы когда он будет готов.
Для больше информации о использовании GCD и оперирующим объектам, обратитесь к Concurrency Programming Guide.
]]>Статьи подобного рода довольно таки распространены, но мет
]]>Статьи подобного рода довольно таки распространены, но методика зачастую отличается. В данном посте я расскажу как изготовить плату в домашних условиях с нанесением маски. Фоторезист, маску и вещество для смывания фоторезиста я приобретал на ebay из Китая.
Очевидно, что для начала работы нам понадобится печатная плата. Моя была реализована в Eagle.

Это схема радиоприемника, плату которой меня попросили реализовать. Чтож, выпиливаем нужный участок текстолита, наждачим и обрабатываем ацетоном. После чего берем наш фоторезист. Как уже говорилось он заказан с Китая, в продаже в России я его еще не видел.

Берем плотную губку или паралон и начинаем наносить фоторезист тонким слоем. После того как результат вас удовлетворит, стоит засушить плату паяльным феном или просто положить плату на часок в темное место. Быстрее все же это сделать феном. Дождемся, пока остынет плата и нанесем второй слой. Повторим процесс сушки. В результате вы получите что то вроде этого:

Далее следует распечатать трафарет

Как видите, трафарет печатается на пленке в негативе. Пленка продается в специализированных магазинах печати наряду с обычными листами А4, А3, А2 и пр.
Поместим трафарет на плату и накроем ее стеклом, после чего следует освещать плату в течении 10 минут под ультрафиолетовой лампой

Смывается лишний фоторезист в растворе воды с парочкой кристалликов спец. штуковины с ebay. По моему мнению, это скорее всего щелочь. Раствор получается довольно таки маслянистым.

Очищаем плату кисточкой, попутно мокая в раствор. У меня вышло не очень то хорошо, судя по всему из за плохого трафарета или неравномерно нанесенного фоторезиста. Так или иначе аккуратно закрашиваем поврежденные участки перманентным маркером и помещаем все это дело в раствор хлорного железа

Через 20-40 минут, в зависимости от концентрации раствора, плата будет готова

Получившуюся плату кидаем в "очищающий" раствор, который мы использовали ранее. Для того чтобы "засвеченый" фоторезист сошел, нужно довольно много времени. Порядка 20 минут. Советую подкинуть в раствор еще пару кристалликов или же налить туда какое нить чистящее средство с щелочью, например, средство для чистки труб "Крот". Ускорить процесс можно также натирая плату грубой кисточкой или зубной щеткой. Далее наждачим, обезжириваем ацетоном, мажем флюсом и лудим.



Теперь настало время маски. Наносим по такой же методике как и фоторезист. Сушится маска еще дольше, так что необходимость в использовании фена возрастает. Наносим два слоя, для верности, и хорошенько сушим. Печатаем трафарет, и помещаем под ультрафиолет на час. Лишняя маска легко стирается ацетоном.


И результат:

Modbus это протокол передачи данных по типу клиент-сервер. обесп
]]>
Modbus это протокол передачи данных по типу клиент-сервер. обеспечивающий передачу данных между устройствами подключенным к различным шинам или сетям. Разработан был в далеком далеком 1979 году и с тех пор обрел неплохую популярность среди миллионов автоматических устройств. Поддержка протокола происходит и по сей день.
Как и свойственно клиент-серверному общению, коммуникации происходят по правилу запрос-ответ, сообщения снабжаются функциональными кодами, которые и говорят о его типе. Передача может происходить по нескольким типам сетей:
Структура пакета
Протокол Modbus описывает простейший типа пакета PDU (Protocol data unit), который лежит в основе всех пакетов Modbus. В зависимости от типа передачи, пакет может дополняться дополнительными полями уже на уровне ADU (Application data unit).

Клиент создает ADU пакет и инициирует передачу, после получения пакета сервер разбирает пакет и по функциональному коду уже решает, что делать с данными. Функциональные коды могут быть в пределах от 1 до 255, где с 128 по 255 коды зарезервированные под ошибки. Поле данных же содержит дополнительную информацию для сервера которая используется в соответствии с функциональным кодом. Это может быть адрес регистра, количество пунктов которые будут обработаны или количество байт в поле. В каких то типах вопросов поле данных может отсутствовать вообще (нулевой длинны).

Если сервер успешно принял пакет и обработал данные, то он возвращает ответ в той форме, в которой этого требует действие декларированное функциональным кодом. Например, если клиент хочет изменить какой либо регистр сервера, то он отправляет соответствующий функциональный код и необходимые и если сервер успешно обработал запрос, то он возвращает точно такой же пакет, который и получил. Но если что то пошло не так, то сервер генерит ошибку, состоящая из функционального кода ошибки и самого кода ошибки:

Размер PDU ограничен по размеру. Для последовательной линии передачи данных это = 256 байт - Адрес сервера (1 байт) - CRC (2 байта). Получается, что размер пакета ограничен 253 байтами. Следовательно:
Протокол Modbus использует big-endian порядок байтов для адреса и данных. Это значит, что длинное число отправленное в пакете начинается с левого байта к правому. Например, значение 0x1234 разобьется на два байта и будет отсылаться в порядке, сначала 0x12, а затем 0x34.
Modbus основан на модели данных из серии таблиц, которые имеют различные характеристики

Адресация элементов таблицы задается 16-значным адресом начиная с 0, что позволяет каждой таблице содержать до 2^16 = 65536 элементов. Очевидно предположить, что все данные обрабатываемые с помощью Modbus (биты и регистры) должны быть расположены в памяти устройства. Но физический адрес в памяти не стоит путать ссылкой на данные. Единственным требованием является описание указанных адресов с физическим адресом в устройстве.
Такое представление типов данных можно сравнить с привычными нами таблицами регистров. Различием является только лишь то, что мы сами придумываем таблицу регистров для нашего сервера.

Обработка пакета PDU на сервере должна выглядеть примерно так:

Возврат ошибки от сервера должен выглядеть как PDU пакет, где в функциональным кодом является код ошибки + 0x80. А код исключения должен описывать причину ошибки.
Распределение функциональных кодов

Все функциональные код протокола Modbus можно разделить на три категории:
Публичные функциональные коды:
Четко определенные функциональные коды, подтвержденные сообществом Modbus.org, публично задокументированы и имеют в своем распоряжении тест соответствия, включают в себя как указанные функциональные коды так и не указанные, зарезервированные для использования в будущем.
Функциональные коды указанные пользователем:
Существует два диапазона таких кодов. от 65 до 72 и от 100 до 110. Пользователь волен выбирать свои действия которые сервер будет совершать получив пакет с данным кодом.
Зарезервированные функциональные коды:
Эти коды в настоящее время используются некоторыми компаниями для устаревших продуктов и не доступны для публичного использования.

Описание функциональных кодов
0x01 - Чтение регистров флагов
Этот код используется для чтения от 1 до 2000 регистров флагов по очереди. Пакеты такого запроса будут выглядеть следующим образом:
Функциональный код: 1 байт - 0x01
Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF
Количество флагов: 2 байта - от 1 до 2000 (0x7D0)
Ответ же будет таким
Функциональный код: 1 байт - 0x01
Количество флагов: 1 байт - n штук
Флаги: n байт
В случае ошибки ответ будет таким
Функциональный код: 1 байт - функциональный код+0x80
Код ошибки: 1 байт - или 01 или 02 или 03 или 04

По данной картинке можно понять какой код будет говорить о какой ошибке.
0x02 - Чтение дискретных входов
Эта функция работает аналогично 0x01
0x03 - Чтение регистров хранения
Здесь нам дозволено читать до 125 значений. Структура запроса и ответа аналогична 0x01 за тем лишь исключением, что принимать мы будем не биты а по 2 байта
0x04 - Чтение входных регистров
Аналогично 0x03
0x05 - Запись регистра флага
Здесь мы можем изменить значение регистра флага. С нуля на единичку или наоборот. Для единички используется 0xFF00, а для ноля 0x0000
Функциональный код: 1 байт - 0x01 Адрес флага: 2 байта - от 0x0000 до 0xFFFF Состояние флага: 2 байта - либо 0x0000, либо 0xFF00
В случае успеха, мы получим точно такое сообщение, в случае провала код ошибки 0x85. Алгоритм работы будет нескольки отличаться:

0x06 - Запись одного регистра
Записывает данные в один регистр.
Функциональный код: 1 байт - 0x08 Адрес регистра: 2 байта - от 0x0000 до 0xFFFF Состояние регистра: 2 байта - от 0x0000 до 0xFFFF
Блок-схема работы аналогична 0x05
0x07 - Чтение сигналов состояния
Запрос состоит из одного функционального кода, а ответом служит тот же функциональный код с одним байтом ответа. Схема работы такова:

0x08 - Диагностика (Только для последовательно линии)
Созданная для того, чтобы проверить связь между клиентом и сервером. Пример пакета:
Функциональный код: 1 байт - 0x08 Под-функция: 2 байта Данные : N x 2 байта
В качестве Под-функции указывается тип диагностики:
Блок-схема работы:

0x0B - Вернуть кол-во событий
Используется чтобы получить с сервера два байта статуса и кол-во событий. Пример:
Функциональный код: 1 байт - 0x0B
Ответ:
Функциональный код: 1 байт - 0x0B Статус: 2 байта - от 0x0000 до 0xFFFF Код-во событий: 2 байта - от 0x0000 до 0xFFFF
2 байта статуса означают, что если предыдущая команда все еще обрабатывается на удаленном устройстве, то ответ будет 0xFFFF, иначе 0x0000
0x0C - Вернуть лог событий
Возвращает лог событий с момента запуска сервера. Нормальный ответ будет содержать функциональный код, один байт содержащий длину следующего сообщения, два байта статуса, два байта с количеством событий, два байта с количеством сообщений и события длинной в один байт. Более подробное описание этой функции можно посмотреть в документации с сайта modbus.org.
0x0F - Записать несколько регистров флагов
Имеет такую структуру:
Функциональный код: 1 байт - 0x0F Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF Количество байт для изменения: 2 байта - от 0x0000 до 0xFFFF Кол-во байт с данными: 1 байт - N Данные для записи: N байт
Ответ:
Функциональный код: 1 байт - 0x0F Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF Количество байт для изменения: 2 байта - от 0x0000 до 0xFFFF
Например, возьмем такой пакет:
Функциональный код: 1 байт - 0x0F Адрес первого флага: 2 байта - 0x0013 Количество байт для изменения: 2 байта - 0x000A Кол-во байт с данными: 1 байт - 0x02 Данные для записи: 1 байт - 0xCD Данные для записи: 1 байт - 0x01
Разложив данные для записи в бинарное число, мы получим, что 0xCD01 = 1100 1101 0000 0001. Количество байт под изменение 0x000A - 10 штук, начиная с адреса 0x0013. Изменение в этом случае будет происходить сначала в первом байте, а затем во втором. Тем самым получим такие флаги:
0xCD:
0x01:
Алгоритм работы похожий как и с последовательной записью
0x10 - Запись нескольких регистров
Позволяет записать от 1 до 123 регистров на удаленное устройство.
Функциональный код: 1 байт - 0x10
Адрес первого регистра: 2 байта - от 0x0000 до 0xFFFF
Количество регистров для изменения: 2 байта - от 0x0001 до 0x007B
Кол-во байт с данными: 1 байт - 2*N
Данные для записи: N*2 байт
Ответ:
Функциональный код: 1 байт - 0x10
Адрес первого регистра: 2 байта - от 0x0000 до 0xFFFF
Количество регистров для изменения: 2 байта - от 0x0001 до 0x007B
В результате у нас последовательно будут заполняться регистры по типа того, что было в 0x0F
0x11 - Сообщить ID сервера
Используется для чтения описания типа, настоящего статуса и другой информации с устройства
Функциональный код: 1 байт - 0x11
Ответ:
Функциональный код: 1 байт - 0x11
Количество байт в сообщении: 1 байт
ID сервера: Длинна задается устройством
Индикатор запуска: 1 байт - 0x00 = Выключен, 0xFF = Включен
Прочая информация: Длинна задается устройством
0x14 - Чтение из файла
Файлы организованы в виде записей. Каждый файл может содержать до 10000 записей, адресованных от 0000 до 9999 или от 0x0000 до 0x270F.
Функциональный код: 1 байт - 0x14
Количество байт: 1 байт - от 0x07 до 0xF5
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Номер файла: 2 байта - от 0x0001 до 0xFFFF
Подзапрос x - Номер записи: 2 байта - от 0x0000 до 0x270F
Подзапрос x - Длинна записи: 2 байта - N
Подзапрос x+1 - ... и т.д.
Ответ:
Функциональный код: 1 байт - 0x14
Длинна ответа: 1 байт - от 0x07 до 0xF5
Подзапрос x - Длинна ответа: 1 байт - от 0x07 до 0xF5
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Данные: N x 2 байт
Подзапрос x+1 - ... и т.д.
0x15 - Запись в файл
Такие же правила как и в 0x14 только происходит запись
Функциональный код: 1 байт - 0x15
Количество байт: 1 байт - от 0x09 до 0xFB
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Номер файла: 2 байта - от 0x0001 до 0xFFFF
Подзапрос x - Номер записи: 2 байта - от 0x0000 до 0x270F
Подзапрос x - Длинна записи: 2 байта - N
Подзапрос x - Данные: N x 2 байт
Подзапрос x+1 - ... и т.д.
Ответ:
Функциональный код: 1 байт - 0x15
Длинна ответа: 1 байт - от 0x09 до 0xFB
Подзапрос x - Тип: 1 байт - 06
Подзапрос x - Данные: N x 2 байт
Подзапрос x - Номер файла: 2 байта - от 0x0001 до 0xFFFF
Подзапрос x - Номер записи: 2 байта - от 0x0000 до 0x270F
Подзапрос x - Длинна записи: 2 байта - N
Подзапрос x - Данные: N x 2 байт
Подзапрос x+1 - ... и т.д.
0x16 - Запись в регистр по маске
Помогает изменить регистр с использованием AND-маски и OR-маски. Результат будет примерно таким
(Исходное значение AND and-Маска) OR (or-Маска AND (NOT and-маска))
Например:
Исходное значение - 0001 0010
and-Маска - 1111 0010
or-Маска - 0010 0101
NOT and-Маска - 0000 1101
Результат - 0001 0111
Структура пакета:
Функциональный код: 1 байт - 0x16
Адресс регистра: 2 байт - от 0x0001 до 0xFFFF
And-Маска: 2 байт - от 0x0001 до 0xFFFF
Or-Маска: 2 байт - от 0x0001 до 0xFFFF
В качестве ответа пакет будет полностью дублирован
0x17 - Чтение/Запись нескольких регистров
Функция комбинирует одну операцию чтения и одну операцию записи в одной транзакции.
Функциональный код: 1 байт - 0x17
Адресс регистра для чтения: 2 байт - от 0x0001 до 0xFFFF
Количество регистров для чтения: 2 байт - от 0x0001 до 0x007D
Адресс регистра для записи: 2 байт - от 0x0001 до 0xFFFF
Количество регистров для записи: 2 байт - от 0x0001 до 0x007D
Количество байт: 1 байт - 2 x N
Регистры для записи: N x 2 байт
Ответ:
Функциональный код: 1 байт - 0x17
Количество байт: 1 байт - 2 x N
Байты для чтения: N x 2 байт
0x18 - Чтение данных из очереди
Функция позволяет читать очередь FIFO (первый вошел - первый вышел) из регистра на удаленном устройстве.
Функциональный код: 1 байт - 0x18
Адресс очереди: 2 байт - от 0x0001 до 0xFFFF
Ответ:
Функциональный код: 1 байт - 0x18
Количество байт: 2 байт
Количество элементов очереди: 2 байта
Ну и в заключение описание кодов ошибок, которое я тупо скопировал с википедии:
Почти год назад я описывал процесс сборки минимального наб
]]>Почти год назад я описывал процесс сборки минимального набора программ для сборки прошивок под STM32. Пришло время рассказать, как же можно реализовать работу с периферией на примере USART.
Если посмотреть в даташит, например на RM0038, то можно заметить что USART там далеко не один. Наряду с тем работать он может в куче режимов, но в пределах данного поста нас будет интересовать старый добрый USART. Прежде чем начать подготовку, сначала необходимо определиться с тактированием. Перед тем как попасть на нашу периферию, частота несколько раз умножается и делится. Рассмотрим этот процесс на основе того микропроцессора, что стоит на STM32L-Discovery (STM32L152RB)
На первоночальной блок-схеме можно выделить следующие пути

Как видно, на схеме все порты ввода/вывода находятся на AHB, а что касательно USART, так USART1 лежит на APB2, а остальные на APB1. Теперь попробуем выяснить какая именно частота попадает на USART-ы. Эмпирическим путем я просмотрел дампы памяти и получил такую схему

На моем STM32L-Discovery в качестве внешнего кварца был впаян кварц на 8МГц, дальше он умножается в 12 раз, что делает частоту в 96Мгц. Для нормальной работы USB, если он будет нужен, на этом этапе частота должна быть равна 48Мгц, так что хорошо бы поменять PLLMUL с x12 на x6. Но пока USB не нужен, можно оставить все не тронутым. Дальше частота делится на 3 и получается 32Мгц. _Прошу обратить внимание, если вы будете изменять параметры PLL, то на это время его необходимо отключить. _

Далее SYSCLK проходит через 3 делителя. Но все они равны 1, так что в финале на все подается 32Мгц.
Теперь можно подойти к самой настройке USART. Начнем с подсчета регистра BRR. Именно он у нас будет отвечать за скорость передачи. В даташите можете найти подобные формулы

Пользоваться будем верхней формулой и возьмем OVER8 за 0. Частота у нас 32Мгц, скорость 9600:(32 000 000 / 9 600) / 16 = 208.(3)
Выделяем целую часть: 208 = 0xD0
Дробную же часть необходимо умножить на 16 и округлить до целого 16*0.(3) = 5.(3) ~ 5 = 0x5
В результате получаем, что USART_BRR = 0xD05
Теперь, когда все что необходимо подсчитано, можно написать код. Сначала нужно подвести тактирование к модулю USART1:
RCC->APB2ENR|= RCC_APB2ENR_USART1EN;
Теперь настраиваем сам USART. По сравнению с STM8L кода тут как то поменьше
USART1->BRR = 0xD05;
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // USART1 ON, TX ON, RX ON
Но вот с настройкой GPIO совсем все по другому. Для начала подведем тактирование к группе портов A.
RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBLPENR_GPIOALPEN;
А теперь еще одна очень важная вещь. Нужно указать в каком режиме какая нога должна работать. За это отвечают регистры AFRL и AFRH

AFRL отвечает за порты от 0 до 7, а AFRH - от 8 до 15. Наш USART1 находится на портах PA9 и PA10, следовательно нам нужен AFRH. Чтобы понять какую цифру вписывать в регистр, нужно посмотреть чуть выше по документации:

Как видно, по нижней части картинки USART1..3 находиться под AF7. AF7 - это 0111, следовательно в регистре должно быть что то вроде 0111 0111 0000 = 0x770. Чтож, так и поступим:
GPIOA->AFR[1] |= 0x770; //AF7(USART1..3) to pins 9,10
Порт вывода работает в альтернативном режиме push-pull без подтяжек и на максимальной скорости:
GPIOA->OTYPER &= ~GPIO_OTYPER_ODR_9; // Output push-pull (reset state)
GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR9; // No pull-up, pull-down
GPIOA->MODER |= GPIO_MODER_MODER9_1; // Alternate function mode
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9_1; // 40 MHz High speed
И порт входа:
GPIOA->MODER |= GPIO_MODER_MODER10_1; // Alternate function mode
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10_1; // 40 MHz High speed
Теперь можно попробовать запустить USART. Для сего можно написать такой код (Первый цикл нужен для того чтобы дождаться готовности передачи):
while(!(USART1->SR & USART_SR_TC));
USART1->DR = 0xFF;
Для полноценного USART-а нам необходимо реализовать хотябы прерывание по приему. В STM32 для USART1 есть только одно прерывание. Нужно лишь переопределить функцию USART1_IRQHandler и вписать туда свой код:
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
while(!(USART1->SR & USART_SR_TC));
USART1->DR = USART1->DR;
}
}
Если регистр говорит, что была принята какая либо информация, то ждем готовности передачи и возращаем то, что приняли.
Теперь остается только лишь резрешить прерывания:
USART1->CR1 |= USART_CR1_RXNEIE; // RXNE Int ON
NVIC_EnableIRQ (USART1_IRQn);
__enable_irq ();
Вот и все готово. Результат:
#include "stm32l1xx.h"
int main(void)
{
RCC->CR |= RCC_CR_HSEON;
while(!(RCC->CR & RCC_CR_HSERDY));
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; //USART1 Clock ON
USART1->BRR = 0xD05; // Bodrate for 9600 on 32Mhz
USART1->CR1 |= USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; // USART1 ON, TX ON, RX ON
RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBLPENR_GPIOALPEN;
GPIOA->AFR[1] |= 0x770; //AF7(USART1..3) to pins 9,10
GPIOA->OTYPER &= ~GPIO_OTYPER_ODR_9; // Output push-pull (reset state)
GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR9; // No pull-up, pull-down
GPIOA->MODER |= GPIO_MODER_MODER9_1; // Alternate function mode
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9_1; // 40 MHz High speed
GPIOA->MODER |= GPIO_MODER_MODER10_1; // Alternate function mode
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10_1; // 40 MHz High speed
USART1->CR1 |= USART_CR1_RXNEIE; // RXNE Int ON
NVIC_EnableIRQ (USART1_IRQn);
__enable_irq ();
while (1) {
}
}
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
while(!(USART1->SR & USART_SR_TC));
USART1->DR = USART1->DR;
}
}
Так же его можно взять в качестве Gist на моем гитхабе: https://gist.github.com/ftp27/10454616
]]>