<![CDATA[ftp27]]>https://ftp27.dev/https://ftp27.dev/favicon.pngftp27https://ftp27.dev/Ghost 6.18Mon, 16 Mar 2026 17:49:52 GMT60<![CDATA[Почему может не работать DateFormatter]]>https://ftp27.dev/date-formatter-issues/63e49f060a40f300018793b7Mon, 14 Nov 2022 16:27:57 GMT

Когда мы интегрируем наше 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. Так что если вы используете этот формат, то можно избавится от пары строк кода.

]]>
<![CDATA[Генерация превью ссылок]]>https://ftp27.dev/link-preview-generation/63e49f060a40f300018793b6Sat, 12 Nov 2022 10:34:09 GMT

Когда речь заходит о создании превью, то первым делом на ум приходит 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.

Частные случаи

Twitter

Для того чтобы получить какие либо дополнительные данные из твиттера нам необходимо использовать их 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

Для 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 канала

Теперь нам доступно не только название канала, но и еще ссылка на его картинку. Документация.

Instagram

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

В случае же с id юзера все проще. Есть достаточно популярный запрос к  приватному API - https://www.instagram.com/<username>?__a=1. Он выдаст нам как минимум ссылку на аватар профиля и его имя.

Следующие шаги

К сожалению, нам не всегда доступны какие либо API, а мета данные не до конца удовлетворяют нашим запросам. В этом случае приходится прибегать к менее надежным вариантам. А именно к парсингу самого html. Здесь нет универсальных путей и потребуется переодически мониторить правильно ли мы достали эти данные. Но с правильно настроеной системой сбора аналитики и сбоев даже такой подход не будет является большой проблемой.

]]>
<![CDATA[Мониторинг доступности сервера с мобильного приложения]]>https://ftp27.dev/mobile-api-analytics/63e49f060a40f300018793b4Mon, 07 Nov 2022 15:50:52 GMT

Во многих проектах, где существует интеграция с собственными сервисами, встает вопрос о сборе аналитики доступности этого сервиса. Это можно реализовать несколькими путями. Наиболее доступным и быстром среди них будет использовать Firebase с его Performance Monitoring. Подключив модуль в проект ваши запроса станут автоматически трекаться, плюс появится возможность добавить свои события время затраты на которые вам необходимо затрекать. Так что если вашей целью является просто мониторить задержку с различных регионов, то можно закрывать эту статью и идти прямиком в Firebase. Однако, если у вас есть необходимость получать какую то дополнительную информацию, например, чтобы трекать падения парсера с клиентской стороны или мониторинг ответов от сервера, то вполне возможно, что вариант описанный в этом статье, вам подойдет.

InfluxDB

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. Все части, кроме тегов, являются обязательными.

К тегам предлагаю отнести такие значения: названия модуля; откуда происходил запрос; хост; путь; локация пользователя (если она доступна) и пр.

Поле значений может быть нескольких типов:

  • Float - числа в формате float - IEEE-754
  • Integer - целочисленное значение, в конце советуют добавить i , но это не обзательно. Например, 12485903i
  • UInteger - беззнаковое целочисленное значение, пишем в конце u аналогично с обычным integer-ом
  • String - строка ограниченая размером в 64Кб. следует указывать ее в кавычках
  • Boolean - обычный булеан, возможные значения: 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 . Где

  • host - адрес облака
  • bucketId - идентифкатор бакета
  • precision - размерность в которой мы отправили время

На данном этапе было бы нецелесообразно отправлять метрики каждый раз, как совершился запрос, но более эффективно было бы собрать их вместе и отправлять раз в определенный промежуток времени, например раз в пять секунд. Поэтому, предлагаю расширить функицонал класса и добавить 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 можно закешировать заранее, чтобы ускорить доступ клиентов к данным.

]]>
<![CDATA[Пример настройки Tuist на существующем проекте]]>Что такое Tuist

Tuist это инструмент для генерации проектов XCode. Это поз

]]>
https://ftp27.dev/tuist-migration-guide/63e49f060a40f300018793b3Fri, 28 Oct 2022 14:12:12 GMTЧто такое TuistПример настройки Tuist на существующем проекте

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.

