lwIP - odbieranie danych przez TCP

Chciałem ostatnio popchnąć trochę do przodu jeden ze swoich poprzednich projektów - sprzętowe radio internetowe o którym pisałem już wcześniej, tylko tym razem w wersji ze zaktualizowaną częścią hardware'ową. Poprzednia wersja była tworzona na PIC32, teraz powoli chciałem przenieść go na STM32.

Większość softu właściwie już przeniosłem, teraz zostało najważniejsze - przeportowanie samej aplikacji odpowiedzialnej za odtwarzanie streamu z Internetu. W przypadku biblioteki MLA na PIC32 było to relatywnie proste. Socket sieciowy dysponuje buforem FIFO o zdefiniowanej pojemności - do niego trafiają dane przychodzące z serwera. Dane te pobieram i ładuję do bufora audio. Robię to jednak dopiero wtedy, gdy sterownik układu VS1003 stwierdzi, że dane są potrzebne.

W przypadku PIC32 było to relatywnie proste. Miałem kilka funkcji:

- TCPIsGetReady() - zwracała liczbę bajtów w buforze

- TCPGetArray() - funkcja zapisywała pod podany adres w pamięci określoną maksymalną liczbę bajtów z bufora. Zwracała liczbę bajtów, które w rzeczywistości udało się pobrać.

Sprawa była prosta - wystarczyło albo pobrać wszystkie dostępne dane, ale (jeśli było ich za dużo) tylko tyle, żeby wypełnić dostępne miejsce. W tym drugim przypadku nadwyżka pozostawała w buforze gniazda sieciowego i była sukcesywnie uzupełniania o kolejne przychodzące dane, które mogłem pobrać wtedy, gdy znów były potrzebne.

Widzę, że w przypadku lwIP (RAW API) sprawa nie jest już tak prosta. Zamiast tego muszę zarejestrować callback, który jest wołany za każdym razem, gdy przyjdą nowe dane. Callback otrzymuje w jednym z parametrów wskaźnik do struct ptr, w której mam m.in.

- void* paylod

- int len

- int tot_len

- struct pbuf* next

Istnieje więc możliwość, że wszystko co będę musiał zrobić to pobranie skopiowanie len bajtów spod adresu na który wskazuje payload. Istnieje jednak szansa, że danych jest więcej - wtedy tot_len > len i kolejnej porcji danych trzeba szukać w kolejnej strukturze, na którą wskazuje wskaźnik next.

Jeśli już zakończymy odczytywać dane, trzeba zawołać tcp_recved informując stos, że czekamy na kolejną paczkę. Tu jeszcze jest wszystko jasne.

Co jednak w sytuacji, gdy powiedzmy do zakończenia wypełniania bufora pozostało mi 100 bajtów, a w otrzymanej struct pbuf mam ich 500? Na PIC32 po prostu pobierałem 100, a reszta czekała na swoją kolej. W jaki sposób uzyskuje się podobny efekt na lwIP?

Reply to
Atlantis
Loading thread data ...

Atlantis snipped-for-privacy@wp.pl napisał(a):

Użyć socketów zamiast męczyć się z RAW API?

Reply to
Grzegorz Niemirowski

Sockety w lwIP chyba wymagają RTOS-a. Tak to jest zrealizowane na ESP8266/ESP32 i tam faktycznie nie ma problemu z odbieraniem danych. Tutaj jednak chciałem to zrobić bezpośrednio na krzemie, więc jestem skazany na RAW API.

Reply to
Atlantis

Ale „kernel” np. FreeRTOS-a zajmuje tyle co nic, jeśli mowa o współczesnych MCU. A idę o zakład, że bardzo ułatwiłby pisanie aplikacji o nazwie „radio internetowe”.

BTW:

formatting link

Reply to
JDX

