Как стать автором
Обновить

Делаем простой гидроакустический модем

Время на прочтение 13 мин
Количество просмотров 12K

Привет, глубокоуважаемые!


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

Всем заинтересованным — милости просим под кат, в реверберирующий мир подводной связи!

А вот релевантная картинка, для привлечения внимания:



«В конечном счете смысл нашего существования — тратить энергию… И по возможности, знаете ли, так, чтобы и самому было интересно, и другим полезно.»
(С) АБС, «Полдень, XXII век»

Для экономии вашего времени — краткое содержание


  • Гидроакустические модемы пока не продают на Aliexpress
  • Есть простой и нетребовательный к вычислительным ресурсам метод детектирования тона, частота которого в 4 раза меньше частоты дискретизации; Для реализации хватит и Arduino
  • Пример кода для PC лежит на GitHub
  • Приемную и передающие антенны делаем из пьезопищалок по 10 р штука
  • Покупаем (или делаем сами) платку усилителя на TDA2030 на Ali за 50 рублей
  • Делаем ЛУТ-ом предусилитель, с суммарной стоимостью ~100 рублей
  • Подключаем и идем на водоем
  • Радуемся

Мотивационная прелюдия


Сейчас вы можете купить на Aliexpress или eBay практически все, что угодно. Особенно много всякого разного для самостоятельного изготовления чего-либо электронного на основе Arduino. Вы можете сделать (если просто купить неинтересно) мильен-стопервую метеостанцию с подключением к интернет, автоматическую кормушку для кота, контроллер домашней пивоварни, но пока еще вы не можете купить гидроакустический модем, конструктор для его изготовления или хотя бы модуль для адруино. Ну и хорошо! И не надо — сейчас мы расскажем как его сделать, а еще расскажем как он работает.

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

Что-нибудь обещающее долгое и увлекательное совершенствование, то, что в последствие можно перенести даже на ардуино, будь оно неладно.

Если подходить к вопросу материалистически, то нам хотелось предложить подробный туториал для изготовления некоего простого устройства, которое было бы более-менее в состоянии передавать данные в мелководном водоеме (мелководный гидроакустический канал — наиболее сложный), подразумевало бы максимум изготовление печатной платы при помощи ЛУТ, с общей стоимостью, не превышающей на минималочках пары-тройки сотен рублей.

Что мы будем делать сегодня?


  • вспомним, как сделать подходящую гидроакустическую антенну и изготовим парочку;
  • одну из антенн подключим к ПК через усилитель на TDA за ~50 рублей и получим передатчик;
  • для второй сделаем при помощи ЛУТ предусилитель за ~100 рублей;
  • напишем (я уже все написал и положил на Git) простой модем на C# и испытаем все на ближайшем водоеме;

Что нам для этого понадобится?


  • два пьезоэлемента. Например, от часов или открытки;
  • кабель RG-174/U (или аналогичный) ~5 метров;
  • безуксусный герметик;
  • водостойки лак;
  • фольгированный текстолит, в общей сложности примерно 100x200 мм;
  • усилитель на TDA2030 (например, такой, за 50 рублей);
  • комплектующие для предусилителя

Как оно работает?


Вся идея простейшего модема построена на, опять же, простейшем (совпадение?) детекторе определенного тона, про который, я к своему стыду не слышал. Рассказал мне про него совершенно невзначай andrey_9999a. Он, кстати, сделал и плату предусилителя.

В связи с этим мне вспомнилась цитата из книги Леонарда Сасскинда «Битва при черной дыре»:
«Как ценитель вина, я более или менее уверен, что даже с закрытыми глазами смогу отличить красное от белого. Еще более надежно я отличу вино от пива. Но вот дальше вкус меня подведет.»
Могу сказать про себя, что как заправский электронщик я более или менее уверен, что точно смогу спаять два толстых провода. Еще более надежно я отличу горячий паяльник от холодного даже с закрытыми глазами, но вот дальше навык меня подведет. Поэтому все, что касается разработки и изготовления плат — это работа моих товарищей и коллег andrey_9999a и StDmitriev.

Итак, вернемся к детектору. Он является упрощенным частным случаем вычисления интеграла Фурье:

$\int_0^Tf(x)sin(\omega t)dt$