Разберем текущий инциализатор по пунктам

  • name - Название проекта, это будет название таргета
  • platform - платформа соответсвенно
  • product - тип таргета, фреймворк, приложение или другое.
  • bundleId - наш идентификатор бандла
  • deploymentTarget - минимальная версия SDK
  • infoPlist - адрес нашего plist (не обязателен, если он почти пустой)
  • sources - адреса исходников. Полезно так как можно соединить код с разных проектов
  • resources -  адреса ресурсов
  • dependencies - зависимости. Могут быть другие таргеты или внешние зависимости указанные в файле Dependencies.swift
  • settings - конфигурации проектов которые мы предварительно экспортировали

Так как это 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
]]>
<![CDATA[Показываем данные с MQTT на Grafana]]>https://ftp27.dev/grafana-with-mqtt/63e49f060a40f300018793b2Thu, 12 Dec 2019 23:16:45 GMT

После того как мы завели наш датчик и начали делиться его показаниями с 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 и наслаждаться плодами трудов.

]]>
<![CDATA[Отправляем данные с MQTT брокера на HomeKit]]>https://ftp27.dev/homekit-with-mqtt/63e49f060a40f300018793b1Wed, 11 Dec 2019 21:54:48 GMT

Данный пост основан на результатах работы предыдущего поста где мы отправляли данные 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

В результате получаем что то вроде

Отправляем данные с MQTT брокера на HomeKit
]]>
<![CDATA[BMP085 и MQTT]]>https://ftp27.dev/bmp085-and-mqtt/63e49f060a40f300018793b0Tue, 10 Dec 2019 23:14:26 GMT

Сегодня я хочу рассказать как можно настроить передачу данных с цифрового датчика давления 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-модулю

BMP085 и MQTT

В качестве протокола общения с 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 клиенте можно наблюдать данные с модуля

BMP085 и MQTT
]]>
<![CDATA[Прогресс бар или спиннер: Что и когда использовать]]>https://ftp27.dev/progress-bars-vs-spinners/63e49f060a40f300018793afTue, 10 Jan 2017 10:07:58 GMTПрогресс бар или спиннер: Что и когда использовать

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

Спиннеры не для длительных процессов

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

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

Прогресс бар или спиннер: Что и когда использовать

Правило 4 секунд

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

Когда показывать спиннер

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

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

Прогресс бар или спиннер: Что и когда использовать

Прогресс бар может сделать длительный процесс допустимым

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

Прогресс бар или спиннер: Что и когда использовать

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

Как показывать прогресс бар

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

Прогресс бар или спиннер: Что и когда использовать

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

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

Не переборщите со спиннерами

Многие дизайнеры имеют привычку использовать спиннеры на всех процессах, но когда вы используете спиннеры для долгих процессов, вы разочаровываете пользователя. Чтобы избежать этого, используйте прогресс бары.

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

Оригинал

]]>
<![CDATA[Почему всегда нужно указывать маску на типизированные поля]]>https://ftp27.dev/why-formatted-data-fields-always-need-input-masks/63e49f060a40f300018793aeMon, 21 Nov 2016 14:27:06 GMTПочему всегда нужно указывать маску на типизированные поля

В мире форм существует два типа данных основанных на текстовых полях. Это поля без формата (только текст) и поля имеющие формат (текст и символы). Вы должны четко разделять эти типы.

Многие дизайнеры возлагают на пользователя форматирование данных. Этот вызывает серьезные проблемы при заполнении форм.

Неизвестный формат ведет к отказу

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

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

Почему всегда нужно указывать маску на типизированные поля

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

Эта неопределенность вызовет лишнюю умственную работу для пользователя, а так же заставляет задуматься, пройдут ли форматированные данные валидацию или нет.

Отсутсвие формата затрудняет исправление ошибок

Существуют пользователи, которые предпочитают не добавлять какое либо форматирование при вводе данных, что в последующем затрудняет поиск ошибок и их исправление.

Почему всегда нужно указывать маску на типизированные поля

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

Форматируйте свои поля с масками

Лучшим способом для улучшения удобства заполнения формы является использования масок. Маски будут автоматически корректировать ввод данных по мере заполнения данных пользователем. Они не должны вводить какие либо символы.

Почему всегда нужно указывать маску на типизированные поля

Больше нет необходимости волноваться о том какой формат использовать и видя данные в форматированном виде пользователю будет проще найти и исправить ошибку.

