↩️ Назад

Категории

Как не платить за интернет на даче и передавать телеметрию с IOT сервера по gsm тоннелю

28.08.2026 | из категории: IOT умный дом

Как передать данные с дачи от IOT сервера без интернета?

Сценарий: на даче в Калужской области стоит Raspberry Pi с кучей датчиков. Интернет работает только по «белым спискам», VPN в Чехию не пробиться, а данные в MySQL копятся и ждут своего часа. В городе (Москва) есть второй Pi, который раз в день звонит на дачный GSM-модем и выкачивает все показатели. Дачная SIM-карта только принимает звонки — тратить деньги на исходящие вызовы не нужно.

Решение — GSM-туннель поверх голосового вызова (CSD) с передачей данных в режиме «модемного» соединения. В этой статье мы разберём полную архитектуру: от схемы подключения до Python-скриптов, которые управляют звонками и передают байты.

🏙️ Москва (инициатор)    📞 GSM-звонок    🏡 Дача (приёмник)
Raspberry Pi + SIM800L    Raspberry Pi + SIM800L
(данные из MySQL → отправка по звонку → сохранение в JSON)

1. Архитектура и почему это работает

Обычный VoIP или VPN блокируются на уровне протоколов. Но голосовой канал (Circuit Switched Data, CSD) — это эмуляция модемного соединения поверх обычного звонка. Оператор видит его как «разговор», а внутри — поток байтов. Обнаружить и заблокировать такую передачу почти невозможно, потому что это просто аудиоданные, закодированные модемом.

Роли:

💡 Ключевая идея: Исходящий звонок делает городская сторона — так мы не тратим деньги на дачной SIM-карте. Дача только отвечает на входящий вызов, что обычно бесплатно или очень дёшево на большинстве тарифов.

2. Железо: подключение SIM800L к Raspberry Pi

Нам понадобятся два комплекта: один на даче, другой в городе. Оба идентичны по подключению.

🔌 Схема подключения

Pin на Raspberry PiPin на SIM800LПримечание
GPIO 14 (TX)RXПередача данных от Pi → модуль
GPIO 15 (RX)TXПриём данных от модуля → Pi
GNDGNDОбщий «минус»
НЕ подключать 5V!VCCПитание 3.7–4.2 В от внешнего источника
⚠️ Питание — критично! SIM800L потребляет до 2 А в пике. От USB-порта Raspberry Pi он перезагружается. Используйте внешний LM2596 (понижающий преобразователь) с аккумулятора 18650 или стабилизированный блок питания на 4 В. Обязательно поставьте электролитический конденсатор 1000 мкФ параллельно питанию — без него модуль сбрасывается при попытке дозвона.

⚙️ Настройка UART на Raspberry Pi

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

sudo raspi-config
# Interface Options → Serial Port → NO (для логина) → YES (включить порт)
sudo reboot

После перезагрузки порт будет доступен как /dev/serial0. Скорость по умолчанию у SIM800L — 9600 бод.


3. Софт: общая библиотека для работы с SIM800L

Создадим универсальный Python-класс, который будет использоваться на обеих сторонах. Он умеет отправлять AT-команды, проверять регистрацию в сети и управлять вызовами.

# gsm_modem.py
import serial
import time
import re

class GSM_MCP(micropython):  # для полноты, но на обычном Python тоже работает
    def __init__(self, port="/dev/serial0", baudrate=9600, timeout=5):
        self.ser = serial.Serial(port, baudrate, timeout=timeout)
        self.ser.flushInput()
        self.ser.flushOutput()
        self._debug = False

    def debug(self, on=True):
        self._debug = on

    def _send_at(self, cmd, wait=0.5):
        """Отправляет AT-команду и возвращает ответ"""
        self.ser.write((cmd + "\r\n").encode())
        time.sleep(wait)
        response = b""
        while self.ser.in_waiting:
            response += self.ser.read(self.ser.in_waiting)
        if self._debug:
            print(f"> {cmd}\n< {response.decode()}")
        return response.decode()

    def is_network_ok(self):
        """Проверяет, зарегистрирован ли модуль в сети"""
        resp = self._send_at("AT+CREG?", 0.5)
        return "+CREG: 0,1" in resp or "+CREG: 0,5" in resp

    def signal_quality(self):
        """Возвращает уровень сигнала (0-31), где >10 — хорошо"""
        resp = self._send_at("AT+CSQ", 0.3)
        match = re.search(r"\+CSQ:\s*(\d+)", resp)
        return int(match.group(1)) if match else 0

    def answer_call(self):
        """Поднимает трубку при входящем звонке"""
        self._send_at("ATA", 0.3)

    def hangup(self):
        """Кладёт трубку"""
        self._send_at("ATH", 0.3)

    def set_auto_answer(self, rings=1):
        """Включает автоответ после N гудков"""
        self._send_at(f"ATS0={rings}", 0.3)

    def dial(self, number):
        """Инициирует звонок на номер"""
        return self._send_at(f'ATD{number};', 2)

    def get_call_status(self):
        """Возвращает статус вызова (RING, CONNECT, NO CARRIER, etc.)"""
        # Читаем всё, что пришло за последние 100 мс
        time.sleep(0.1)
        data = self.ser.read(self.ser.in_waiting).decode().strip()
        return data

    def send_raw(self, data):
        """Отправляет сырые байты (текст или бинарные данные)"""
        self.ser.write(data.encode() if isinstance(data, str) else data)

    def read_raw(self, size=1024):
        """Читает сырые байты из порта"""
        return self.ser.read(size)

4. Дачная сторона (приёмник): ждём звонок и отдаём данные

На даче скрипт работает в фоновом режиме как демон. Он:

# dacha_server.py — запускается на дачной Raspberry Pi
import mysql.connector
from gsm_modem import GSM_MCP
import time
import json
from datetime import datetime, timedelta

DB_CONFIG = {
    "host": "localhost",
    "user": "iot_user",
    "password": "secure_pass",
    "database": "sensors"
}

def fetch_daily_data():
    """Забирает данные из MySQL за последние 24 часа и возвращает JSON-строку"""
    conn = mysql.connector.connect(**DB_CONFIG)
    cursor = conn.cursor(dictionary=True)
    yesterday = datetime.now() - timedelta(days=1)
    query = """
        SELECT timestamp, temperature, humidity, pressure, voltage
        FROM sensor_log
        WHERE timestamp > %s
        ORDER BY timestamp
    """
    cursor.execute(query, (yesterday,))
    rows = cursor.fetchall()
    cursor.close()
    conn.close()
    # Сжимаем в компактный JSON без пробелов
    return json.dumps(rows, default=str, separators=(',',':'))

def main():
    gsm = GSM_MCP()
    gsm.debug(False)

    # Ждём, пока модуль зарегистрируется в сети
    for _ in range(30):
        if gsm.is_network_ok():
            print("✅ Зарегистрирован в сети")
            break
        time.sleep(1)
    else:
        print("❌ Нет сети")
        return

    # Включаем автоответ после 1 гудка
    gsm.set_auto_answer(1)
    print("📞 Жду звонка...")

    while True:
        status = gsm.get_call_status()
        if "CONNECT" in status:
            print("🔗 Соединение установлено, отправляю данные...")
            # 1. Забираем данные из MySQL
            payload = fetch_daily_data()
            print(f"📦 Размер данных: {len(payload)} байт")

            # 2. Отправляем в поток
            gsm.send_raw(payload + "\n")  # \n — маркер конца передачи

            # 3. Даём время на передачу
            time.sleep(3)

            # 4. Вешаем трубку
            gsm.hangup()
            print("📴 Звонок завершён, жду следующего...")

        time.sleep(0.5)

if __name__ == "__main__":
    main()
📌 Маркер окончания передачи: мы добавляем \n в конце, чтобы принимающая сторона знала, что данные закончились. Для больших объёмов можно добавить заголовок с длиной сообщения.

5. Городская сторона (инициатор): звоним и забираем данные

В Москве скрипт запускается по крону раз в сутки. Он:

# moscow_client.py — запускается по крону в Москве
from gsm_modem import GSM_MCP
import time
import json
from datetime import datetime

DACHA_PHONE = "+7XXX..."  # Номер дачной SIM-карты

def main():
    gsm = GSM_MCP()
    gsm.debug(False)

    # Проверяем сеть
    if not gsm.is_network_ok():
        print("❌ Нет сети, выходим")
        return

    # Звоним на дачу
    print("📞 Звоню на дачу...")
    gsm.dial(DACHA_PHONE)

    # Ждём ответа (CONNECT) или отбоя (NO CARRIER)
    start = time.time()
    data_buffer = ""

    while time.time() - start < 60:  # таймаут 60 сек
        response = gsm.get_call_status()

        if "CONNECT" in response:
            print("🔗 Соединение установлено, принимаю данные...")
            # Читаем данные до символа \n
            while True:
                chunk = gsm.read_raw(256).decode(errors='ignore')
                data_buffer += chunk
                if "\n" in data_buffer:
                    break
                time.sleep(0.1)
            break

        elif "NO CARRIER" in response:
            print("❌ Абонент не ответил или сбросил")
            break

        time.sleep(0.3)

    # Завершаем звонок (на всякий случай)
    gsm.hangup()

    if data_buffer:
        # Удаляем лишние символы и парсим JSON
        clean_data = data_buffer.strip().split("\n")[0]
        try:
            records = json.loads(clean_data)
            print(f"✅ Получено {len(records)} записей")

            # Сохраняем в файл с меткой времени
            filename = f"dacha_data_{datetime.now().strftime('%Y%m%d_%H%M')}.json"
            with open(filename, "w") as f:
                json.dump(records, f, indent=2)
            print(f"💾 Сохранено в {filename}")

        except json.JSONDecodeError as e:
            print(f"❌ Ошибка парсинга JSON: {e}")
            print(f"Сырые данные: {data_buffer[:200]}...")
    else:
        print("⚠️ Данные не получены")

if __name__ == "__main__":
    main()

6. Настройка автозапуска и крона

На даче (демон)

Создаём systemd-сервис, чтобы скрипт стартовал при загрузке и перезапускался при падении:

sudo nano /etc/systemd/system/gsm-server.service
[Unit]
Description=GSM Data Server for Dacha
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/pi/dacha_server.py
Restart=always
RestartSec=5
User=pi

[Install]
WantedBy=multi-user.target
sudo systemctl enable gsm-server
sudo systemctl start gsm-server
sudo systemctl status gsm-server

В Москве (крон)

Добавляем задание в crontab для ежедневного запуска в 8:00 утра:

crontab -e
# Добавляем строку:
0 8 * * * /usr/bin/python3 /home/pi/moscow_client.py >> /home/pi/gsm_client.log 2>&1

7. Отладка и решение проблем

СимптомВероятная причинаРешение
Модуль не отвечает на AT Нет питания / неправильная скорость порта Проверьте питание (3.7–4.2 В) и baudrate=9600
В ответ на звонок — только шум, нет данных CSD-звонок заблокирован оператором Попробуйте отправить данные через SMS как fallback
Модуль перезагружается при звонке Недостаточная мощность питания Поставьте конденсатор 1000 мкФ и блок питания на 3А
Скрипт зависает на read_raw() Нет маркера окончания передачи Убедитесь, что дача отправляет \n в конце
«NO CARRIER» сразу после набора Номер набран неверно / сеть не поддерживает голосовые вызовы Проверьте формат номера: +7XXXXXXXXXX
⚠️ Важное ограничение: SIM800L — это 2G-модуль. Убедитесь, что в вашем районе работает 2G. В 2026 году в некоторых регионах его уже отключают. Альтернатива — модули с 4G (SIM7000, SIM7600), но там схема CSD аналогичная.

8. Что дальше? Улучшения


✅ Итог: Мы построили полностью автономный GSM-туннель, который не зависит от блокировок интернета. Raspberry Pi на даче отвечает на входящий звонок и передаёт данные из MySQL. Городской Pi инициирует звонок раз в сутки и забирает всю информацию. Никакого VPN — только обычная голосовая сеть, которую не заблокировать.

✍️ Нашли ошибку или есть вопросы? Пишите в комментарии!
Теги: #IoT #GSM #RaspberryPi #CSD #обход_блокировок

👍 Оцените статью
Всего голосов: 0



Категории:

Категории

Комментарии

Пока нет комментариев. Будьте первым!

Оставить комментарий

← Назад к списку

Посетителей сегодня: 0
о блоге | карта блога | 📡 Подписаться на RSS

© Digital Specialist | Не являемся сотрудниками Google, Яндекса и NASA