Rigol na smyczy

fałszywy telefon zaufania

rozpoznawanie kodów DTMF w LabVIEW


Rigol na smyczy vol.1 - część pierwsza, o SCPI w linuksowej konsoli
Rigol na smyczy vol.2 - część druga, o SCPI na diagramach LabVIEW

Sygnał

DTMF czyli dual-tone multi-frequency jest od lat standardem wybierania numerów telefonicznych, czy to w sieciach mobilnych czy stacjonarnych. Na wybieraniu docelowego numeru oczywiście zastosowania się nie kończą – skoro w urządzeniu jest już koder (generator) DTMF, to mając po drugiej stronie odbiornik-dekoder możemy zapewnić sobie proste sterowanie poprzez wcześniej zestawione łącze, cały wic w tym aby odpowiednio rozpoznać kombinację rzeczonych tonów.

O DTMF napisano już bardzo wiele, powielanie dostępnych treści jest więc bez sensu, wskażę tylko punkt zaczepienia
DTMF, Wybieranie tonowe (Wiki) a resztę detali proszę sobie doczytać.

Do generowania testowych kombinacji dwóch częstotliwości wykorzystałam generatory pokładowe Analog Discovery 2, pierwsze naiwne podejście jakby to wyglądało na DSO przedstawia filmik:



Aby zapewnić sobie powtarzalność zarówno nastaw jak i ich sekwencji wykorzystałam Qt Script dostępny w WaveForms, programik znajdziemy poniżej:

 ad2dtmf.dwf3work

function doGen() {
    const freqPairs = [
        [697, 1209, "1"], [697, 1336, "2"], [697, 1477, "3"], [770, 1209, "4"],
        [770, 1336, "5"], [770, 1477, "6"], [852, 1209, "7"], [852, 1336, "8"],
        [852, 1477, "9"], [941, 1336, "0"], [697, 1633, "A"], [770, 1633, "B"],
        [852, 1633, "C"], [941, 1633, "D"], [941, 1209, "*"], [941,1477, "#"]
    ];

    Wavegen1.Channel1.Mode.text = "Simple";
    Wavegen1.Channel2.Mode.text = "Simple";

    Wavegen1.run();

    while ( 1 ) {
        for (var i = 0; i < freqPairs.length; i++ ) {
            print( "klawisz [" + freqPairs[i][2] + "] -> " + freqPairs[i][0] + ":" + freqPairs[i][1] );
            Wavegen1.Channel1.Simple.Frequency.value = freqPairs[i][0];
            Wavegen1.Channel2.Simple.Frequency.value = freqPairs[i][1];
            Wavegen1.Channel1.Simple.Amplitude.value = 0.3;
            Wavegen1.Channel2.Simple.Amplitude.value = 0.3;
            wait ( 0.4 ); // czas tonu
            Wavegen1.Channel1.Simple.Amplitude.value = 0;
            Wavegen1.Channel2.Simple.Amplitude.value = 0;
            print( "----------------");
            wait ( 0.4 ); // czas ciszy
        }
    }
}

if ( !('Wavegen1' in this) ) {
    throw("no wavegen!");
}

doGen();


Do poprawnego działania programikowi potrzeba otwartego okna WaveForm Generator, co jest skrzętnie sprawdzane na starcie skryptu. Dalej, w nieskończonej pętli wybierane są kolejne elementy tablicy zawierające tony do odegrania, a także odpowiadające im litery klawiatury w celu wizualizacji na ekranie. Czas trwania pary tonów określa pierwszy z widocznych wait(), drugi to odstęp pomiędzy sygnałami (cisza). Sprawa wartości amplitudy jest umowna. Ja podłączyłam sobie dodatkowo słuchawkę W66, więc aby nie piskać wieczorem po całym domu, ustawione jest na ~0.3V. Wartości par częstotliwości przepisane żywcem z Wikipedii, włącznie z obsługą ‘nietelefonicznych’ guzików A-D.

  
  

Uruchomiony skrypt ćwierkający sobie w kółko tony z tabelki zostawiamy w oknie WaveForms, o mniej więcej tak:



Całość na żywo możemy podziwiać na filmiku, a następnie zabieramy się za LabVIEW.



☘☘
Detekcja

Koncepcyjnie dekodowanie DTMF nie jest jakoś specjalnie skomplikowane. Należy pobrać sygnał analogowy do bufora, zakładając niemo, że to przebieg okresowy (suma dwóch sin() o równych amplitudach i odmiennych pulsacjach) napuścić na niego coś w rodzaju transformaty Fouriera, obliczywszy dwie dominujące w sygnale częstotliwości składowe ich wartości sprawdzamy w/g wzorcowej tabelki, znaleziony kod (o ile) należy wyświetlić na ekranie lub zrobić na jego podstawie coś równie mądrego. Znakomita większość materiałów dotyczących dekodowania DTMF w LabVIEW zakłada wykorzystanie klocków 'Signal Analysis Express Vis' lub bezpośrednio bloczka FFT z paletki 'Transforms VI', więc powielanie tego uznałam za zwyczajnie nudne. Przeglądając paletkę 'Waveform Measurements VI' znajdziemy za to inny ciekawy bloczek Extract Multiple Tone Information VI. W środku i tak zawiera on wywołanie FFT, ale z takim interfejsem jaki przygotowano – jest bardzo wygodny w użyciu i jak dalej zobaczymy liczy bardzo zwinnie. I dlatego też rozwiązanie demonstracyjne bazuje na tym właśnie komponencie.

Diagram i panel prostej aplikacji rigol-scpi-dtmf-1.vi przedstawiają rysunki poniżej.





Oczywiści kilka zdań komentarza co za czym idzie, a więc: aby wydobyć z oscyloskopu nasz przebieg należy skonfigurować wyjście w tryb NORMAL (dane z ekraniku, 1200 wartości) i dla wygody format danych na ASCII (tą decyzją strzelamy sobie właśnie w kolano, o czym później). W międzyczasie należy przygotować dwie tablice z częstotliwościami do rozpoznawania, dla rzędów i kolumn telefonicznej klawiatury. Ponieważ oczekiwanie że klocek obliczający FFT i wyszukujący ton zwróci nam dokładnie takie wartości jak nadajemy jest naiwnością – na wzorcowe częstotliwości nałożyłam pewną tolerancję, kilka %. Oznacza to, że przykładowo ton 697Hz to legalny zakres wejściowych częstotliwości w granicach 669-724 Hz. Tak więc finalnie powstają dwie tablice dla dwóch serii częstotliwości zawierające akceptowalne wartości minimalną i maksymalną. Dodatkowo, na boku budujemy matrycę z kodami klawiszy, przyda się do wyświetlania tego, co ewentualnie uda się nam zdekodować.

W nieskończonej pętli głównej wykonujemy co następuje: pobieramy z Cukierka aktualną nastawę podstawy czasu (to na okazję gdyby ktoś pokręcił gałką na przedzie oscyloskopu i złośliwie zmienił), następnie wyciągamy z DSO informacje o przebiegu, znane już 1200 próbek. Bufor z próbkami podajemy bezpośrednio na wejście bloczka 'Extract Multiple Tone Information', prosimy o wyszukanie dwóch dominujących wartości częstotliwości, przy czym ich amplitudy muszą mieć minimalny oczekiwany przez nas poziom (treshold). Bloczek po krótkiej chwili zwraca nam tablicę informacji o znalezionych tonach – to takie małe strukturki opisujące częstotliwość, amplitudę i fazę rozpoznanego sygnału, nas interesuje f. Nasz bloczek nic nie wie o fizycznych cechach sygnału, obliczenia dokonuje w dziedzinie numerów próbek, podobnie jak znane z drugiej części komponenty do analizy czasowej. My potrzebujemy wartości w Hz zatem przeliczamy wyniki z komponentu w kontekście pozyskanej wcześniej podstawy czasu. Taką parę dwóch zagadkowych częstotliwości podajemy na dwie niezależnie biegnące pętelki, które pracowicie sprawdzą we wzorcowych tabelach min-max czy czasami nie podajemy znajomej wartości w obrębie analizowanego przedziału. W przypadku znalezienia – pętelka jest przerywana zwracając przy okazji indeks znalezionej częstotliwości. Aby nastąpiła detekcja DTMF obie pętle muszą coś znaleźć – stąd iloczyn logiczny wyników ich poszukiwań – wypracowane indeksy częstotliwości wskażą nam rząd i kolumnę kodu klawiaturowego, które w formie napisów pojawią się w polu tekstowym panelu frontowego. Ot i cała filozofia.

Działanie tego programiku widzimy na krótkim filmie:



Dalsza rozbudowa testowej aplikacji to dołożenie animowanej 'klawiatury' – to matryca kontrolek binarnych typu Push Button, aby ułatwić sobie trafianie sygnałem wciskającym guziki – powstała dodatkowa (bliźniacza do tej z kodami klawiszowymi) macierz, jej (szesnastkowe) elementy określają bitowy stan aktywnych klawiszy ekranowej klawiatury. Diagram aplikacji rigol-scpi-dtmf-2.vi widzimy poniżej:



a działającą i radośnie popiskującą instalację zobaczymy na filmie:



☘☘☘
Modularyzacja

Dołożenie klawiaturki skutkowało pojawieniem się kolejnego stada komponentów na diagramie, dalsze modyfikacje w tym gąszczu przewodów i różnorakich klocków zaczynają się robić cokolwiek upierdliwe. Nadeszła zatem sposobność na to, aby aplikację nieco zmodularyzować, może nie tyle w celu ponownego wykorzystania jakichś bloków (choć to po części też) ale głównie po to, aby zamknąć pewne obszary funkcjonalne w osobnych diagramach celem oszczędności miejsca na głównym schemacie. I tak na oddzielnych diagramach wylądowały po kolei:

  • Konfiguracja landrynkowego Rigola – timeout sesji, formaty danych, etc rigol-scpi-dtmf-init(SubVI).vi



  • Budowanie tabeli częstotliwości wzorcowych min-max: rigol-scpi-dtmf-build-freq-minmax-array(SubVI).vi



  • Odczyt danych o podstawie czasu, danych przebiegu oraz konwersja danych z DSO na wartości numeryczne rigol-scpi-dtmf-get-osc-data(SubVI).vi



  • Wyszukiwanie zadanej częstotliwości w tabelce min-max rigol-scpi-dtmf-find-matching-freq(SubVI).vi



    No i oczywiście schemat główny rigol-scpi-dtmf-3.vi po porządkach:






    Dygresja w formie osobistego punktu widzenia:

    Hmm, ktoś może odnieść (skądinąd słuszne) wrażenie, że wcale tego majdanu na diagramie nie ubyło i dalej jest rozstrzelony, a niektóre druciki poukładane są co najmniej nieestetycznie. No i tu dochodzimy do ciemnych stron graficznego programowania i konsekwencji używania opcji automatycznego porządkowania diagramu czyli CTRL+U – 'Clean Up Diagram'.
    Z perspektywy tych kilkunastu miesięcy mego dłubania w LabVIEW powiem tak: ładne układanie na ekranie tego wszystkiego, dbanie o przejrzystość schematu, o to aby podczas rysowania zachować kierunkowość przepływu danych – to wszystko jest zapewne godne pochwały, normalnie brawa za upór i chęci tylko ... bardzo szybko dojdziemy do wniosku, że zbyt wiele czasu i energii poświęcamy na dopieszczenie wizualnych aspektów diagramu i zabawę w kolorowe układanki zamiast skupić się nad merytoryką aplikacji, nad logiką jej działania. Oczywiście byłoby super, gdyby schemat był równomiernie rozłożony, bez zbędnych przecięć magistral z pogrupowanymi obszarowo elementami, zgodnie z funkcjami które pełnią. Tylko że w praktyce, na dłuższą metę za cholerę tak się nie da. Efektywne modyfikacje diagramu to swego rodzaju łatki in-place, zaraz po zmianach wyglądające dość szpetnie. Następnie na całości ja przynajmniej wykonuje ulubione przez niektórych, a przeklęte przez innych 'Clean Up Diagram'. To powoduje automatycznie przerysowanie schematu zgodnie z zasadami przyjętymi w konfiguracji naszego środowiska (Tools-Options-Block Diagram). Te zasady automatycznej dystrybucji elementów schematu możemy za to dostosować do swoich preferencji, jeżeli pracujemy w większej ekipie to zapewne przejmiemy choćby częściowo przyzwyczajenia innych ze stada lub zaakceptujemy lokalne standardy.
    No i żarcik - schemat po tym zabiegu wygląda czasem zupełnie inaczej niż ten, co pracowicie wydziergaliśmy!
    W sensie strukturalnym cały graf przepływu danych pozostaje oczywiście niezmieniony, ale wizualnie - jest inaczej, niekoniecznie dla nas czytelniej. Zatem cały wic w tym wszystkim taki, że zamiast tracić czas na 'ładne pisanie' to warto go według mnie poświęcić na naukę 'szybkiego czytania' diagramów, na trening w błyskawicznej analizie co do czego na schemacie idzie, z czego wynika, jakie są zależności pomiędzy widocznymi sygnałami i jaka ich rola w większej całości. Gdy w tym nabierzemy wprawy, będziemy w stanie mentalnie ogarnąć dowolnie fikuśnie (lub niechlujnie, bo różnie bywa) narysowany diagram w LabVIEW i to po dowolnej ilości cleanup-ów.


    Wracając do naszej klawiatury – na koniec akapitu filmik:



    No i oczywiście kilka uwag krytycznych – wprawne oko zauważy, że poniżej pewnych wartości czasu trwania tonu (pierwszy wait() w skrypcie AD2) aplikacja nie była w stanie rozpoznać kodu klawisza, programik pomijał niektóre sygnały zajęty albo obliczeniami albo pobieraniem danych z DSO. Zatem dalsza część będzie dotyczyła pewnych zabiegów optymalizacyjnych, aby zapewnić responsywność (co za słowo) naszego programu na jakimś akceptowalnym poziomie.

    ☘☘☘☘
    Optymalizacja

    Będzie Faster, ponieważ taki tytuł ma świetny utwór z albumu ‘The Unforgiving’ grupy Within Temptation, ale to tak na marginesie, wracamy do LabVIEW. Kolokwialnie ujęty truizm w postaci 'skoro gubi kody, to pewnikiem aplikację gdzieś zamula' nabierze głębszego sensu, gdy zbadamy ile czasu LV spędza na poszczególnych etapach wykonywania diagramu. Metoda na uzyskanie oglądu na całą sytuację jest prosta, niemniej jednak odrobinę pracochłonna i inwazyjna, a polega na takiej przebudowie schematu aby móc dokonać pomiarów czasu. Może na początek zaprezentuję trywialny przykład, będzie łatwiej zrozumieć ideę tych zabiegów.



    Naszkicujmy diagram flat-sequence-timing-1.vi jak powyżej – w głównej pętli widzimy Flat Sequence z kilkoma slajdami, w nich na przemian pobieranie bieżących milisekund oraz programowe opóźnienie o zdefiniowany czas. Wartości czasu pozyskane z kolejnych slajdów i odjęte od siebie (Tn+1-Tn) dadzą liczbę milisekund jaką proces LV spędził w danej klatce Flat Sequence. Na podobnej zasadzie możemy pozyskać informację o czasie trwania jednego obiegu pętli, po prostu odejmujemy od siebie kolejno bieżący i ostatnio zapamiętany znacznik czasu.

    Filmik z działania tej prostej demonstracji poniżej:



    Proszę zwrócić uwagę, że liczby milisekund wyliczone dla kolejnych slajdów zgadzają się z wartościami sztucznych opóźnień, ale tego akurat należało się spodziewać. Zauważmy także, jakie wartości wynikają z pomiaru czasu trwania jednej iteracji. Do chwili, gdy w pętli nie ma bloczka Wait nic ciekawego się nie dzieje suma opóźnień determinuje całkowity czas obiegu pętli. Gdy wprowadzimy ogólny dla pętli bloczek opóźniający powstaje ciekawostka:

  • dla wartości mniejszych od sumarycznego czasu wykonania całej zawartości Flat Sequence czas obiegu pętli wyznacza sekwencja
  • dla wartości większych od czasu z Flat Sequence czas jednej iteracji zależy od podanego w Wait opóźnienia

    Wniosek z tego taki (o czym pisałam drzewiej w EdW, w pierwszym wypracowaniu-zajawce od LabVIEW) – pewne elementy diagramu wykonywane są równolegle, w szczególności te, które nie są związane żadnymi zależnościami. Tak jest w przypadku Flat Sequence i luźnego bloczka Wait, ich czas biegnie równolegle. Jeden skończy się wcześniej, drugi pewnie później, który pierwszy? To zależy od ustawionych wartości opóźnienia.
    Drugi wniosek taki, że jeżeli chcemy twardo wymusić określoną kolejność działań bez ryzyka, że LV w nadgorliwości swej wykona zrównoleglone akcje – stosujemy właśnie strukturę Flat Sequence, elementy schematu umieszczając w kolejnych slajdach. Zyskujemy wtenczas pełną kontrolę nad sekwencją poleceń i kolejnością ich wykonania, tracimy natomiast wszelkie zyski z optymalizacji wykorzystania czasu CPU i obliczeń równoległych. To poważna konsekwencja i Flat Sequence należy używać z rozwagą.

    Mając już wiedzę jak pozyskać dane o czasowym koszcie wykonania elementów diagramu – przerabiamy nasza zabawkę-dekoder DTMF. Wrócę może do wersji sprzed modularyzacji – akurat to spaghetti będzie wygodniejsze to poprzekładania w kolejne klatki Flat Sequence, proszę oto rigol-scpi-dtmf-11-time.vi



    Oczywiście filmik z działania aplikacji z zegarkiem w dłoni:



    I już z pierwszego uruchomienia widać, że główny szkodnik i konsument cennego czasu to ... odbiór danych o przebiegu z bufora DSO. No i słusznie. Idąc na łatwiznę przyjęłam format ASCII co oznacza konieczność przesłania do LV 1200 liczb zmiennoprzecinkowych zapisanych w notacji naukowej XE±Y, nie licząc przecinków. To mimo wszystko spora piguła danych, przepchnięcie tego przez sieć odrobinę czasu zajmuje, w modelowym przypadku nieco ponad 200ms. Ale to automatycznie oznacza, że nie będziemy w stanie wykonać więcej niż pięć analiz tonu na sekundę!

    Zatem pierwsza zmiana optymalizacyjna to rezygnacja z formatu ASCII na rzecz BYTE. W uzasadnieniu: w sumie to nie interesują mnie konkretne wartości amplitudy w kontekście ustawień sondy i wzmocnienia, do analizy czasowej z powodzeniem wystarczą ośmiobitowe (bo tyle ma Rigol) surowe wartości z przetwornika A/C. Wprawdzie wartości U8 (0x00-0xFF) należy sprowadzić do formatu 8-bit ze znakiem, ale to robimy jedną szybką pętelką.

    Druga zmiana to przeniesienie odczytu ustawień podstawy czasu przed pętlę główną – będzie to robione raz, na start aplikacji. To wprawdzie rezygnacja z bajeru, że aplikacja sama się ogarnie po zmianie czasówki w oscyloskopie – ale skoro walczymy o milisekundy to także odpowiednio ustalajmy priorytety. Obsługa pobrania podstawy czasu to kolejne kilka cennych milisekund, niwelujemy zatem i ten narzut.

    Po wspomnianych przeróbkach nowa wersja aplikacji rigol-scpi-dtmf-111-time.vi szczyci się diagramem jak poniżej:



    No i teraz proszę bardzo, takie oto efekty poprawek architektonicznych widać na filmie:



    Całość zagadnienia - pozyskanie danych oraz ich numeryczna analiza mieści się w średnio 20-30ms, oznacza to teoretycznie możliwość rozpoznawania kilkunastu tonów na sekundę, co jest w przypadku tak na kolanie nabazgranej aplikacji wynikiem chyba całkiem niezłym.

    #slowanawiatr, październik 2018

  • Natasza Biecek 2004-2018/~, e-mail