Вы должны применять маски в тех полях, которые имеют особый формат. Например, телефонные номера, даты, время, серийные номера или номера кредитных карт.

Большенство текстовых полей требуют только ввод символов. Но не стоит игнорировать форматирование полей там где это необходимо. Возьмите на себя ответственность за форматирование введенных данных.

Оригинал - Why Formatted Data Fields Always Need Input Masks

]]>
<![CDATA[Реализация кастомного UIControl компонента]]>Очень часто приходится сталкиваться с различными нестанд

]]>
https://ftp27.dev/custom-uicontrol/63e49f060a40f300018793adMon, 21 Nov 2016 12:12:20 GMT

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

Разберем задачу на примере радио-кнопки. В изначальной поставке у нас есть похожий компонент 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: Ссылка

]]>
<![CDATA[Работа в фоне iOS приложения]]>

Данная статья является вольным переводом из официальной д

]]>
https://ftp27.dev/rabota-v-fonie-ios-prilozhieniia/63e49f060a40f300018793acMon, 31 Aug 2015 09:00:00 GMT

Данная статья является вольным переводом из официальной документации. В случае каких то неясностей прошу обратиться к первоисточнику: 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.

Процесс создания объекта конфигурации, который поддерживает фоновые загрузки выглядит следующим образом:

  1. Создание объекта конфигурации используя метод backgroundSessionConfigurationWithIdentifier: объекта NSURLSessionConfiguration.
  2. Выбрать значение sessionSendsLaunchEvents объекта конфигурации в YES.
  3. Если ваше приложение начинает передачу на переднем плане, советуется выбрать значение discretionary в YES.
  4. Настройте любые другие свойства объекта конфигурации в зависимости от обстоятельств.
  5. Используйте объект конфигурации чтобы создать объект NSURLSession.

Единожды сконфигурированный, ваш объект NSURLSession сможет использоваться для задач выгрузки и скачивания в системе в нужное время. Если задачи выполнены пока приложение все еще запущено (или на переднем плане или в фоне), объект сессии будет уведомлен в обычном порядке. Если задачи не завершены и система выключива ваше приложение, система автоматический продолжит управлять задачей в фоне. Если пользователь зщавершил задачу, система завершит люую выполняемую задачу.

Когда все задачи происходящие в фоновой сесси были завершены, система перезапускает выключеное приложение (предпологается, что свойство sessionSendsLaunchEvents установлено в YES и пользователь не завершил работу приложения самостоятельно) и вызывает приложение с помощью метода application:handleEventsForBackgroundURLSession:completionHandler:. (Система может также перезапустить приложение для выполнения задач аутентификкации и других событий, связанные с вниманием вашего приложения.) В вашем приложении реализация этого метода, происходит с созданием нового NSURLSessionConfiguration и NSURLSession объекта с похожей конфигурацией что и ранее. Система переподключает ваши новый объект сессии на предыдущие задачи и оповещает об их статусе в новый объект сессии.

Реализация долго-работающих задач

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

  • Приложения играющие аудио данные пользователю в фоне, такие как приложения музыкальных плееров
  • Приложения записывающие звук пока находяться в фоне
  • Приложения информирующие пользователя о его локации все время, такие как приложения навигации
  • Приложения поддерживающих VoIP
  • Приложения нуждающиеся в загрзки новых данных регулярно
  • Приложения принимающие постоянное обновления от внешних аксессуаров

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

Объявление ваших фоновых задач

Поддержка некоторых типов фоновых работ должна быть объявлена в вашим приложении. В 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 вашего приложения.) Приложение, что играет аудиоданные в фоне должны играть играть звуковой контент, а не тишину.

Обычными примерами приложений играющих в фоне является:

  • Приложения музыкальных плееров
  • Звукозаписывающие приложения
  • Приложения поддерживающих аудио или видио проигрывание через AirPlay
  • Приложения IP-телефонии

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

Вы можете использовать любой из системных аудио фреймоврков для работы со звуком в фоне, и обрабатывать для использования в рамках фреймворка. (Для видео проигрывания через AirPlay, вы можете использовать Media Player  или фреймворк AV Foundation для отображения видео.) Поскольку ваше приложение не может быть приостановлено пока играют медиафайлы, обратная связь работает нормально пока ваше приложение находиться в фоновом режиме. В вашей обратной связи вы должны выполнять работу только два управлением проигрывания. Например, приложение для потокового аудио должно качать данные о музыкальном потоке с сервера и помещать верные куски аудио в воспроизведенеи. Приложение не должны выполнять какие либо посторонние задачи, которые не связаны с воспроизведением.

