В начало | Зарегистрироваться | Заказать наши киты почтой
 
 
 
 

Обмен данными между смартфоном и ESP32. Android. BLE. GAP. Часть 1

📆6 августа 2023   ✒️AYAN   🔎1.756   💬0  

Привет, карады-датагорцы! Эта моя статья открывает серию, посвящённую обмену данными между ESP32 и смартфоном. Сегодня мы рассмотрим одностороннюю передачу данных на Android по протоколу GAP Bluetooth Low Energy.

Видео-демонстрация проекта



Приложение для ESP32

Программа написана на языке Си в VS Code с плагином Espressif IDF. За основу взят пример ble_ibeacon из папки esp-idf\examples\bluetooth\bluedroid\ble.
Передаваемые в составе широковещательного пакета данные — значение температуры, измеряемое датчиком DS18B20, контроль за которым обеспечивают две представленные ниже библиотеки.

OneWire.h


#ifndef ONEWIRE_H_
#define ONEWIRE_H_

#include <rom/ets_sys.h>
#include "driver/gpio.h"

#define OWI_BUS         23
#define OWI_BUS_HIGH    gpio_set_direction(OWI_BUS, GPIO_MODE_INPUT)
#define OWI_BUS_LOW     gpio_set_direction(OWI_BUS, GPIO_MODE_OUTPUT); gpio_set_level(OWI_BUS, 0) 

#define DELAY_A         6
#define DELAY_B         64
#define DELAY_C         60
#define DELAY_D         10
#define DELAY_E         9
#define DELAY_F         55
#define DELAY_G         0
#define DELAY_H         480
#define DELAY_I         70
#define DELAY_J         410

#define MSBit           0x80
#define LSBit           0x01

void owiInit();
void owiReset();
void owiWriteBit_0();
void owiWriteBit_1();
unsigned char owiReadBit();
void owiWriteByte(unsigned char data);
unsigned char owiReadByte();

#endif


OneWire.c


#include "oneWire.h"

void owiInit()
{
  OWI_BUS_HIGH;
  ets_delay_us(DELAY_H);      
}

void owiReset()
{
  OWI_BUS_LOW;
  ets_delay_us(DELAY_H);  
  OWI_BUS_HIGH;
  ets_delay_us(DELAY_I);  
  ets_delay_us(DELAY_J);
}

void owiWriteBit_0()
{
  OWI_BUS_LOW;
  ets_delay_us(DELAY_C);
  OWI_BUS_HIGH;
  ets_delay_us(DELAY_D);  
}

void owiWriteBit_1()
{
  OWI_BUS_LOW;
  ets_delay_us(DELAY_A);
  OWI_BUS_HIGH;
  ets_delay_us(DELAY_B);  
}

unsigned char owiReadBit()
{
  unsigned char data = 0;

  OWI_BUS_LOW;
  ets_delay_us(DELAY_A);  
  OWI_BUS_HIGH;
  ets_delay_us(DELAY_E);  
  int owiBusLevel = gpio_get_level(OWI_BUS);
  if(owiBusLevel) 
      data = 1;  
  ets_delay_us(DELAY_F);  

  return data;
}

void owiWriteByte(unsigned char data)
{
  for (unsigned char bitNum = 0; bitNum < 8; bitNum++) 
  {
    if (data & LSBit) 
    {
      owiWriteBit_1();
    } 
    else 
    {
      owiWriteBit_0();
    }
    data >>= 1;
  }  
}

unsigned char owiReadByte()
{
  unsigned char data = 0;  

  for (unsigned char bitNum = 0; bitNum < 8; bitNum++) 
  {  
    data >>= 1;
    if (owiReadBit()) 
    {
      data |= MSBit;
    }
  }

  return data;    
}


DS18B20.h


#ifndef DS18B20_H_
#define DS18B20_H_

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "oneWire.h"

#define SKIP_ROM            0xCC
#define READ_SCRATCH_PAD    0xBE
#define CONVERT_T           0x44

#define MINUS               1
#define PLUS                0

#define SIGN_BITS           0xf8

#define FREE_STATE          0
#define CONVERSION_STATE    1

void ds18b20_Init();
void ds18b20_StartConversion();
float ds18b20_ReadTemperature();

extern unsigned char dataBuff[2], temperatureSign, ds18b20_timeCounter, ds18b20_state;

#endif 


DS18B20.c


#include "ds18b20.h"

unsigned char dataBuff[2], temperatureSign, ds18b20_timeCounter, ds18b20_state = FREE_STATE;

