Kanał - ATNEL tech-forum
Wszystkie działy
Najnowsze wątki



Teraz jest 15 sie 2018, o 07:58


Strefa czasowa: UTC + 1


 Menu
Zawartość
 Strona główna
 Forum
 Szukaj
 Zarejestruj
Pomoc
 FAQ
 BBCode FAQ
 Warunki użytkowania
 Polityka prywatności

 Linki
 www.atnel.pl
 mirekk36 - BLOG
 SKLEP - ATNEL

 Szukaj


Zaawansowane

 Polecaj nas...
Prosimy o bezinteresowne polecanie ATNEL tech-forum za pomocą następującego kodu HTML:


 Najaktywniejsi
Nazwa użytkownika Posty
mirekk36 23354
SunRiver 8677
majster 3803
Jaglarz 3176
Krauser 2078

 Nowi użytkownicy
Nazwa użytkownika Dołączył(a)
puhmik1 14 sie
Loq14 13 sie
leszek_1904 13 sie
janekmaj 13 sie
xgrubyx997 13 sie
gbr161 12 sie
mewa 12 sie
webster0 10 sie

 Załączniki
Nazwa
 RFID_2_PROBA
Rozmiar:40.1 KiB
Plik pobrano:5

 wachcio_nawodni...
Rozmiar:153.84 KiB
Plik pobrano:3

 Wi-Fi Clock Mod...
Rozmiar:45.81 KiB
Plik pobrano:18

 termostat
Rozmiar:14.12 KiB
Plik pobrano:12

 mainC
Rozmiar:3.23 KiB
Plik pobrano:2

 mainC
Rozmiar:3.2 KiB
Plik pobrano:9

 wachcio_nawadni...
Rozmiar:632.72 KiB
Plik pobrano:4

 wachcio_nawadni...
Rozmiar:15.13 KiB
Plik pobrano:4

Powitanie
Witamy na forum pomocy technicznej firmy ATNEL. Wystartowaliśmy 10 października 2011 roku.

Najnowsze ogłoszenia/tematy
Najnowsze ogłoszeniaNajpopularniejsze tematyOstatnie tematy
AVR - PC - WiFi - Audio Control - zabawy z C
Efekt stroboskopowy - zbudujmy razem porządny stroboskop ;)
Program SQP-I2Cscan GUI - C#
Moja stacja pogodowa
ATNEL WEBINARS - Lipiec 2018
NADESŁANE PRACE na Turniej rycerski do oceny dla wszystkich
Jak przeportować kod z m32 na m328P
TURNIEJ RYCERSKI lekkie przesunięcie terminu
Jak zrobić OBUDOWĘ - taknio ale profesjonalnie ?
Wakacyjne warsztaty - czyli dzisiaj o lutowaniu ;)
Nie mogę uruchomić Raspberry PI 3 B+
Wykrywanie włączenia urządzenia 230V
Parametry funkcji
Sinus na Atmeg8A
Wakacyjne warsztaty z Yellowbook! - tego jeszcze nie było
I2C Scaner
zapowiedź - być może ciekawego końkursu ? ;)
STM32F410 SPL DAC - odwrócone wartości napięć
ATB MATRIX - problem od kodu nr 10
COG ChipOnGlass - ST7565 - ciekawe wyświetlacze i biblioteka
taśma WS2812B 12V - więcej obwodów
YB 1.11.6 nietypowe zastosowanie nowych timerów programowych
SPRZEDAM ZASILACZ LED 12V/5A
SPRZEDAM WTYK/GNIAZDO DC
SPRZEDAM TAŚMY LED 60W
[Sprzedam] NAS - Dysk sieciowy LinkSys NAS200 + HDD 500 GB
[Sprzedam] Elbert v2 - kurs VHDL forbot.pl - FPGA, Xilinx
Zabezpieczenie czujnika temperatury
Uruchomienie wyświetlacza OLED DEP160128A z kontr. SSD1353
Sterownik i Termostat

Ogłoszenia globalne
Zobacz najnowszy post Program SQP-I2Cscan GUI - C#
Napisane przez: rskup » 21 lip 2018, o 18:02 Ogłoszenie globalne

Obrazek

W ramach Mirkowego konkursu, dotyczącego API dla ATB-USBasp 4.2, powstał program SQP-I2Cscan :roll: napisany w C# z wykorzystaniem biblioteki "LibUsbDotNet C# USB Library", która jest do pobrania ze strony https://sourceforge.net/projects/libusbdotnet/.


Jest to mały i prosty program - jak chodzi o główną funkcjonalność skanowania I2C, to tylko kilkanaście linijek :). Znaczna część kodu to GUI z dodatkami, które powstawały, bo Mirek przeciągał termin zakończenia konkursu ;). Zasadniczo realizuje on tylko jedną praktyczną funkcję - skanuje magistralę I2C wykorzystując ATB-USBasp 4.2. Dla uprzyjemnienia korzystania, program realizuje autodetekcję podłączenia / odłączenia ATB-USBasp oraz graficznie prezentuje postęp oraz wyniki skanowania. Dodatkowo poprzez dostępny suwak można regulować prędkość magistrali I2C.
Wynik skanowania I2C prezentowany jest w tabeli podając adresy znalezionych urządzeń w formacie dziesiętnym i szesnastkowym oraz w konwencjach adresów 7-bitowego i 8-bitowego. Dodatkowo dodawana jest informacja o możliwych typach układów posiadających wykryty adres. Informacja o możliwych urządzeniach pobierana jest z pliku i2c_table.txt, dzięki czemu istnieje możliwość samodzielnej definicji nazw układów prezentowanych po ich wykryciu.

Obrazek

Dzięki temu, że biblioteka LibUsbDotNet dostępna jest zarówno dla Windows jak i Linux, powstała możliwość stworzenia aplikacji poprawnie kompilującej się, bez jakichkolwiek przeróbek, zarówno pod Windows jak i pod Linux (wykorzystując środowisko Mono). Co ciekawe, to ten sam plik wykonywalny SQP-I2Cscan.exe można uruchomić pod Windows jak i pod Linux :) i nawet nie ma znaczenie gdzie go skompilujemy :D.

Cały kod udostępniam na licencja GNU GPL v. 3.0, czyli dostępny jest kod źródłowy z pełnymi prawami do samodzielnej modyfikacji i rozpowszechniania.
Kod programu znajduje się w wątku konkursowym https://forum.atnel.pl/topic20920.html#p209345.
Dla lubiących narzędzia do wersjonowania, to ostateczną wersję wrzuciłem też do publicznego repozytorium https://bitbucket.org/rskup/sqp-i2cscan/.

Skończy z marketingiem i przejdźmy do szczegółów programowania :)

Jako, że nie lubię korzystać kobyły Microsoftu, to do programowania w C# używam środowiska SharpDevelop. Dlatego nie będę opisywał szczegółów tworzenia programu w GUI aplikacji (bo te w środowisku od MS może trochę się różnić) a skupię się tylko kilka kluczowych punktach.
Podstawową rzeczą jest dodanie biblioteki LibUsbDotNet do naszego stworzonego projektu. W moim środowisku wystarczy wejść w menu Project (Projekt) i wybrać Manage Packages... (Zarządzanie Pakietami) i w nim wyszukać w liście proponowanych dodatkowych pakietów LibUsbDotNet, następnie kliknąć Add (Dodaj) i po chwili mamy dodaną tę biblioteką do naszego projektu :D

Niestety przy korzystaniu z biblioteki LibUsbDotNet pojawia się drobny problem. W bibliotece mamy kilka metod do obsługi urządzeń libusb i niestety nie działają one zamiennie jak to chyba teoretycznie miało być :(. Dla niektórych wersja driverów libusb poprawnie działa wywołanie części funkcji a dla innych działają inne :(.
Najprościej jest pod Linuxem, bo tam działają te zbliżone nazwami do natywnych funkcji libusb. Te same funkcje działają pod Windows, ale tylko gdy mamy drivery pobierane bezpośrednio z projektu libusb-win32. Ale już prawie ten sam driver (te same wersje dll-ek) w paczce z podpisem cyfrowym, czyli ten sam co instaluje mkAVRCalculator (driver ten ma dodany dodatkową bibliotekę dedykowaną dla Windows libusbK.dll) wymaga używania funkcji z grupy UsbRegistry. Więc będziemy czasami mieli w kodzie sprawdzenie jaki mamy system :(.

Dla ułatwienia i zwiększenia czytelności podefiniowałem sobie w programie kilka stałych:
- stałe z informacjami identyfikującymi ATB-USBasp
- nazwę pliku w którym przechowywane są opisy urządzeń I2C
- zakres adresów I2C do skanowania
- potrzebne komendy pochodzące z API ATB-USBasp
[syntax=csharp]// ATB-USBasp device description parameters:
const string ATBUSBasp_NAME = "USBasp";
const short ATBUSBasp_PID = 0x05DC;
const short ATBUSBasp_VID = 0x16C0;
const string ATBUSBasp_SERIAL_STRING = "www.atnel.pl";

// I2C devices description file:
const string I2C_DESCRIPTION_FILE = "i2c_table.txt";

// Addresses range to scan:
const byte I2C_7BIT_ADDRESS_MIN = 0x08;
const byte I2C_7BIT_ADDRESS_MAX = 0x77;

// ATB-USBasp LibUSB commands I2C:
const byte USBASP_I2C_SLA_CHECK = 100;
const byte USBASP_FUNC_I2C_BITRATE = 101;[/syntax]


No to zacznijmy coś konkretnego pokodować :D

Na samy początku program, w celu wykrycia czy podpięty jest do komputera ATB-USBasp, wywołujemy funkcję FindATBUSBaspDevice(), która to przegląda wszystkie urządzenia libusb i na podstawie VID i PID oraz nazwy i serial stringa ustala czy jest podpięte właściwe urządzenie. Niestety w Linux tą metodą nie jest zwracana nazwa i serial, więc one są tam nie weryfikowane. Czyli pod Windows wykrycie urządzenia jest z dokładnością do ATB-USBasp a pod Linuxem każda wersja USBasp zostanie uznana za prawidłową (oczywiście jest możliwość dokładnego kolejnego sprawdzenia czy to jest na pewno ATB-USBasp, ale ja już tego nie zrobiłem ;) ):
W przypadku wykrycia ATB-USBasp funkcja oprócz zwrócenia tego (poprzez wartość true), aktualizuje zmienną globalną MyUsbRegistry w której przechowujemy wskazanie na nasze urządzenie.
Czyli przy starcie programu wykonujemy:
[syntax=csharp]ATBUSBasp = FindATBUSBaspDevice();
SetATBUSBaspStatus();[/syntax]Zmienna bool ATBUSBasp przechowuje informację czy mamy podłączone ATB-USBasp, a funkcja SetATBUSBaspStatus() ustawia odpowiednio GUI i informacje prezentowane na nim. Funkcja FindATBUSBaspDevice() realizuje właściwe sprawdzenie istnienia urządzenia ATB-USBasp wygląda następująco:
[syntax=csharp]private static UsbRegistry MyUsbRegistry = null;

private bool FindATBUSBaspDevice()
{
bool Status = false;

UsbRegDeviceList allDevices = UsbDevice.AllDevices;
foreach (UsbRegistry usbRegistry in allDevices)
{
try
{
if ((usbRegistry.Vid == ATBUSBasp_VID) &&
(usbRegistry.Pid == ATBUSBasp_PID) &&
((UsbDevice.IsLinux) ||
((usbRegistry.Name == ATBUSBasp_NAME) &&
(usbRegistry.Device.Info.SerialString == ATBUSBasp_SERIAL_STRING))))
{
MyUsbRegistry = usbRegistry;
Status = true;
}
}
catch {}
}
UsbDevice.Exit();
return Status;
}[/syntax]

Ewentualne dalsze zmiany urządzeń libusb zrzucimy na karb zaimplementowanych w bibliotece procedur DeviceNotifier. Podpinamy naszą funkcję OnDeviceNotifyEvent(), by była wywoływana na zdarzeniach zmian stanów urządzeń:
[syntax=csharp]UsbDeviceNotifier = DeviceNotifier.OpenDeviceNotifier();
UsbDeviceNotifier.OnDeviceNotify += OnDeviceNotifyEvent;
UsbDeviceNotifier.Enabled = true;[/syntax]
Nasz funkcja sprawdza czy zdarzenie dotyczy naszego VID, PID i czy jest typ zdarzenia nas interesujący (pojawienie się urządzenia => DeviceArrival oraz usunięcie urządzenia => DeviceRemoveComplete):
[syntax=csharp]private void OnDeviceNotifyEvent(object sender, DeviceNotifyEventArgs e)
{
if ((e.Device.IdProduct == ATBUSBasp_PID) && (e.Device.IdVendor == ATBUSBasp_VID))
{
switch (e.EventType)
{
case EventType.DeviceArrival:
if (UsbDevice.IsLinux) ATBUSBasp = true;
else ATBUSBasp = FindATBUSBaspDevice();
break;

case EventType.DeviceRemoveComplete:
ATBUSBasp = false;
break;
}
SetATBUSBaspStatus();
}
}[/syntax]
Sprawdzanie w przypadku pojawienia się urządzenia, czy pracujemy pod Linuxem, spowodowane jest tym, że drugie wywołanie funkcji FindATBUSBaspDevice() w tym środowisku powoduje błędne informacje, więc pomijamy to i bazujemy tyko na VID i PID ze zdarzenia.

Mamy więc wykrywanie i informowanie użytkownika (poprzez modyfikacje GUI) o podpięciu lub usunięciu urządzenia ATB-USBasp. Pora więc zacząć się z nim komunikować :).
Jak to bywa przy komunikacji ze wszystkimi urządzeniami, to pierwszą funkcją jaką należy wykonać jest otwarcie urządzenia. Realizowane jest to, za każdym razem po wyzwoleniu skanowania I2C, poprzez funkcję OpenATBUSBaspDevice(), która przy okazji przypisuje nam identyfikator urządzenia do zmiennej globalnej MyUsbDevice.
Oczywiście tutaj także musimy użyć różnych funkcji dla Windows i Linux, bo wspominany wcześniej problem wersji sterowników ma tutaj znaczenie.
[syntax=csharp]private static UsbDevice MyUsbDevice = null;
private static UsbDeviceFinder MyUsbFinder = new UsbDeviceFinder(ATBUSBasp_VID, ATBUSBasp_PID);

private bool OpenATBUSBaspDevice()
{
bool status = false;
try
{
if (UsbDevice.IsLinux)
{
if ((MyUsbDevice = UsbDevice.OpenUsbDevice(MyUsbFinder)) != null) status = true;
}
else
{
if (MyUsbRegistry.Open(out MyUsbDevice)) status = true;
}
}
catch {}

return status;
}[/syntax]

Zamknięcie połączenia z urządzeniem (wykorzystywane po skończeniu skanowania urządzeń na I2C) realizowane jest w funkcji CloseATBUSBaspDevice():
[syntax=csharp]private void CloseATBUSBaspDevice()
{
try
{
if (MyUsbDevice.IsOpen) MyUsbDevice.Close();
}
catch {}
}[/syntax]


Po otwarciu urządzenia zostaje nam tylko zacząć wysyłać do niego komendy zgodnie z API.

Samo wysłanie danych do urządzenia realizujemy w programie poprzez funkcję SendToATBUSBaspDevice(), która korzysta z funkcji biblioteki MyUsbDevice.ControlTransfer(). API ATB-USBasp zwraca status wykonania większości komend poprzez zwrócenie stringa "OK" lub "ERROR". Sprawdzamy to, ale dla uproszczenia (bo po co porównywać ciągi ;)), patrzymy tylko na długość zwróconego ciągu :). Dzięki takiemu podejściu od razu dla komend które nic nie zwracają mamy także interpretację OK :D
[syntax=csharp]private bool SendToATBUSBaspDevice ( UsbSetupPacket setupPacket, byte[] dataBuffer )
{
int Tranferred;

if (MyUsbDevice.ControlTransfer(ref setupPacket, dataBuffer, 254, out Tranferred))
{
if (Tranferred < 5)
{
return true;
}
}
return false;
}[/syntax]

Do wywołania funkcji konieczne jest zdefiniowanie wartości zmiennych wejściowych (zgodnie z API ATB-USBasp), co jest banalne :D:
Ustawienie prędkości zegara SCL wygląda następująco:
[syntax=csharp]private bool i2cSetSCL ( short clock )
{
byte[] dataBuffer = new byte[254];
UsbSetupPacket setupPacket = new UsbSetupPacket((byte)(UsbCtrlFlags.Direction_In | UsbCtrlFlags.RequestType_Vendor),
USBASP_FUNC_I2C_BITRATE,
clock,
0,
254);

return SendToATBUSBaspDevice(setupPacket, dataBuffer);
}[/syntax]

A sprawdzenie obecności urządzenia na szynie I2C wygląda następująco:
[syntax=csharp]private bool i2cCheck ( byte address )
{
byte[] dataBuffer = new byte[254];
UsbSetupPacket setupPacket = new UsbSetupPacket((byte)(UsbCtrlFlags.Direction_In | UsbCtrlFlags.RequestType_Vendor),
USBASP_I2C_SLA_CHECK,
address,
0,
254);

return SendToATBUSBaspDevice(setupPacket, dataBuffer);
}[/syntax]


Teraz pozostaje złożyć części kodu w całość i dorzucić trochę iteracji z GUI, by użytkownik lubi jak się zmieniają różne rzeczy jak program działa a szczególnie jak coś klika ;)

Podsumowując, po kliknięciu przycisku skanowania I2C, gdy mamy podłączone ATB-USBasp, musimy otworzyć urządzenie (korzystając z funkcji OpenATBUSBaspDevice()), następnie ustawić wymaganą przez użytkownika prędkość dla I2C (poprzez funkcję i2cSetSCL()) i przystąpić do odpytywania kolejnych adresów (korzystając z funkcji i2cCheck()). Jeżeli funkcja zwróci prawdę, to znaczy że mamy urządzenie o takim adresie na szynie I2C, wtedy dodajemy informacje o tym urządzeniu do wyświetlanej tabelki i skanujemu kolejne adresy. Na koniec skanowania musimy zamknąć połączenie z urządzeniem i powiadomić użytkownika o zakończeniu działań.

Jak we wszystkich bibliotekach i poradnikach Mirka, także w API ATB-USBasp stosowany jest adres w notacji 8 bitowej (wartość łącznie z bitem R/W na najmłodszej pozycji), dlatego przy skanowaniu nie wysyłamy kolejnych wartości, tylko z przeskokiem co dwa.
Ale ja, na przekór :twisted: , lubię stosować notację 7-bitową, i taką używam w programie. Dlatego wartości zakresów adresów do skanowania zdefiniowane w stałych są 7 bitowe (I2C_7BIT_ADDRESS_MIN / I2C_7BIT_ADDRESS_MAX) a wartość wysyłana poprzez API jest przesunięta o jeden bit w lewo
[syntax=csharp]i2cCheck((byte)(i << 1))[/syntax]
Zgodnie ze specyfikacją I2C, nie wszystkie adresy przeznaczone są dla normalnych urządzeń. Dlatego nie skanujemy całego zakresu a tylko jego część. Początek i koniec jest zdefiniowany we wspominanych wcześniej stałych I2C_7BIT_ADDRESS_MIN (najmniejszy adres) oraz I2C_7BIT_ADDRESS_MAX (największy adres).

Program posiada możliwość pokazywania w tabeli, oprócz adresów znalezionych urządzeń, także dodatkowych informacji o możliwym typie tego urządzenia. Realizowane jest po poprzez wczytanie takich informacji zaraz po uruchomieniu programu z pliku o nazwie zdefiniowanej w stałej programu I2C_DESCRIPTION_FILE (domyślnie jest to plik i2c_table.txt) do zdefiniowanej listy:
[syntax=csharp]struct I2CDeviceDescription
{
public byte Address;
public string Description;
};

private List<I2CDeviceDescription> listNew = new List<I2CDeviceDescription>();[/syntax]

Wczytanie pliku odbywa się poprzez funkcję readI2CDeviceDescriptionFile(). Gdy plik zostanie załadowany ustawiana jest zmienna I2CDescriptionInfo, która to informuje czy mamy skąd próbować podawać nazwę urządzenia po jego wykryciu na I2C.
Informacje w plik mogą być modyfikowane przez użytkowników, o ile zachowana zostanie jego struktura (w źródłach dołączony jest przykładowy drugi plik i2c_table_long.txt, zawierający więcej informacji o urządzeniach).

Skanowanie urządzeń na I2C jest bardzo szybkie. Stosowanie normalnego sterowania progress bar-a (paska postępu) powoduje, że gdy pojawia się już komunikat o zakończeniu skanowania to progress bar jeszcze jest w połowie i sobie powoli jeszcze cały czas rośnie. Spowodowane jest to stosowaniem animacji progress bara przez Windowsowy interfejs Aero. Niestety nie ma opcji wyłączającej tę animację :(. Aby animacja się nie wykonywała to wymagane jest zastosowanie triku przy wpisywaniu kolejnych wartości do progress bar-a (dlatego prograss bar sterowany nie jest bezpośrednio a poprzez dodaną funkcję SetProgressNoAnimation()).

Dziwne dodane wpisy na końcu w funkcji MainFormFormClosing() spowodowane są tym, że biblioteka LibUsbDotNet ma chyba jakieś błędy i pod Linuxem są problemy z zamykaniem programu - przy eleganckim zwalnianiem zasobów. A w ten sposób robimy to trochę mniej elegancko ;).

--
Pozdrawiam,
Robert


Wyświetlone: 2105  •  Komentarze: 6  •  Odpowiedz [ Wróć ] Góra

40 ogłoszenia • Strona 1 z 141, 2, 3, 4, 5 ... 14

Najnowsze posty
Brak nowych postów taśma WS2812B 12V - więcej obwodów

Napisane przez rafal2302 » wczoraj, o 08:52
Forum: Elementy elektroniczne

2

162

wczoraj, o 17:43

rafal2302 Zobacz najnowszy post

Brak nowych postów YB 1.11.6 nietypowe zastosowanie nowych timerów programowych

Napisane przez fobos » wczoraj, o 07:44
Forum: Język C dla AVR

4

234

wczoraj, o 09:55

fobos Zobacz najnowszy post

Brak nowych postów SPRZEDAM ZASILACZ LED 12V/5A

Napisane przez durekzdw » 13 sie 2018, o 22:20
Forum: KUPIĘ - SPRZEDAM - ZAMIENIĘ - PODARUJĘ

0

90

13 sie 2018, o 22:20

durekzdw Zobacz najnowszy post

Brak nowych postów SPRZEDAM WTYK/GNIAZDO DC

Napisane przez durekzdw » 13 sie 2018, o 22:15
Forum: KUPIĘ - SPRZEDAM - ZAMIENIĘ - PODARUJĘ

0

74

13 sie 2018, o 22:15

durekzdw Zobacz najnowszy post

Brak nowych postów SPRZEDAM TAŚMY LED 60W

Napisane przez durekzdw » 13 sie 2018, o 22:07
Forum: KUPIĘ - SPRZEDAM - ZAMIENIĘ - PODARUJĘ

4

170

wczoraj, o 23:56

durekzdw Zobacz najnowszy post

Wątki: 18385 • Strona 1 z 36771, 2, 3, 4, 5 ... 3677

Kto jest online?
Kto przegląda forum Forum przegląda 47 użytkowników :: 4 zidentyfikowanych, 0 ukrytych i 43 gości (dane z ostatnich 5 minut)
Najwięcej użytkowników online (170) było 30 maja 2014, o 23:24

Zidentyfikowani użytkownicy: Bing [Bot], Google [Bot], LA72, Majestic-12 [Bot]
Legenda: Administratorzy, Moderatorzy globalni, Moderatorzy lokalni, PINKI, Tech-support, Zasłużeni


Skocz do:  

 Menu użytkownika
Nazwa użytkownika:


Hasło:


Zapamiętaj mnie

Zarejestruj się!


 Zegar


 Kalendarz
<< Sie. 2018 >>
Nd Pn Wt Śr Cz Pt So
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

 Statystyki
Wszystkich
Liczba postów: 191979
Liczba wątków: 18527
Wszystkich Ogłoszeń: 9
Wszystkich Przyklejonych: 71
Wszystkich Załączników: 2192

Tematów na dzień: 7
Postów na dzień: 77
Użytkowników na dzień: 8
Tematów na użytkownika: 1
Postów na użytkownika: 10
Postów na temat: 10

Liczba użytkowników: 19097
Najnowszy użytkownik: puhmik1

 Ekipa
Administratorzy
atneladmin
Moderatorzy
mirekk36
Sonix
SunRiver

 Wizyty botów
Google [Bot]
mniej niż minutę temu



Sitemap
Technologię dostarcza phpBB® Forum Software © phpBB Group phpBB3.PL
phpBB SEO