В случае цифрового сигнала, для вычисления амплитуды произвольной гармоники потребуется выполнить дискретное преобразование Фурье, для Ардуины это тяжеловато, но хитрость состоит в том, что если взять в качестве несущей частоты Fc такую, что она будет ровно в 4 раза меньше частоты дискретизации Fs, то амплитуду этой гармоники можно вычислять демонически проще.

В этом случае dt = 2π*(Fs/4)/Fs = π/2, и на период несущей приходится всего 4 сэмпла:



Если все сдвинуть на π/4 то сэмплы будут принимать только два значения: √2/2 и -√2/2, для простоты оставим только знаки — «+» и «-».

Суть же метода состоит в том, что синусную фазу мы представляем как последовательность знаков «+» «+» «-» «-», а косинусную как «+» «-» «-» «+».

Пусть входной сигнал лежит в буфере sn, у нас есть два кольцевых буфера усреднения для синусной и косинусной фазы — bs и bc размером N. Указатели головы и хвоста у них общие — bH и bT. В начальный момент времени bH = N-1, bT = 0. Счетчик циклов усреденения C = 0.

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

Пример кода
a = sn(i)
bs(bH) = a
bc(bH) = a
s1 = s1 + a - bs(bT)
s2 = s2 + a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N

a = sn(i+1)
bs(bH) = a
bc(bH) = -a
s1 = s1 + a - bs(bT)
s2 = s2 - a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N

a = sn(i+2)
bs(bH) = -a
bc(bH) = -a
s1 = s1 - a - bs(bT)
s2 = s2 - a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N

a = sn(i+3)
bs(bH) = -a
bc(bH) = a
s1 = s1 - a - bs(bT)
s2 = s2 + a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N


После каждой обработанной четверки сэмплов проверяем счетчик циклов усреднения и если он перевалил за N, то вычисляем амплитуду cA несущей:

if ++cycle >= N
      cA = sqrt(s1 * s1 + s2 * s2)
      cycle = 0
end

Вот так это выглядит на идеальном сигнале:



Синим показан сам сигнал, красным — значения амлитуды несущей (все приведено к диапазону -1..1). В данном случае N=2 т.к. нет никаких шумов и все и так отлично работает.

Теперь добавим немного белого шума и посмотрим, как на это отреагирует наш детектор:



Я добавил белого шума таким образом, чтобы соотношение сигнал-шум было равно 0 дБ. На рисунке выше синим показан зашумленный сигнал, зеленым — исходный, а красным — значение амплитуды. В этом случае детектор при N=2 уже ничего не задетектировал, и минимальное N при котором он исправно работет равно 32. Т.е. размер окна обработки в сэмплах составил 32*4 = 128 сэмплов.

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

Это очень хорошо, но нам нужно передавать биты, а биты могут принимать два значения.

Реализовать систему с двумя сигнальными состояниями при помощи одного — так себе идея, поэтому мы не будем кодировать одно из состояний тишиной (паузой). Это бы сильно затруднило детектирование: нужно было бы как-то выделять начало посылки, придумывать как оформить ее конец и т.д.

Вместо этого «1» и «0» мы будем кодировать импульсами разной длины, между битами наличествует т.н. защитный интервал — ведь нам же еще нужно бороться с многолучевым распространением и реверберацией. Говоря простым языком, защитный интервал — это то место (время), где затухнут все отражения предшествующего бита, все послезвучия и эхо.

Забегая вперед, примем к сведению, что при такой структуре сигнала алгоритм работы приемника очень сильно упрощается: ждем когда появился тон, засекаем начало, ждем, когда тон пропал и опять засекаем время — если полученное время больше похоже по длине на «1» то, наверное мы приняли бит со значением «1», если больше похоже на «0» — то видимо мы приняли бит, со значением «0».

В общем, можно сказать, что это некий вариант морзянки.

Софтовая часть модема


Для нетерпеливых — пример лежит на GitHub. Написан на скорую руку на C# (потому что для ПК я пишу на нем и мне просто так удобнее и быстрее).

Для воспроизведения и захвата звука с микрофонного входа используется замечательная библиотека NAudio.

Вся логика модема находится в классе SUAModem (Simple Underwater Acoustic Modem).

В конструктор передаются следующие параметры:

double sRateHz — частота дискретизации в Герцах;
int wSize — размер окна обработки в сэмплах;
int oneMultiplier — сколько «окон» длится бит со значением «1»
int zeroMultiplier — сколько «окон» длится бит со значением «0»
double eThreshold — порог, скажем о нем позже