void ds18b20_Init()
{
    owiInit();
}

void ds18b20_StartConversion()
{
  ds18b20_state = CONVERSION_STATE;
  owiReset();
  owiWriteByte(SKIP_ROM);
  owiWriteByte(CONVERT_T);
}

float ds18b20_ReadTemperature()
{    
  owiReset();
  owiWriteByte(SKIP_ROM);
  owiWriteByte(READ_SCRATCH_PAD);
  for(int i = 0; i < 2; i++)
  {
    dataBuff[i] = owiReadByte();
  }
  owiReset();
  
  if(dataBuff[1] & SIGN_BITS)
  {
    temperatureSign = MINUS;
  }
  else
  {
    temperatureSign = PLUS; 
  }

  float temperature = (dataBuff[1] << 8 | dataBuff[0]) / 16.0;

  return temperature;
}


Библиотека esp_ibeacon_api ответственна за инициализацию составляющих широковещательного пакета: UUID, тип и длину данных, ID компании, RSSI и пр.

esp_ibeacon_api.h


#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>

#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"

#define IBEACON_SENDER         0
#define IBEACON_RECEIVER       1
#define IBEACON_MODE           CONFIG_IBEACON_MODE
#define ENDIAN_CHANGE_U16(x)   ((((x)&0xFF00)>>8) + (((x)&0xFF)<<8))
#define ESP_UUID               {0xFD, 0xA5, 0x06, 0x93, 0xA4, 0xE2, 0x4F, 0xB1, 0xAF, 0xCF, 0xC6, 0xEB, 0x07, 0x64, 0x78, 0x25}
#define ESP_MAJOR              10167
#define ESP_MINOR              61958


typedef struct 
{
 uint8_t flags[3];
 uint8_t length;
 uint8_t type;
 uint16_t company_id;
 uint16_t beacon_type;
}__attribute__((packed)) esp_ble_ibeacon_head_t;

typedef struct 
{
 uint8_t proximity_uuid[16];
 uint16_t major;
 uint16_t minor;
 int8_t measured_power;
}__attribute__((packed)) esp_ble_ibeacon_vendor_t;


typedef struct 
{
 esp_ble_ibeacon_head_t ibeacon_head;
 esp_ble_ibeacon_vendor_t ibeacon_vendor;
}__attribute__((packed)) esp_ble_ibeacon_t;

extern esp_ble_ibeacon_head_t ibeacon_common_head;
bool esp_ble_is_ibeacon_packet (uint8_t *adv_data, uint8_t adv_data_len);
esp_err_t esp_ble_config_ibeacon_data (esp_ble_ibeacon_vendor_t *vendor_config, esp_ble_ibeacon_t *ibeacon_adv_data);


esp_ibeacon_api.c


#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "esp_gap_ble_api.h"
#include "esp_ibeacon_api.h"

const uint8_t uuid_zeros[ESP_UUID_LEN_128] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

esp_ble_ibeacon_head_t ibeacon_common_head = 
{
  .flags = {0x02, 0x01, 0x06},
  .length = 0x1A,
  .type = 0xFF,
  .company_id = 0x004C,
  .beacon_type = 0x1502
};

esp_ble_ibeacon_vendor_t vendor_config = 
{
  .proximity_uuid = ESP_UUID,
  .major = ENDIAN_CHANGE_U16(ESP_MAJOR), //Major=ESP_MAJOR
  .minor = ENDIAN_CHANGE_U16(ESP_MINOR), //Minor=ESP_MINOR
  .measured_power = 0xC5
};

bool esp_ble_is_ibeacon_packet (uint8_t *adv_data, uint8_t adv_data_len)
{
  bool result = false;

  if ((adv_data != NULL) && (adv_data_len == 0x1E))
  {
    if (!memcmp(adv_data, (uint8_t*)&ibeacon_common_head, sizeof(ibeacon_common_head)))
    {
      result = true;
    }
  }

  return result;
}

esp_err_t esp_ble_config_ibeacon_data (esp_ble_ibeacon_vendor_t *vendor_config, esp_ble_ibeacon_t *ibeacon_adv_data)
{
  if ((vendor_config == NULL) || (ibeacon_adv_data == NULL) || (!memcmp(vendor_config->proximity_uuid, uuid_zeros, sizeof(uuid_zeros))))
  {
    return ESP_ERR_INVALID_ARG;
  }

  memcpy(&ibeacon_adv_data->ibeacon_head, &ibeacon_common_head, sizeof(esp_ble_ibeacon_head_t));
  memcpy(&ibeacon_adv_data->ibeacon_vendor, vendor_config, sizeof(esp_ble_ibeacon_vendor_t));

  return ESP_OK;
}


Основная функция main():
1. Инициализирует и включает стек проколов Bluedroid.
2. Инициализирует и включает контроллер BLE.
3. Инициализирует датчик DS18B20.
4. В цикле c периодичностью 800 мс считывает текущее значение температуры и включает его в состав широковещательного пакета.

main.c


#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_ibeacon_api.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "DS18B20.h"

unsigned int tensCurrent = 0x00, unitsCurrent = 0x00, fractionCurrent = 0x00;

static const char* TAG = "Temperature";
extern esp_ble_ibeacon_vendor_t vendor_config;

static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);

static esp_ble_adv_params_t ble_adv_params = 
{
  .adv_int_min        = 0x20,
  .adv_int_max        = 0x40,
  .adv_type           = ADV_TYPE_NONCONN_IND,
  .own_addr_type      = BLE_ADDR_TYPE_PUBLIC,
  .channel_map        = ADV_CHNL_ALL,
  .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};

static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
  esp_err_t err;

  switch (event) 
  {
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
      esp_ble_gap_start_advertising(&ble_adv_params);
    break;
    
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
      if ((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) 
      {
        ESP_LOGE(TAG, "Scan start failed: %s", esp_err_to_name(err));
      }
    break;
        
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
      if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) 
      {
        ESP_LOGE(TAG, "Adv start failed: %s", esp_err_to_name(err));
      }
    break;
    
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
      if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS)
      {
        ESP_LOGE(TAG, "Adv stop failed: %s", esp_err_to_name(err));
      }
    break;

    default:
    break;
  }
}

void ble_ibeacon_appRegister(void)
{
  esp_err_t status;

  ESP_LOGI(TAG, "register callback");

  if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) 
  {
    ESP_LOGE(TAG, "gap register error: %s", esp_err_to_name(status));
    return;
  }
}

void ble_ibeacon_init(void)
{
  esp_bluedroid_init();
  esp_bluedroid_enable();
  ble_ibeacon_appRegister();
}

void app_main(void)
{
  ds18b20_Init();
  ESP_ERROR_CHECK(nvs_flash_init());
  ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
  esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
  esp_bt_controller_init(&bt_cfg);
  esp_bt_controller_enable(ESP_BT_MODE_BLE);

  ble_ibeacon_init();

  esp_ble_ibeacon_t ibeacon_adv_data;
  esp_err_t status = esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);
  if (status == ESP_OK)
  {
    esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
  }
  else 
  {
    ESP_LOGE(TAG, "Config iBeacon data failed: %s\n", esp_err_to_name(status));
  }

  while(1)
  {
    if(ds18b20_state == FREE_STATE)
    {
      ds18b20_StartConversion();
    }
    else if(ds18b20_state == CONVERSION_STATE)
    {
      ds18b20_state = FREE_STATE;            
      float currentTemperature = ds18b20_ReadTemperature();   
      tensCurrent = (unsigned int)(currentTemperature / 10);
      unitsCurrent = (unsigned int)currentTemperature - tensCurrent * 10;
      fractionCurrent = (unsigned int)(currentTemperature * 10.0) - tensCurrent * 100 - unitsCurrent * 10;
      ESP_LOGE(TAG, "%3.1f", currentTemperature);

      esp_ble_gap_stop_advertising();
      vendor_config.proximity_uuid[13] = (unsigned char)tensCurrent;
      vendor_config.proximity_uuid[14] = (unsigned char)unitsCurrent;
      vendor_config.proximity_uuid[15] = (unsigned char)fractionCurrent;
      esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);
      esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
      esp_ble_gap_start_advertising(&ble_adv_params);
    }
    vTaskDelay(800 / portTICK_PERIOD_MS);        
  }
}


Android-приложение

написано на языке Java в Android Stidio. В манифесте прописаны разрешения на работу с Bluetooth.

AndroidManifest


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.Gap"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


В xml-файле определены параметры:
• кнопки сканирования BLE-устройств,
• текстового поля значения температуры.

activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/scanButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="124dp"
        android:onClick="scan"
        android:text="Scan"
        android:textSize="20dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/temperatureTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="+00.0℃"
        android:textSize="60dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.495"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/scanButton"
        app:layout_constraintVertical_bias="0.208" />

</androidx.constraintlayout.widget.ConstraintLayout>


Java-код:
а) По нажатию кнопки сканирует BLE-устройства.
б) Отражает в текстовом поле текущее значение температуры.

MainActivity.java


package com.punchthrough.gap;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
    String[] peripheralAddresses = new String[]{"3C:71:BF:9D:70:96"};
    List<ScanFilter> filters = null;
    BluetoothDevice device;
    Button scanButton;
    TextView temperatureTextView;

    ScanSettings scanSettings = new ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
            .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
            .setReportDelay(0L)
            .build();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        scanButton = findViewById(R.id.scanButton);
        temperatureTextView = findViewById(R.id.temperatureTextView);

        if (peripheralAddresses != null) {
            filters = new ArrayList<>();
            for (String address : peripheralAddresses) {
                ScanFilter filter = new ScanFilter.Builder()
                        .setDeviceAddress(address)
                        .build();
                filters.add(filter);
            }
        }
    }

    public void scan(View view) {

        if (scanner != null) {
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            scanner.startScan(filters, scanSettings, scanCallback);
            Toast.makeText(this, "scanning...", Toast.LENGTH_LONG).show();
        } else {
            Toast.makeText(this, "could not get scanner object", Toast.LENGTH_LONG).show();
        }
    }

    private final ScanCallback scanCallback = new ScanCallback() {
        @SuppressLint("SetTextI18n")
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            device = result.getDevice();
            Toast.makeText(MainActivity.this, "device with address" + "\n" + device.getAddress() + "\n" + "found", Toast.LENGTH_LONG).show();
            if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
            }
            byte[] data = result.getScanRecord().getManufacturerSpecificData(76);
            temperatureTextView.setText("+" + data[15] + data[16] + "." + data[17] + "\u2103");
        }
    };
}


Файлы

🎁Все файлы проекта, исходники: ayan-esp32.7z  45.64 Mb ⇣ 25

Спасибо за внимание!
Продолжение следует.
 

Читательское голосование

Нравится

Статью одобрили 7 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Обмен данными между Android-приложением и nRF52832. Часть 2. BLE... Приветствую вас, уважаемые датагорцы! Сегодня — обмен данными между Android-приложением и nRF52832...
Схема на Датагоре. Новая статья Электронные часы-термометр с беспроводным датчиком через радиомодуль nRF24L01... Здравствуйте, уважаемые Датагорцы! Представляю вашему вниманию электронные часы с функцией...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 2. Шаблонные файлы и инструкции МК... В предыдущей части статьи мы провели подготовительную работу и вкратце разобрали принципы работы...
Схема на Датагоре. Новая статья Обмен данными между Java-приложением и МК. Часть 1. По проводу, USB-UART... Приветствую всех жителей и гостей кибер-города Датагор! Работа устройства на базе микроконтроллера...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.... Сегодня мы рассмотрим работу следующих модулей периферии: • порта ввода-вывода, • таймера •...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 7. Компиляция, отладка, загрузка... Привет датагорцам и гостям нашего кибер-города! В предыдущих частях материала по Ассемблеру...
Схема на Датагоре. Новая статья Обмен данными между Android-приложением и nRF52832. Часть 1. Serial... Здравствуйте! Представляю вашему вниманию проект обмена данными между Android-приложением и SoC...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI... В проекте из предыдущей части нашей ассемблерной эпопеи мы подключали к микроконтроллеру светодиод...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 3. TFT дисплей 2.8" (240х320) на ILI9341... Битва за урожай закончена, можно продолжить повествование. Полноцветный TFT-дисплей 240×320 ILI9341...
Схема на Датагоре. Новая статья Программирование микроконтроллеров на языке C. Часть 2... Добрый день, уважаемые камрады-датагорцы! Сегодня, рассмотрев некоторые общие моменты, мы займёмся...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 5. Графика... В предыдущих частях статьи нами более или менее подробно были рассмотрены основные принципы работы...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 3. Макросы и функции... Привет, датагорцы — любители Ассемблера! В пункте 2.5.2 «Инструкции условного перехода» предыдущей...
 

Комментарии, вопросы, ответы, дополнения, отзывы

 

Добавить комментарий, вопрос, отзыв 💬

Камрады, будьте дружелюбны, соблюдайте правила!

  • Смайлы и люди
    Животные и природа
    Еда и напитки
    Активность
    Путешествия и места
    Предметы
    Символы
    Флаги
 
 
В начало | Зарегистрироваться | Заказать наши киты почтой