Tyle tylko, że ja już mam działający kod, napisany pod PIC32. Wymaga co prawda jeszcze paru ulepszeń i poprawek, ale działa. Kod jest napisany z myślą o działaniu bezpośrednio pod krzemem - opiera się na maszynie stanów skończonych, zdarzeniach i callbackach. Większość jego funkcjonalności już udało mi się przenieść pod STM32. To co zostało, to uruchomienie funkcji samego odbierania streamów z sieci. Tutaj też już mam jakieś 90% wykonanej pracy - na chwilę obecną prawidłowo łączy się z serwerem oraz pobiera i parsuje nagłówki HTTP. Jedyne czego potrzebuje do właśnie rozwiązanie kwestii wyjmowania z bufora odbiorczego tylko tylu bajtów, ile potrzeba (w zależności od miejsca w buforze) i zostawienie reszty na później.

Chcąc przenosić to na RTOS musiałbym część pracy zaczynać od nowa. Tak - samo włączenie FreeRTOS-a to kwestia kilku kliknięć w STM32CubeMX, ale potem jeszcze trzeba by dostosować inne części projektu do pracy pod tym systemem.

No i jednak o ile sam system nie zajmuje dużo miejsca we flashu, to już jednak powoduje zwiększenie zapotrzebowania na RAM.

Reply to
Atlantis

Atlantis snipped-for-privacy@wp.pl napisał(a):

Nie znam Twojego projektu, ale tego nie powinno być dużo. I tak mniej niż użerać się z RAW API.

FreeRTOS także RAM-u nie potrzebuje dużo.

Reply to
Grzegorz Niemirowski

Nie uzywalem lwIP ale zerknelem do dokumentacji. Z dokumantacji lwIP wychodzi ze to ma byc "zero copy". Czyli trzymasz buforki tak dlugo jak sa potrzebne, a potem zwalniasz. Jak cos zostalo to sobie zapamietujesz polozenie bufora i uzywasz przy kolejnym callbacku. tcp_recved _nie_ zwalnia buforow, wyglada ze musisz to robic oddzielnie przez pbuf_free.

Reply to
antispam

Ok, czyli chyba faktycznie najprościej będzie posłużyć się dodatkowym buforem pomocniczym, w którym trzymałbym nadwyżkę. Na dobrą sprawę można by nawet alokować te dane dynamicznie, na stercie. Wtedy po prostu przed zapisaniem kolejnej paczki sprawdzałoby się czy wskaźnik do bufora pomocniczego ma wartość inną niż NULL, zapisywało te dane do głównego bufora, zwalniało pomocniczy i przypisywało NULL do wskaźnika. Dopiero wtedy go głównego bufora szłaby paczka nowych danych, a jeśli coś by zostało, to nadwyżka trafiałaby do zaalokowanego na nowo bufora dynamicznego. To chyba jednak bardziej eleganckie rozwiązanie niż opóźnianie zwalniania całej struktury pbuf do kolejnego callbacka.

Po prostu miałem nadzieję, że można to zrobić bez takich kombinacji, jedynie pobierając potrzebny w danej chwili kawałek bufora, jak w przypadku MLA na PIC32.

Reply to
Atlantis

W sumie mam jeszcze inny pomysł. W układzie mam układ pamięci RAM na SPI. W późniejszej wersji softu planowałem go wykorzystać w roli nieco większego bufora audio, jednak chyba wykorzystam go jako bufor cykliczny, pośredniczący w komunikacji pomiędzy siecią a dekoderem MP3. W callbackach lwIP będę po prostu ładował kolejne przychodzące dane do kolejnych adresów w tym układzie, a maszyna stanów obsługująca odtwarzanie będzie je sobie pobierała, uzupełniając swój własny, mniejszy bufor w pamięci mikrokontrolera. To powinno mi pozwolić na rozwiązanie kwestii przekazywania danych w sposób asynchroniczny.

Reply to
Atlantis

A tak swoja drogą - te radia działają na TCP ? Bo chyba powinny na UDP ...

Kolejna sprawa - jak te radia działają? Dawniej podawali jakies parametry do bezposredniej transmisji, dzis wszystko pochowane. Wyswietl nasza strone www.

A radia (te niby "sprzetowe") jakos działają ... maja jakies serwery z listami parametrów ?

Na moj gust, gdzies musisz sobie założyc bufor odpowiedniej dlugosci, wpychac tam dane które przyszly, i potwierdzac otrzymanie, lub poczekac jak sie bufor konczy ... aby zatrzymac transmisje.

J.

Reply to
J.F
2022-09-27 o 15:35 +0200, J.F napisał:

Z reguły jednak TCP. Co gorsze - nierzadko z dodatkową warstwą TLS. Przykłady streamów:

https://31.192.216.10/RMF2

formatting link

Da się znaleźć, np. tu:

formatting link
W domu mały NAS Linuksowy z VLC robi za radio, podaję mu URLe skopiowane właśnie z powyższej strony.

Mateusz

Reply to
Mateusz Viste

Wszysto idzie po TCP. UDP jest cięzkie do implementacji po stronie klienta bo trzeba ogarnąć:

- jitter

- packet loss

- out of order packets

Oczywiście da się to ogarnąc bo tak działa cały VoIP... Myślę że łatwość implementacji klienta tutaj wygrała - większość streamów to TCP (HTTP)

Reply to
Cezar

Działają na TCP, a konkretnie na zwyczajnym protokole HTTP. Otwierasz połączenie z serwerem i wysyłasz GET-a, prosząc o udostępnienie konkretnego streamu. W odpowiedzi dostajesz odpowiedź i nagłówki. Jeśli jest to 200 OK, możesz zacząć odbierać i dekodować przychodzące dane audio. Oczywiście trzeba też obsłużyć inne sytuacje, jak np. przekierowanie pod inny adres.

Po co? Strumień audio nie potrzebuje niskiej latencji - wszystko i tak jest buforowane po stronie serwera. Można się o tym dość łatwo przekonać włączając jednocześnie dowolne radio FM i jego sieciowy odpowiednik, albo jeszcze lepiej - Jedynkę na falach długich i tę samą stację nadawaną on-line.

Niektóre stacje nadal podają bezpośrednie URL-e do swoich streamów. Jest też trochę różnych zestawień w Internecie. W przypadku stacji narzucających własną aplikację na stronie i tak bardzo często pod spodem mamy zwyczajne połączenie HTTP - wtedy wystarczy wyłuskać adres analizując ruch, np. za pomocą narzędzi deweloperskich Chrome'a.

Tak. I to jest właśnie głównym powodem dla którego zabrałem się za ten projekt. Wkurzał mnie fakt, że większość dostępnych komercyjnie rozwiązań będzie działała tak długo, jak długo producent raczy utrzymywać infrastrukturę. Tutaj sam wpisuję URL-e stacji, które chcę słuchać.

No właśnie tutaj jak na razie rozbijam się o szczegóły. W przypadku stosu MLA z PIC32 po prostu pobierałem sobie z bufora odbiorczego tyle danych, ile w danym momencie potrzebowałem. Stos robił pod spodem całą resztę - jeśli w buforze było miejsce, prosił serwer o więcej danych. Efekt był taki, że dekoder MP3 z automatu narzucał prędkość transmisji i nawet przy stosunkowo niewielkich buforach wszystko działało płynnie.

Tutaj sprawa jest trochę bardziej skomplikowana. Dane nie są buforowane w taki sposób i nie mogę po prostu pobrać dowolnej ilości w dowolnym momencie. Jeśli zostanie wywołany callback odbiorczy, mam obowiązek odebrać całą paczkę, zwolnić pamięć i wywołać tcp_recved. Jednak z tego co rozumiem ta ostatnia funkcja jedynie zwiększa rozmiar okna odbiorczego, więc nawet pomimo braku jej wywołania nie mam gwarancji, że jeszcze przez chwilę nie przyjdą kolejne callbacki - być może uda mi się uzyskać taki efekt odpowiednio manipulując ustawieniami stosu.

Przerobiłem wczoraj nieco swoją aplikację, implementując bufor cykliczny do którego trafiają dane ze strumienia. Serwer jednak wysyła je nieco szybciej niż wynikałoby to z bitrate'u strumienia audio - ma to sens, bo nadmiarowe dane w normalnych warunkach mogą zostać użyte do uzupełniania bufora. Tak więc jeśli po prostu odbieram dane, wrzucam je do bufora i potwierdzam otrzymanie, to dość szybko dochodzi do przepełnienia bufora

- dźwięk jest, ale postrzępiony. Próbowałem wywoływać tcp_recved() w innym miejscu dopiero wtedy, gdy w buforze była dostatecznie dużo wolnego miejsca, jednak jak na razie nie udało mi się uzyskać poprawnego działania. Będę jeszcze eksperymentował.

Eksperymenty z zewnętrzną pamięcią SPI RAM 128 kB jak na razie też nie przyniosły idealnych rezultatów. Nawet przy maksymalnej dostępnej prędkości (18 MHz) wydaje się ona zwyczajnie zbyt wolna. Trochę mnie to zaskoczyło, bo karta SD przy dwa razy wolniejszej transmisji spokojnie wyrabiała z dostarczaniem danych z pliku MP3. Z drugiej strony jednak tutaj mam operacje odczytu i zapisu, występujące naprzemiennie w krótkich odstępach czasu.

Zaczynam zastanawiać się czy faktycznie dobrym pomysłem nie będzie zastosowanie FreeRTOS-a i socketów. Nie wiem tylko czy problemem nie będzie niewielka ilość RAM-u mikrokontrolerze (64kB).

Reply to
Atlantis

Moje pierwsze podejście do tematu autonomicznego radia internetowego polegało właśnie na wykorzystaniu Raspberry Pi (Zero) z zewnętrznym DAC-iem i standardowym oprogramowaniem do odtwarzania audio (MPD) pod spodem. Otwierało to sporo dodatkowych możliwości, jednak trochę absurdalny wydawał mi się fakt, że urządzenie musi najpierw załadować i uruchomić cały system operacyjny, żeby zacząć odbierać stację. W celach edukacyjnych postanowiłem więc złożyć wersję na mikrokontrolerze. ;)

Reply to
Atlantis

Chwalebnie... i ambitnie - bo trzeba samemu obsłużyć TCP, HTTP, kilka popularnych kodeków, TLS... sporo warstw do przebicia. Niemniej doceniam i rozumiem myśl. Też mnie denerwuje, że po włączeniu "radia" muszę czekać niemal minutę na dźwięk.

Mateusz

Reply to
Mateusz Viste

Od tego jest stos. Generalnie projekt zaczynałem jeszcze na PIC24, potem przeniosłem go na PIC32. W obydwu przypadkach korzystałem już z nieco leciwych, ale całkiem fajnych bibliotek MLA (w ostatnich latach Microchip przestał je wspierać, zastępując środowiskiem Harmony). Ich obsługa jest bardzo prosta, trzeba tylko potrafić napisać odpowiednią maszynę stanów, ale taki sposób programowania mikrokontrolerów jest dla mnie najbardziej naturalny.

Tak naprawdę nie potrzeba pełnej obsługi HTTP. Inicjując połączenie za każdym razem wysyłamy takiego samego GET-a, z kilkoma podmienionymi polami (adres serwera i lokalizacja streama na serwerze). Dostajemy odpowiedź, którą trzeba przeparsować. Tutaj też nie ma konieczności, żeby implementować jakąś pełną bibliotekę do obsługi klienta HTTP. Wystarczy uwzględnić kilka odpowiedzi. Najważniejsze to 200 OK oraz przekierowanie pod inny adres. Wszystkie inne można uznać za błąd oznaczający konieczność zamknięcia połączenia (i ewentualnie podjęcia kolejnej próby).

To w całości załatwia sprzętowy dekoder VS10xx. W przypadku software'owego podejścia de facto można by się ograniczyć do MP3 (zdecydowana większość stacji nadaje właśnie w tym formacie) i tutaj sprawę załatwia popularna biblioteka libmad. Większość współczesnych

32bitowych mikrokontrolerów powinna ją udźwignąć (u mnie nie miało z tym najmniejszych problemów Raspberry Pi Pico) jednak w przypadku tego projektu nie chciało mi się z tym bawić. Bibliotekę najwygodniej obsługuje się za pomocą wysokopoziomowego API, które wymaga RTOS-a (to znaczy nie wymaga, ale bez niego będzie nam blokowało pętlę główną) a niskopoziomowego jeszcze nie rozgryzłem.

To jest właśnie plan na dalszą rozbudowę projektu. W wersji na PIC32 było to trochę kłopotliwe, bo biblioteka TLS z MLA jest dość leciwa. Da się ponoć wykorzystać wolfSSL-a, jednak jeszcze się w to nie wgryzłem. W przypadku STM32 dodanie tej funkcjonalności ogranicza się do wyklikania MBEDTLS-a w STM32CubeMX. To był jeden z powodów dla których w ogóle zacząłem eksperymenty z przeniesieniem projektu na STM32. Niestety nie wziąłem pod uwagę tego, że obsługa stosu będzie bardziej problematyczna. Niezbyt przemyślany był także wybór układu STM32F107, który ma stosunkowo mało RAM-u i nie ma nawet parametrów pozwalających na uruchomienie na nim MBEDTLS-a. Mam nadzieję, że w serii STM32F4 znajdę coś pinowo kompatybilnego, mającego wszystkie peryferia w tych samych miejscach, co pozwoliłoby mi wykonać łatwą podmianę. ;)

Tak czy inaczej, na szczęście ciągle jeszcze całkiem sporo stacji internetowych nadaje po czystym HTTP. W tym właściwie wszystkie te, na których mi najbardziej zależało.

Ten projekt tak naprawdę i tak jest trochę przesadnie skomplikowany, w celach edukacyjnych. Widziałem już w internecie znacznie prościej robione radia internetowe na ESP32. :)

Reply to
Atlantis

Nie o latencje chodzi, tylko o zaciecia na straconych pakietach.

Owszem, jest opoznienie spore ... ale to serwer czy po stronie klienta buforowanie jest ?

Serwer do kompresji cos musi buforowac, ale dla radia to chyba niewiele - gorzej z video mpeg. No ale telewizja cyfrowa tez kompresuje ..

No tak, to jest argument.

Wyszukac wlasciwy adres na stronie np rmf.fm nie tak latwo, ale lepiej miec mozliwosc wyszukania i wpisania, niz miec martwe radio.

Z drugiej strony - to i serwer powinien narzucac predkosc, bo co ma przyslac, jesli za szybko potwierdzisz ?

No tak sobie ma to sens - bo skad ma wziac tresc do wyslania, jak sie uzbiera tych nadmiarowych danych minuta czy kwadrans?

Co prawda teraz sporo radiostacji z dysku nadaje, ale i tak nie ma za duzego bufora, bo chocby aktualnosci powinny byc aktualne :-)

Sprobuj wiekszy bufor. Gdzies musi byc granica.

No ale to bardziej z ciekawosci, bo jak najbardziej trzeba potwierdac w miare ubywania danych.

Karta SD mogla mies jakis szybszy tryb pracy niz SPI, ale wydaje sie, ze te 18MHz powinno wystarczyc z naddatkiem. W koncu strumien ma rzedu 256kbit/s.

Ok, to ciekawosc by trzeba sprawdzic na innym modelu, z wieksza pamiecią. Ale pomijac wymagania RTOS na pamiec - jesli on potrafi zbuforowac dane, to i Ty powinienes potrafic dostępnymi srodkami ...

J.

Reply to
J.F

Co prawda trochę tutaj spekuluję, jednak wydaje mi się, że w przypadku normalnego serwera HTTP i tak nie bardzo w grę wchodzi zapewnienie prędkości idealnie dopasowanej do bitrate'u. Serwer dysponuje pewna pulą na bieżąco uzupełnianych danych, a klient o nie prosi. Może się zdarzyć, że do klienta dane będą docierały przez pewien czas z prędkością poniżej bitrate'u (bo np. pojawi się konieczność kilkukrotnej retransmisji któregoś pakietu) więc czemu nie miałaby mieć miejsca odwrotna sytuacja

- kiedy klient prosi o udostępnienie danych z pewnym wyprzedzeniem?

Kiedyś podstawiłem obok siebie moje radio internetowe odbierające radiową Jedynkę oraz stare radio AM, nastawione na 225 kHz. Różnica mogła wynosić dobre 10 sekund. Oczywiście nie znaczy to, że całe opóźnienie pochodzi od bufora w serwerze, bo jeszcze częściowo może być wprowadzane na różnych łączach pomiędzy studiem a serwerem. Pomiędzy Jedynką na FM i AM też jest widoczne opóźnienie, chociaż może nie tak znaczne.

Ale to chyba właśnie tak działa w TCP. Po odebraniu pakietu klient informuje serwer, że teraz dysponuje większym oknem odbiorczym i może przyjmować dane. Serwer decyduje ile wyśle - równie dobrze może wysłać mniej niż jest miejsca w oknie.

Wiadomo, że serwer nie wyśle więcej, niż w danej chwili ma.

Ok, trochę pozmieniałem w kodzie i mam kilka nowych obserwacji. Możliwe też, że niektóre z moich wcześniejszych wniosków mogły być błedne... Po pierwsze dodałem odpowiednie zabezpieczenia, chroniące przed nadpisywaniem bufora cyklicznego. Teraz przed każdym zapisem sprawdzana jest ilość wolnego miejsca. Jeśli miejsca jest na tyle, zapis do bufora odbywa się w callbacku odbiorczym. Jeśli miejsca jest za mało, to jedynie zapisywany jest wskaźnik do otrzymywanych danych i pętli program sprawdza czy dane się zwolniło - jeśli tak, dane trafiają do bufora i wywoływana jest funkcja tcp_recved(), zgłaszająca gotowość przyjęcia kolejnych danych. Dodałem także zabezpieczenie przed opóźnieniem bufora - jeśli danych zaczyna być za mało przestają one być pobierane do bufora audio (a więc przestaje on grać) i zamiast tego jedynie przyjmujemy dane z serwera. Odtwarzanie jest wznawiane dopiero po zapełnieniu bufora do pewnej wartości.

Teraz jednak zauważyłem, że dane do bufora przychodzą bardzo wolno. Ilość danych odpowiadająca ułamkowi sekundy odtwarzania nieraz ładuje się przez ładnych kilka sekund, a więc odtwarzanie jest poszatkowane, z wyraźnymi przerwami. Jednak nie zawsze tak jest - parę razy udało mi się trafić na idealny moment, kiedy odtwarzanie było płynne, a ilość danych w buforze oscylowała wokół jednej wartości.

Karta jest właśnie podłączona do standardowego SPI - STM32F107 nie ma nawet SDIO. ;) Początkowo myślałem, że może to być wina niekoniecznie optymalnych funkcji transmisyjnych z biblioteki HAL, ale potem przypomniałem sobie, że dokładnie tak samo była zrealizowana obsługa komunikacji z kartą SD. ;)

Reply to
Atlantis

Takie coś nie występuje w przyrodzie bez jakiejs wymyślnej implementacji (a na pewno nie w HTTP) Klient może tylko spowolnić odbiór (w przypadku TCP) W UDP nawet tego nie może.

Nie wiem jak dochodzi feed do nadajnika AM ale FM to są głównie satelity

Kompresja w telewizji (czy też DAB) to inna bajka. Jest tam sporo danych nadmiarowych do FEC. Tam gdzie używa się UDP (RTP) do transmisji głosu i obrazu też jest używane FEC Przy obrazie to widać że w przypadku utraty pakietów, część obrazu staje się bardziej "zamazana" (H264, H265) a w przypadku głosu, dzwiek ma mniejsze pasmo (Opus) Samo uzycie FEC juz wprowadza dodatkowe (choc niewielkie) opóźnienie.

Często słucham radia internetowego i w zasadzie zauważyłem że jeśli są problemy w transmisji i następują przerwy to po przerwie radio gra dokładnie od momentu kiedy przerwało co by znaczyło że bufor po stronie nadawcy zostaje zwiększony ale prędkość odgrywania radia się nie zwiększa. Ten bufor jest resetowany co jakiś czas, co skutkuje że transmisja nagle przeskakuje o kilka minut do przodu. Głównie zauważalne w samochodzie podczas jazdy. Moim zdaniem UDP np z Opusem o wiele lepiej by zdało egzamin...

c.

Reply to
Cezar

A co powstrzymuje serwer przed podjęciem próby wypchnięcia klientowi swojego całego dostępnego bufora? ;)

Reply to
Atlantis

ElectronDepot website is not affiliated with any of the manufacturers or service providers discussed here. All logos and trade names are the property of their respective owners.