Для формирования сигнала из массива байт есть метод ModulateData(byte[] data), который возвращает массив 16-ти битных знаковых сэмплов.

public short[] ModulateData(byte[] data)
public short[] ModulateData(byte[] data)
{
   double alpha = 0;
   double phase = 0;           

   List<short> samples = new List<short>();            
   BitArray bits = new BitArray(data);         

   for (int i = 0; i < bits.Length; i++)
   {
         int sLim = (bits[i]) ? oneDurationSmp : zeroDurationSmp;
         alpha = 0;
         phase = 0;
   
         for (int sIdx = 0; sIdx <= sLim; sIdx++)
         {
               alpha = Math.Sin(phase);
               phase += delta;
               if (phase >= alimit)
                        phase -= alimit;
               samples.Add(Convert.ToInt16(alpha * short.MaxValue));
         }
         samples.AddRange(new short[defenseIntervalSmp]);                
     }
     return samples.ToArray();
}


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

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

Для генерации тона с частотой $F_n$ при заданной частоте дискретизации $F_s$ соответствующее значение $\delta$ вычисляется просто:

$\delta=2\pi F_n/F_s$



Для формирования и излучения сигнала есть метод TransmitData(byte[] data), который внутри себя вызывает ModulateData:

public double TransmitData(byte[] data)
public double TransmitData(byte[] data)
{
     var samples = ModulateData(data);
     double txTime = ((double)samples.Length) / SampleRateHz;

     var rawBytes = new byte[samples.Length * 2];
     for (int i = 0; i < samples.Length; i++)
     {
          var bts = BitConverter.GetBytes(samples[i]);
          rawBytes[i * 2] = bts[0];
          rawBytes[i * 2 + 1] = bts[1];
     }

     using (var ms = new MemoryStream(rawBytes))
     {
          using (var rs = new RawSourceWaveStream(ms, new    WaveFormat(Convert.ToInt32(SampleRateHz), 16, 1)))
                {
                    using (var wo = new WaveOutEvent())
                    {
                        wo.Init(rs);
                        wo.Play();
                        while (wo.PlaybackState == PlaybackState.Playing)
                        {
                            Thread.SpinWait(1);
                        }                        
                    }

                    rs.Close();
             }
             ms.Close();
       }
       return txTime;
}


О принятии очередного байта класс SUAModem сообщает при помощи события DataReceivedEventHandler.

Входные сэмплы послупают в анализ при помощи метода ProcessInputSignal(short[] data), где записываются в кольцевой буфер ring. Анализ происходит в отдельном потоке, в методе Receiver.

А сам приемник живет в методе Receive:

private void Receive()
private void Receive()
int a;
            while (rCnt >= 4)
            {
                a = ring[rRPos];
                rRPos = (rRPos + 1) % rSize;
                rCnt--;
                dRing1[rHead] = a;
                dRing2[rHead] = a;
                s1 += a - dRing1[rTail];
                s2 += a - dRing2[rTail];
                rHead = (rHead + 1) % windowSize;
                rTail = (rTail + 1) % windowSize;

                a = ring[rRPos];
                rRPos = (rRPos + 1) % rSize;
                rCnt--;
                dRing1[rHead] = a;
                dRing2[rHead] = -a;
                s1 += a - dRing1[rTail];
                s2 += -a - dRing2[rTail];
                rHead = (rHead + 1) % windowSize;
                rTail = (rTail + 1) % windowSize;

                a = ring[rRPos];
                rRPos = (rRPos + 1) % rSize;
                rCnt--;
                dRing1[rHead] = -a;
                dRing2[rHead] = -a;
                s1 += -a - dRing1[rTail];
                s2 += -a - dRing2[rTail];
                rHead = (rHead + 1) % windowSize;
                rTail = (rTail + 1) % windowSize;

                a = ring[rRPos];
                rRPos = (rRPos + 1) % rSize;
                rCnt--;
                dRing1[rHead] = -a;
                dRing2[rHead] = a;
                s1 += -a - dRing1[rTail];
                s2 += a - dRing2[rTail];
                rHead = (rHead + 1) % windowSize;
                rTail = (rTail + 1) % windowSize;

                if (++cycle >= windowSize)
                {
                    cycle = 0;

                    currentEnergy = Math.Sqrt(s1 * s1 + s2 * s2) / windowSize;
                    double de = currentEnergy - prevEnergy;                   

                    #region analysis

                    if (skip > 0)
                        skip -= windowSize * 4;
                    else
                    {
                        if (isRise)
                        {
                            if (de > -Threshold)
                            {
                                riseSmp += windowSize * 4;
                            }
                            else
                            {
                                // analyse symbol
                                isRise = false;

                                double oneDiff = Math.Abs(oneDurationSmp - riseSmp);
                                double zeroDiff = Math.Abs(zeroDurationSmp - riseSmp);

                                if (oneDiff > zeroDiff)
                                {
                                    // Mostly likely "0"
                                    AddBit(false);
                                }
                                else
                                {
                                    // Mostly likely "1"
                                    AddBit(true);
                                }

                                samplesSinceLastBit = 0;
                                skip = defenseIntervalSmp / 2;
                            }
                        }
                        else
                        {
                            if (de > Threshold)
                            {
                                isRise = true;
                                riseSmp = windowSize * 4;
                            }
                        }
                    }

                    #endregion

                    prevEnergy = currentEnergy;

                    if (bPos > 0)
                    {                        
                        samplesSinceLastBit += 4 * windowSize;
                        if (samplesSinceLastBit >= defenseIntervalSmp * 2 + zeroDurationSmp + oneDurationSmp)
                        {                            
                            DiscardBits();
                        }
                    }
                }                
            }
        }        