Потому что более чем одно приложение может поддерживать аудио, система определяет, каким приложениям разрешено играть или записывать аудио в любой момент. Приложение на переднем плане всегда имеет наивысший преоритет для аудио операций. Вполне возможно, что более чем одно фоновое приложение может быть доступно к проиграванию аудио и такие определения основаны на конфигурации каждого объекта аудио сессии приложения. Вы всегда должны конфигурировать ваш объект аудио сессии нашлежащим образом и работать осторожно с фреймворком системы для обработки прерываний и других типов уведомлений связанные с аудио. Для большей информации о конфигурации объекта аудио сессий для работы в фоне читайте в Audio Session Programming Guide.

Реализация приложения с IP-телефонией

Приложение VoIP позволяет пользователю совершать телефонные звоки посредством интернет подключения через сотовую службу устройства. Такое приложение должно постоянно поддерживать интернет соединение с соответсвующей службой так что он может принимать входящие выховы и другие соответствующие данные. Вместо того, чтобы держать VoIP приложение запущеным все время, система позволяет ему быть приостановленным и предоставлять средства для мониторинга сокета для него. Когда взодящий трафик будет определен, система “разбудит” VoIP приложение и вернет ему контроль на сокетом.

Чтобы настроить VoIP приложение, вы должны сделать следующее:

  • Включить поддержку Voice over IP из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение voip в ключ UIBackgroundModes в файле Info.plist вашего приложения.)
  • Настроить один сокет приложения для использования VoIP.
  • До перехова в фоновом режим, вызвать метод setKeepAliveTimeout:handler: для установки обработчика который будет выполняться переодически. Ваше прилжение может использовать его для того чтобы поддерживать связь с сервером.
  • Настроте аудио сессю для обработки передачи к и от активного использования.

Содержание значения 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-уведомление устройству пользователя о том, что новые данные доступны для вашего приложения, вы можете запросить у системы запуск вашего приложения в фоне где вы сможете запустить загрузку новых данных. Этот запуск в фоне минимизирует время между тем когда пользователь увидит push-уведомление и когда ваше приложение начнет отображать соответсвующие данные. Приложения, как правила, просыпаются примерно в тоже время когда пользователь увидел уведомление, но это это все еще дает вам немного времени.

Поддержка этого режима включается из раздела Background modes во вкладке Capabilities вашего проекта Xcode. (Вы также можете включить поддержку вставив значение remote-notification в ключ UIBackgroundModes в файле Info.plist вашего приложения.)

Чтобы включить операцию загрузки для push-уведомлений, уведомление должно содержать значение 1 в ключе content-available. Кгда ключ предосталвен, система “будит” приложение для работы в фоне (или запускает в фон) и вызывает объявленный метод application:didReceiveRemoteNotification:fetchCompletionHandler:. Ваша реализация этого метода должна загружать удаленные данные и интегрировать их в приложение.

При загрузке любых данных, рекомендуется использовать класс NSURLSession для инициализации и управлением вашими загрузками. Для большей информации о том, как использовать класс управления задачами выгрузки и загрузки, смотрите URL Loading System Programming Guide.

Загрузка данных Newsstand в фоне

Приложение 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 и ранее, эти сессии закрываются автоматически если приложение было приостановлено.) Когда новые данные приходят из аксессуара, фремворк открывает ваше приложение чтобы оно обработало полученые данные. Система так же может “пробудить” ваше приложение для обработки подключения и отключения аксессуара.

Любое приложение, поддерживающее фоновую обработку обновлений аксессуара должны следовать нескольким основным указаниям:

  • Приложение должно предосталять интерфейс, позволяющий пользователям запустить и остановить получение данных с устройства. Интерфейс должен открывать и закрывать сессию с аксессуаром.
  • После пробуждения, у приложения есть порядка 10 секунд, чтобы обработать данные. В идеале оно должно обрабатывать данные как можно быстрее и позволить приостановить себя снова. Тем не менее, если нужно больше времени, приложение может использовать метод beginBackgroundTaskWithExpirationHandler: запрашивающий дополнительное время, делать это надо только в самых необходимых случаях.
Общение с Bluetooth акусессуарами

Приложения которые работают с 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 аксессуара должны следовать нескольким основным указаниям:

  • Приложение должно предосталять интерфейс, позволяющий пользователям запустить и остановить получение данных с Bluetooth устройства. Интерфейс должен открывать и закрывать сессию с аксессуаром.
  • После пробуждения, у приложения есть порядка 10 секунд, чтобы обработать данные. В идеале оно должно обрабатывать данные как можно быстрее и позволить приостановить себя снова. Тем не менее, если нужно больше времени, приложение может использовать метод beginBackgroundTaskWithExpirationHandler: запрашивающий дополнительное время, делать это надо только в самых необходимых случаях.
Привелечение внимания пользователя из фонового режима

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

Расписание может отправлять локальные уведомления, создав объект класса 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.

Когда ваше приложение будет запущено фоне

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

Для навигационных приложений:

  • Система приняла новые данные о локации, встречается когда приложение соответсвующим образом
  • Усройстов вошло или вышло из зарегестрированного региона. (Регионы могут быть географическими регионами или регионами iBeacon)
  • Для аудиоприложений, фреймворк аудио нуждается в обработке некоторых данных приложением. (Аудиоприложение содержит то, что играет аудио или использует микрофон)

Для Bluetooth приложений:

  • Приложения играющие роль центрального приемника данных с подключенной переферии.
  • Приложения игращего роль переферии и получающего команды с подключенного центрального устройства.

Для приложений качающих данные в фоне:

  • Push-уведомления прибывшие приложения и данные уведомления содержат ключ content-available со значением 1.
  • Система “пробуждает” приложение в моменты когда начинается загрузка новых данных.
  • Для приложений качающих данные в фоне с использованием класса NSURLSession, все задачи связанные с этим объектом сессии либо при успешном завершении, либо при ошибке
  • Загрузка запущеная Newsstand приложением.

В большинстве случаев, система не перезапускает приложение выключеное пользователем. Одним исключением является приложения навигации, которые в iOS 8 и позже перезапускают приложение после выключения пользователем. В други случаях, пользователь должен запустить приложение или перезапустить устройство для того чтобы приложение могло запуститься автоматически в фон системой.

Будте ответственны с фоновыми приложениями.

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

  • Не делайте какие либо OpenGL ES запросы из вашего кода. Нельзя создавать объект EAGLContext или вызывать команды рисования OpenGL ES в любом вифе работы в фоне. Использование этих вызывов приведет к выключению вашего приложения немедленно. Приложение должно также обеспечить, чтобы все ранее представлвеные команды завершились до перехода в фоновой режим. Для получения большей информации, смотрите Implementing a Multitasking-aware OpenGL ES Application в OpenGL ES Programming Guide for iOS.
  • Отменяйте любые Bonjour-сервисы перед приостановкой. Когда ваше приложение перемещено в фон, и познее останавливается, вы должны отменить регистрацию в Bonjour и закрыть прослушивание сокетов связанных с каким либо сетевым сервисом. Приостановленное приложение не может отвечать на входящие запросы в любом случае. Закрытие этих сервисов позвляет им быть доступными, когда на самом деле это не так. Если вы не закроете сервисы Bonjour самостоятельно, система закроет эти сервисы автоматически, когда ваше приложение будет приостановлено.
  • Будте готовы получать ошибки соединений в ваших сетевых сокетах. Система может выключать сокет соединения, когда ваше приложение было приостановлено по некоторым причинам. Пока код основанный на сокетах готов для всех типов ошибок сети, таких как потеря сигнала или сетевой передачи, это не должно привести к каким либо проблемам. Когда ваше приложение возвращается, если оно сталкнется с ошибкой при использовании сокета, то ему будет довольно просто восстановить соединение.
  • Сохраняйте состояние вашего приложение прежде чем перейти в фон. При нехватке памяти, фоновое приложение может быть удалено из памяти для освобождения места. Приостановленные приложения удаляются первым, и не оповещают об этом приложение. Как результат, приложени должны воспользоваться инструментом сохранения состояния в iOS 6 и познее, чтобы сохранить состояние интерфейса на диск. ДЛя информации о поддержки этой функции, смотрите Preserving Your App’s Visual Appearance Across Launches.
  • Удаляйте жесткие ссылки на ненужные объекты при перемещении в фон. Если ваше приложение занимает много места в кеше объектов (например картинок), удалите все жесткие ссылки на эти объекты перед перемещением в фон. Для большей информации, смотрите Reduce Your Memory Footprint.
  • Останавливаетй использование общих ресурсов системы перед приостановкой. Приложения, которые взаимодействуют с общими ресурсами системы, таких как Адресная Книга или база данных Календаря должны остановить это использование перед приостановкой. Приоритет для таких ресрсов всегда предосталвяется приложениям на переднем плане. Когда ваше приложение приостановлено и будет использовать общие ресурсы, то оно будет выключено.
  • Избегайте обновления ваших окон и Views. Потому что окна и view вашего приложения не видны, когда ваше приложение находится в фоне, вы должны избегать их обновления. Исключением являются те случаи, когда вам нужно обновить данные в окне прежде чем совершится снимок вашего приложения.
  • Отвечайте уведомления о подключении и выключении от внешних аксессуаров. Для приложений, которые общаются с внешними аксессуарами, система автоматически отправляет уведомления отключения, когда сообщение перемещено в фон. Приложение должно регистрировать эти уведомления и использовать это для закрытия сессии аксессуара. Когда приложение перемещается из фона в передний план, соответствующее уведомление подключения отправляется, давая приложению возможность воссоздать подключение. Для получения большей информации, смотрите External Accessory Programming Topics.
  • Очищайте ресурсы для активных алертов при переходе в фон. Для того чтобы сохранять контекст при переключении между приложениями, система не освобождает автоматически списки action (UIActionSheet) или view алертов (UIAlertView), когда ваше приложение пермещено в фон. Например, вы можете отменить список action или view алертов программно или сохранить информацию достаточную чтобы восстановить view позже (В случае когда ваше приложение было выключено).
  • Удаляйте конфеденциальную информацию из view прежде чем перейти в фон. Когда приложение перемещено в фон, система может сделать снимок окна приложения, которое представляется короткое время при переходе в ваше приложение обратно. Перед возвращаением из вашего applicationDidEnterBackground: метода, вы должны скрыть или “замылить” пароли и другую персональную информацию которая может быть захвачена с помощью снимка.
  • Делайте минимальную работаю пока работаете в фоне. Время выполнения предостовляемое в фоне более длительное чем время выполнения на переднем плане. Приложения выполняющиее слишком долгую работу в фоне могут быть отключены системой.

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

Отказ от работы в фоне

Если вы не хотите запускать ваше приложение в фоне, то можете добавить ключ UIApplicationExitsOnSuspend (со значением YES) в вашем файле Info.plist. Когда приложение работает в этом режиме, то его циклы not-running, inactive, и active состояния никогда не попадают в состояния фона или приостановки. Когда пользователь нажимает на кнопку Home, приложение выключается. вызывая метод applicationWillTerminate: объявленный в вашем приложении давая примерно 5 секунд на очистку и выход, прежде чем завершится и перевестись в состояние not-running.

Опция отказа от работы в фоне может быть предпочтителен в некоторых условиях. В частности, если работа в фоне значительно усложняет ваше приложение. Кроме того, если ваше приложение потребляет большие объемы памяти и не может легко освободить ее, то система убьет ваше приложение быстро в любом случае, чтобы освободить место для других приложений. Таким образом может пренести пользу, чтобы сохранить время разработки.

Для большей информации о ключах содержащихся в файле Info.plist, читайте Information Property List Key Reference.

]]>
<![CDATA[Жизненый цикл приложения iOS]]>

Данная статья является вольным переводом из официальной д

]]>
https://ftp27.dev/live-ios-application/63e49f060a40f300018793aaWed, 26 Aug 2015 09:00:00 GMT

Данная статья является вольным переводом из официальной документации. В случае каких то неясностей прошу обратиться к первоисточнику: 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.

]]>
<![CDATA[Изготовление печатной платы методом фоторезиста с нанесением маски]]>

Статьи подобного рода довольно таки распространены, но мет

]]>
https://ftp27.dev/izghotovlieniie-piechatnoi-platy-mietodom-fotoriezista-s-naniesieniiem-maski/63e49f060a40f300018793a9Thu, 05 Feb 2015 09:00:00 GMT

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

Очевидно, что для начала работы нам понадобится печатная плата. Моя была реализована в Eagle.

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

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

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

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

Поместим трафарет на плату и накроем ее стеклом, после чего следует освещать плату в течении 10 минут под ультрафиолетовой лампой

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

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

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

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

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

И результат:

]]>
<![CDATA[Modbus. Что это и с чем его едят]]>

Modbus это протокол передачи данных по типу клиент-сервер. обесп

]]>
https://ftp27.dev/modbus/63e49f060a40f300018793a8Wed, 13 Aug 2014 15:06:00 GMTModbus. Что это и с чем его едят

Modbus это протокол передачи данных по типу клиент-сервер. обеспечивающий передачу данных между устройствами подключенным к различным шинам или сетям. Разработан был в далеком далеком 1979 году и с тех пор обрел неплохую популярность среди миллионов автоматических устройств. Поддержка протокола происходит и по сей день.

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

  • TCP/IP
  • Асинхронные последовательные линии передач (EIA/TIA-232-E, EIA-422, EIA/TIA-485-A, оптоволокно, радио и пр.

Структура пакета

Протокол Modbus описывает простейший типа пакета PDU (Protocol data unit), который лежит в основе всех пакетов Modbus. В зависимости от типа передачи, пакет может дополняться дополнительными полями уже на уровне ADU (Application data unit).

Modbus. Что это и с чем его едят

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

Modbus. Что это и с чем его едят

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

Modbus. Что это и с чем его едят

Размер PDU ограничен по размеру. Для последовательной линии передачи данных это = 256 байт - Адрес сервера (1 байт) - CRC (2 байта). Получается, что размер пакета ограничен 253 байтами. Следовательно:

  • RS232/R485 размер ADU = 253 байта + Адрес сервера (1 байт) + CRC (2 байта) = 256 байт
  • TCP размер ADU = 253 байта + MBAP (7 байт) = **260 байт **

Протокол Modbus использует big-endian порядок байтов для адреса и данных. Это значит, что длинное число отправленное в пакете начинается с левого байта к правому. Например, значение 0x1234 разобьется на два байта и будет отсылаться в порядке, сначала 0x12, а затем 0x34.

Modbus основан на модели данных из серии таблиц, которые имеют различные характеристики

Modbus. Что это и с чем его едят

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

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

Modbus. Что это и с чем его едят

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

Modbus. Что это и с чем его едят

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

Распределение функциональных кодов

Modbus. Что это и с чем его едят

Все функциональные код протокола Modbus можно разделить на три категории:

Публичные функциональные коды:

Четко определенные функциональные коды, подтвержденные сообществом Modbus.org, публично задокументированы и имеют в своем распоряжении тест соответствия, включают в себя как указанные функциональные коды так и не указанные, зарезервированные для использования в будущем.

Функциональные коды указанные пользователем:

Существует два диапазона таких кодов. от 65 до 72 и от 100 до 110. Пользователь волен выбирать свои действия которые сервер будет совершать получив пакет с данным кодом.

Зарезервированные функциональные коды:

Эти коды в настоящее время используются некоторыми компаниями для устаревших продуктов и не доступны для публичного использования.

Modbus. Что это и с чем его едят

Описание функциональных кодов

0x01 - Чтение регистров флагов

Этот код используется для чтения от 1 до 2000 регистров флагов по очереди. Пакеты такого запроса будут выглядеть следующим образом:

Функциональный код: 1 байт - 0x01
Адрес первого флага: 2 байта - от 0x0000 до 0xFFFF
Количество флагов: 2 байта - от 1 до 2000 (0x7D0)

Ответ же будет таким

Функциональный код: 1 байт - 0x01
Количество флагов: 1 байт - n штук
Флаги: n байт 

В случае ошибки ответ будет таким

Функциональный код: 1 байт - функциональный код+0x80
Код ошибки: 1 байт - или 01 или 02 или 03 или 04

Modbus. Что это и с чем его едят

По данной картинке можно понять какой код будет говорить о какой ошибке.

0x02 - Чтение дискретных входов

Эта функция работает аналогично 0x01

0x03 - Чтение регистров хранения

Здесь нам дозволено читать до 125 значений. Структура запроса и ответа аналогична 0x01 за тем лишь исключением, что принимать мы будем не биты а по 2 байта

0x04 - Чтение входных регистров

Аналогично 0x03

0x05 - Запись регистра флага

Здесь мы можем изменить значение регистра флага. С нуля на единичку или наоборот. Для единички используется 0xFF00, а для ноля 0x0000

Функциональный код: 1 байт - 0x01
Адрес флага: 2 байта - от 0x0000 до 0xFFFF
Состояние флага: 2 байта - либо 0x0000, либо 0xFF00

В случае успеха, мы получим точно такое сообщение, в случае провала код ошибки 0x85. Алгоритм работы будет нескольки отличаться:

Modbus. Что это и с чем его едят

0x06 - Запись одного регистра

Записывает данные в один регистр.

Функциональный код: 1 байт - 0x08
Адрес регистра: 2 байта - от 0x0000 до 0xFFFF
Состояние регистра: 2 байта - от 0x0000 до 0xFFFF

Блок-схема работы аналогична 0x05

0x07 - Чтение сигналов состояния

Запрос состоит из одного функционального кода, а ответом служит тот же функциональный код с одним байтом ответа. Схема работы такова:

Modbus. Что это и с чем его едят

0x08 - Диагностика (Только для последовательно линии)

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

Функциональный код: 1 байт - 0x08
Под-функция: 2 байта 
Данные : N x 2 байта

В качестве Под-функции указывается тип диагностики:

  • 00 - Вернуть отосланные данные
  • 01 - Перезапустить линию передачи
  • 02 - Вернуть регистр диагностики
  • 03 - Изменить ASCII код разделителя
  • 04 - Включить режим только чтения
  • 10 - Очистить счетчики и диагностические регистры
  • 11 - Вернуть количество принятых сообщений на линии
  • 12 - Вернуть количество ошибок полученных на линии
  • 13 - Вернуть количество исключений полученных на линии
  • 14 - Вернуть количество сообщений отправленных сервером
  • 15 - Вернуть количество не отвеченных сообщений
  • 16 - Вернуть количество NAK (Negative Acknowledge) сообщений
  • 17 - Вернуть количество сообщений полученных сервером, когда он был занят
  • 18 - Вернуть количество переполнений
  • 20 - Сбросить счетчик переполнения и флаг

Блок-схема работы:

Modbus. Что это и с чем его едят

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:

  • 1 - на адрес 0x1B
  • 1 - на адрес 0x1A
  • 0 - на адрес 0x19
  • 0 - на адрес 0x18
  • 1 - на адрес 0x17
  • 1 - на адрес 0x16
  • 0 - на адрес 0x15
  • 1 - на адрес 0x14

0x01:

  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0 - на адрес 0x1D
  • 1 - на адрес 0x1C

Алгоритм работы похожий как и с последовательной записью

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 байта

Ну и в заключение описание кодов ошибок, которое я тупо скопировал с википедии:

  • 01 — Принятый код функции не может быть обработан
  • 02 — Адрес данных, указанный в запросе, не доступен
  • 03 — Величина, содержащаяся в поле данных запроса, является недопустимой величиной
  • 04 — Невосстанавливаемая ошибка имела место, пока подчинённый пытался выполнить затребованное действие.
  • 05 — Подчинённый принял запрос и обрабатывает его, но это требует много времени. Этот ответ предохраняет главного от генерации ошибки тайм-аута.
  • 06 — Подчинённый занят обработкой команды. Главный должен повторить сообщение позже, когда подчинённый освободится.
  • 07 — Подчинённый не может выполнить программную функцию, принятую в запросе. Этот код возвращается для неудачного программного запроса, использующего функции с номерами 13 или 14. Главный должен запросить диагностическую информацию или информацию об ошибках от подчинённого.
  • 08 — Подчинённый пытается читать расширенную память, но обнаружил ошибку паритета. Главный может повторить запрос, но обычно в таких случаях требуется ремонт.
]]>
<![CDATA[USART на STM32L]]>

Почти год назад я описывал процесс сборки минимального наб

]]>
https://ftp27.dev/usart-na-stm32l-2/63e49f060a40f300018793abFri, 11 Apr 2014 09:00:00 GMT

Почти год назад я описывал процесс сборки минимального набора программ для сборки прошивок под 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 00000x770. Чтож, так и поступим:

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

]]>