Из кода видно, что анализ ведется по 4 сэмпла, при желании можно сохранять состояние и вести обработку и по одному сэмплу, что будет полезно при переносе на какой-нибудь немощный МК.
По мере поступления данных вычисляется значение амлитуды s на частоте sRateHz/4. Вычисляется разница между предыдущим и текущим значением амплитуды и затем сравнивается с заданным порогом, подобранным экспериментально. Пример позволяет в реальном времени этот порог менять.

Резкое увеличение амплитуды свидетельствует о начале «бита», резкий (несколько менее резкий — из-за реверберации) спад — о завершении «бита».

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

Железная часть модема


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

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

У меня они остались с того раза, так что я этот шаг пропускаю.

Ту антенну, которая предназначена для передачи мы подключаем к плате усилителя с алиэкспресс. Для нескольких десятков метров (может даже для сотни) нам этого вполне хватит. Никаких хитростей тут нет — выход звуковой карты ноутбука идет на вход усилителя, который питается от свинцового АКБ 12 вольт 4 Ач. На выход подключена наша гидроакустическая передающая антенна из пьезопищалки. В моем случае это выглядит вот так:





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

С приемной антенной несколько сложнее. Хоть пьезопищалки и очень чувствительны, все же этого недостаточно. Нам придется собрать плату предусилителя с фильтром на полосу 5-35 кГц.
Коэффициент усиления мы берем 1000.

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

Технология ЛУТ обсуждалась стотыщмильенов раз, но дайте же и нам внести свою лепту.

Фото технологического процесса
Итак, берем подходящий журнал, у нас под рукой оказался только этот:



Берем оттуда пару страниц и печатаем на них слои при помощи лазерного принтера.



Совмещаем при помощи иголочек и склеиваем по одной стороне, как показано на фото:



Перед применением утюга смачиваем тонер изопропиловым спиртом:



Утюжим через сложенный вчетверо лист А4:



Размачиваем теплой водой под краном:



И отмываем остатки бумаги. После чего получаем заготовку, готовую к травлению:



Лишнее отрезаем при помощи ножниц по металлу или кому чем удобнее.

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



В результате, после травления и отмывки тонера получаем такой полуфабрикат:





После напайки компонентов и отмывки плата выгладит вот так.



А так выглядит приемная часть в сборе. Питание осуществляется от такого же свинцового АКБ 12 вольт:



Небольшой дисклеймер по поводу фильтра
Если читатель захочет изменить полосу, то предлагаем пересчитать фильтр 8-ого порядка, собранный на дешевом 4-х канальном операционнике TL084C (DA2 на схеме), резисторах R10-R13, R15-R23 и конденсаторах C5-C8, C11, C12, C14 и С15.

На всякий случай приведем здесь АЧХ текущей реализации фильтра:



А вот еще проект для этого фильтра, созданный в приложении Qucs

Опыты и испытания


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

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



Синим показан сам сигнал, красным — разница между текущим и предыдущим значениями амплитуды (фронт), зеленым — разница между предыдущим и текущим значениями (спад). Без труда можно «демодулировать» эту часть посылки: 1 0 0 0 1 1 0. Ноль у нас в два раза дольше единицы, а длительность защитного интервала равна длительности нуля.

Далее, также без усилителя и предусилителя опускаем наши антенны в металлический бак, размерами 3х1.5х1.5. У нас такой стоит в лаборатории, и мы завели себе правило, что не занимаемся никакой связью, если она хоть как-то не в состоянии работать в этом баке. Дело в том, что в таком замкнутом объеме энергии особо некуда деваться — звук чудесно и многократно отражается от металлических стенок и в точке приема получается каша. А с учетом того, что мы обычно проверяем готовые девайсы с энергетикой, рассчитанной на тысячи метров, можете представить что там творится.

Например, два наших модема RedLINE устойчиво работают в этом баке только на расстоянии не больше двух метров, а два uWAVE-а стабильно работают примерно на 1 метре. При том что первый в открытой воде работает до 8000 метров, а второй — до километра.
Конечно, все коммерческие продукты не используют такие примитивные схемы модуляции, о которой идет речь в статье и устроены гораздо сложнее, но нам сейчас важно понять азы и с пользой поделать что-то руками.

В общем, опускаем антенны в бак на расстояние порядка 50 сантиметров и получаем уже нечто гораздо менее благообразное, чем при непосредственном контакте антенн:



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

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



Из скрина выше видно, что при тех настройках, передача сообщения «Hello, habr!!! :-)» состоящее из 19 байт занимает 9.132 секунд, то есть скорость передачи составила 16,6 бит/с. К слову сказать, чтобы модем работал в нашем баке пришлось увеличить защитный интервал так, что скорость передачи упала до ~3 бит/с.

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



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





Антенны приемника и передатчика опускались прямо с берега, глубина там резко уходит с 0.5 до 2 метров. В опыте, который показан на фото выше были, как ни странно, самые плохие условия, дистанция там была всего порядка 5 метров — это вообще была первоначальная настройка. Из 20 переданных сообщений по 3 байта, в шести из них было побито по одному байту.

Потом, когда мы подключили приемник ко второму ноутбуку и перенесли на другой берег пруда (дистанция порядка 30 метров) передача проходила значительно лучше — на 40 сообщений размером от 3 до 13 байт было всего пару ошибок.



На следующем фото на карте видны места, где располагались антенны.



Заключение и дальнейшие изыскания


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

Чтобы не быть голословным относительно построения навигационных систем на простых сигналах, взгляните на интересную работу, в которой построили полноценную длиннобазисную навигационную систему. В этой системе определяют положение пингера, который периодически передает свою глубину. Значение глубины при этом кодируется расстоянием между двумя простыми импульсами на определенной частоте. Так что да-да, горшки не боги обжигают, дорогу осилит идущий, терпение и труд, учиться, учиться, учиться — вот это вот все.

Возможно, при наличии времени мы сделаем DIY-проект по позиционированию автономного пингера, излучающего простые сигналы. Нечто подобное, но не DIY мы уже делали на базе наших модемов uWAVE, о чем даже попытались снять видео. Будет очень интересно услышать ваши мнения на этот счет — очень важно иметь подтверждение, что делаешь что-то не напрасно.

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

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

На этом заседание объявлю закрытым, а если вас заинтересовала тема, вот список наших предыдущих статей:

Подводный GPS с нуля за год
Подводный GPS на подводном роботе: опыт использования
Мы сделали самый маленький в мире гидроакустический модем
К вопросу о влиянии цианобактерий на речевые функции президента
Делаем простую гидроакустическую антенну из мусора
Сеанс передачи видео звуком через воду с разоблачением
Подводный «GPS» на двух приемопередатчиках
Навигация под водой: пеленгуй не пеленгуй — обречен ты на успех
Подводный GPS: продолжение

P.S.


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

P.P.S.


Железки далеко не убирайте — в следующий раз мы с их помощью будем опять передавать «видео» через воду.
Теги:
Хабы:
+48
Комментарии 25
Комментарии Комментарии 25

Публикации

Истории

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн