Author Topic: Open Source Laser Tag project - ARMada  (Read 24707 times)

Theuer

  • Full Member
  • **
  • Posts: 58
    • View Profile
    • Email
Re: Open Source Laser Tag project - ARMada
« Reply #285 on: August 09, 2017, 06:43:13 PM »
  What programmer do you use? Which do you indicate to buy?
  Here in Brazil I found ST-Link V2, is it works?
 Thanks again.

Pingvin

  • Developer
  • Veteran
  • ***
  • Posts: 408
    • View Profile
Re: Open Source Laser Tag project - ARMada
« Reply #286 on: August 11, 2017, 05:03:11 AM »
  What programmer do you use? Which do you indicate to buy?
  Here in Brazil I found ST-Link V2, is it works?
 Thanks again.

I use ST-Link which is installed on the developer's board.


Yes - ST-Link  V2 should work!

Pingvin

  • Developer
  • Veteran
  • ***
  • Posts: 408
    • View Profile
Re: Open Source Laser Tag project - ARMada
« Reply #287 on: August 16, 2017, 10:13:56 PM »
Again I took up radio modules.
I received two new radio modules.
Before that, one of the two radio modules I had broke.
Radio modules can be connected to two different ports - to the LCD port or to the sensor port.

I plan to do this: in the weapon, the radio module will be connected to the sensor port, and in the bandana to the LCD port.

If the radio module connected to the sensor port is already working, then in order to use the LCD port, you must disable the LCD driver.

With the existing project structure it is not easy to do this.
The main code is overloaded with low-level functions for working with hardware.

And since the equipment can be different (color or black and white LCD, for example), then there are many more macros.
The code became difficult to understand.

Of course - I, first of all - wanted to check the efficiency of my ideas as soon as possible, the quality of the code was in the background.
Now that the concept has generally justified itself, it's time to take up the quality of the code.

It is necessary to hide all low-level functions in the drivers.
Add a "middlware" layer to the interface so that there is a single interface for working with the connected devices and using the various devices, the code of the main program did not change.

This is such a difficult task...
« Last Edit: August 16, 2017, 10:21:34 PM by Pingvin »

berk

  • Veteran
  • *****
  • Posts: 371
    • View Profile
    • funwithelectrons.blogspot.com
Re: Open Source Laser Tag project - ARMada
« Reply #288 on: August 19, 2017, 03:52:57 AM »
You have just described what is known as a 'Hardware Abstraction Layer'. Shouldn't be too hard but if done properly a new piece of hardware should just drop straight in.

Good luck!

Pingvin

  • Developer
  • Veteran
  • ***
  • Posts: 408
    • View Profile
Re: Open Source Laser Tag project - ARMada
« Reply #289 on: Today at 06:18:09 AM »
You have just described what is known as a 'Hardware Abstraction Layer'. Shouldn't be too hard but if done properly a new piece of hardware should just drop straight in.

Good luck!
That's what I mean.
There are two types of screen, the initialization and text output functions are also different.
I want to make sure that the user who will write his code does not delve into the intricacies of the hardware implementation of the screen. It will be provided with an API for working with an abstract display.
Here is an example, so it was
Code: [Select]
bool configure_bluetooth(void)//настраиваем блютус модуль
{

bool at_res;
#ifdef COLOR_LCD
static volatile TextParamStruct TS;
TS.XPos =  0;
TS.YPos = 10;
TS.Size = 0;
TS.Font = StdFont;
TS.TxtCol = iWhite;//iGreen;//iRed;
TS.BkgCol =iBlack;//iWhite;
#endif


USART_DeInit(USART1);
InitCOM1(38400);
bt_set_at_commands_mode(true);
vTaskDelay(100);
bt_reset();
BL_ON;

at_res = send_test_at_command();
if(!at_res){
#ifndef COLOR_LCD
lcd8544_putstr(0, 8, "38400 тест не прошел", 0); // вывод первой строки
if (!screen_auto_refresh) lcd8544_dma_refresh(); // вывод буфера на экран ! без этого ничего видно не будет !
#else


if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
PStr("38400: Test error", &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif
USART_DeInit(USART1);
InitCOM1(9600);
vTaskDelay(100);
}
if(strlen(armadaSystem.bluetooth.name))//если строка с именем не пустая
{

#ifndef COLOR_LCD
lcd8544_putstr(0, 16, "Имя: ", 0); // вывод первой строки
lcd8544_putstr(8, 16, armadaSystem.bluetooth.name, 0); // вывод первой строки
if (!screen_auto_refresh) lcd8544_dma_refresh(); //
#else
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
PStr( "Name: ", &TS);
PStr(armadaSystem.bluetooth.name, &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif


at_res = send_set_at_command(at_name,armadaSystem.bluetooth.name);
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
if (at_res) PStr( "OK", &TS);
else PStr( "ERROR", &TS);
xSemaphoreGive(xColorLCDSemaphore);
}

#else
if (at_res)
{

lcd8544_putstr(0, 24, "Имя успешно задано ", 0); // вывод первой строки
if (!screen_auto_refresh) lcd8544_dma_refresh(); //


}

#endif
}
if(strlen(armadaSystem.bluetooth.cmode))//если строка с именем не пустая
{
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
PStr( at_cmode, &TS);
PStr( "=", &TS);
PStr( armadaSystem.bluetooth.cmode, &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif
at_res = send_set_at_command(at_cmode,armadaSystem.bluetooth.cmode);

#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
if (at_res) PStr( "OK", &TS);
else PStr( "ERROR", &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif



}
if(strlen(armadaSystem.bluetooth.role))//если строка с именем не пустая
{
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
PStr( at_role, &TS);
PStr( "=", &TS);
PStr( armadaSystem.bluetooth.role, &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif


at_res = send_set_at_command(at_role,armadaSystem.bluetooth.role);
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
if (at_res) PStr( "OK", &TS);
else PStr( "ERROR", &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif



}
if(strlen(armadaSystem.bluetooth.mac_adress_for_bind))//если строка с именем не пустая
{

#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
PStr( "BIND:", &TS);
// PStr( "=", &TS);
PStr( armadaSystem.bluetooth.mac_adress_for_bind, &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif
at_res = send_set_at_command(at_bind,armadaSystem.bluetooth.mac_adress_for_bind);
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
if (at_res) PStr( "OK", &TS);
else PStr( "ERROR", &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif

}
if(strlen(armadaSystem.bluetooth.pswd))//если строка с именем не пустая
{
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
PStr( "PASSWORD:", &TS);
// PStr( "=", &TS);
PStr( armadaSystem.bluetooth.pswd, &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif

at_res = send_set_at_command(at_pswd,armadaSystem.bluetooth.pswd);
#ifdef COLOR_LCD
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//если LCD занят, ждем 2 с
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.XPos =  0;
TS.YPos +=10;
if (at_res) PStr( "OK", &TS);
else PStr( "ERROR", &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif

}
bt_set_at_commands_mode(false);
USART_DeInit(USART1);
if(armadaSystem.bluetooth.baudrate) InitCOM1(armadaSystem.bluetooth.baudrate);
else InitCOM1(BAUDRATE);
bt_reset();
vTaskDelay(TIC_FQR*2);
#ifndef COLOR_LCD
BL_OFF;
#endif
}



It's terrible!
Unreadable!
Difficult to perceive!

Now this function looks like this
Code: [Select]


bool configure_bluetooth(void)//íàñòðàèâàåì áëþòóñ ìîäóëü
{

bool at_res;
unsigned char line_counter=0;
unsigned char line_on_screen=display_vertical_screen_resolution()/display_standard_symbol_height();


USART_DeInit(USART1);
InitCOM1(38400);
bt_set_at_commands_mode(true);
vTaskDelay(100);
bt_reset();
BL_ON;
display_put_string(0,line_counter++, "Set BT");

at_res = send_test_at_command();

if(!at_res){
if(!(line_counter<line_on_screen)){
vTaskDelay(150);
line_counter=0;
display_clear_screen();
}
display_put_string(0,display_standard_symbol_height()*(line_counter++),"38400: Test error");

USART_DeInit(USART1);
InitCOM1(9600);
vTaskDelay(100);
}
if(strlen(armadaSystem.bluetooth.name))//åñëè ñòðîêà ñ èìåíåì íå ïóñòàÿ
{

if(!((line_counter+1)<line_on_screen)){
vTaskDelay(150);
line_counter=0;
display_clear_screen();
}
at_res = send_set_at_command(at_name,armadaSystem.bluetooth.name);
display_put_string(0,display_standard_symbol_height()*(line_counter),"Name: ");
display_put_string(display_standard_symbol_width()*5,display_standard_symbol_height()*(line_counter++),armadaSystem.bluetooth.name);

if (at_res)
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"OK");
}
else
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"ERROR");
}
}
if(strlen(armadaSystem.bluetooth.cmode))//åñëè ñòðîêà ñ èìåíåì íå ïóñòàÿ
{

if(!((line_counter+1)<line_on_screen)){
vTaskDelay(150);
line_counter=0;
display_clear_screen();
}

at_res = send_set_at_command(at_cmode,armadaSystem.bluetooth.cmode);
display_put_string(0,display_standard_symbol_height()*(line_counter),at_cmode);
display_put_string(display_standard_symbol_width()*8,display_standard_symbol_height()*(line_counter),"=");
display_put_string(display_standard_symbol_width()*9,display_standard_symbol_height()*(line_counter++),armadaSystem.bluetooth.cmode);
if (at_res)
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"OK");
}
else
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"ERROR");
}
}
if(strlen(armadaSystem.bluetooth.role))//åñëè ñòðîêà ñ èìåíåì íå ïóñòàÿ
{
if(!((line_counter+1)<line_on_screen)){
vTaskDelay(150);
line_counter=0;
display_clear_screen();
}
at_res = send_set_at_command(at_role,armadaSystem.bluetooth.role);
display_put_string(0,display_standard_symbol_height()*(line_counter),at_role);
display_put_string(display_standard_symbol_width()*7,display_standard_symbol_height()*(line_counter),"=");
display_put_string(display_standard_symbol_width()*8,display_standard_symbol_height()*(line_counter++),armadaSystem.bluetooth.role);
if (at_res)
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"OK");
}
else
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"ERROR");
}
}
if(strlen(armadaSystem.bluetooth.mac_adress_for_bind))//åñëè ñòðîêà ñ èìåíåì íå ïóñòàÿ
{
if(!((line_counter+1)<line_on_screen)){
vTaskDelay(150);
line_counter=0;
display_clear_screen();
}
at_res = send_set_at_command(at_bind,armadaSystem.bluetooth.mac_adress_for_bind);
display_put_string(0,display_standard_symbol_height()*(line_counter),"BIND:");
display_put_string(display_standard_symbol_width()*5,display_standard_symbol_height()*(line_counter++),armadaSystem.bluetooth.mac_adress_for_bind);
if (at_res)
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"OK");
}
else
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"ERROR");
}
}
if(strlen(armadaSystem.bluetooth.pswd))//åñëè ñòðîêà ñ èìåíåì íå ïóñòàÿ
{

if(!((line_counter+1)<line_on_screen)){
vTaskDelay(150);
line_counter=0;
display_clear_screen();
}
at_res = send_set_at_command(at_pswd,armadaSystem.bluetooth.pswd);
display_put_string(0,display_standard_symbol_height()*(line_counter),"PASSWORD:");
display_put_string(display_standard_symbol_width()*9,display_standard_symbol_height()*(line_counter++),armadaSystem.bluetooth.pswd);
if (at_res)
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"OK");
}
else
{
display_put_string(0,display_standard_symbol_height()*(line_counter++),"ERROR");
}
}
bt_set_at_commands_mode(false);
GPIO_ResetBits(GPIOC, GPIO_Pin_4);//
USART_DeInit(USART1);
if(armadaSystem.bluetooth.baudrate) InitCOM1(armadaSystem.bluetooth.baudrate);
else InitCOM1(BAUDRATE);
// bt_reset();
vTaskDelay(TIC_FQR*2);
#ifndef COLOR_LCD
BL_OFF;
#endif
}

Now it works on both a color and black-and-white screen, the same code.
Even if I can connect another type of LCD, I do not need to change this code.
It looks a little better, does not it?
But the ideal is still far away.  ;) :)
« Last Edit: Today at 06:35:53 AM by Pingvin »

Pingvin

  • Developer
  • Veteran
  • ***
  • Posts: 408
    • View Profile
Re: Open Source Laser Tag project - ARMada
« Reply #290 on: Today at 06:25:36 AM »
file display.h
Code: [Select]
#ifndef __DISPLAY_H
#define __DISPLAY_H
#include <stdio.h>
#include <stdint.h>

#include "cipher.h"
#include <GFXC.h>
#include "types.h"
#include "global_variables.h"

#ifndef COLOR_LCD
#define STANDARD_SYMBOL_HEIGHT 8
#define STANDARD_SYMBOL_WIDTH 6
#define HORIZONTAL_SCREEN_RESOLUTION 84
#define VERTICAL_SCREEN_RESOLUTION 48

#else
#define STANDARD_SYMBOL_HEIGHT 10
#define STANDARD_SYMBOL_WIDTH 6
#define HORIZONTAL_SCREEN_RESOLUTION 128
#define VERTICAL_SCREEN_RESOLUTION 128
#endif


void display_clear_screen(void);
void display_init(void);
// âûâîä ñòðîêè
void display_put_string(unsigned char x, unsigned char y, const unsigned char str[]);

unsigned char display_standard_symbol_height(void);
unsigned char display_standard_symbol_width(void);
unsigned char display_horizontal_screen_resolution(void);
unsigned char display_vertical_screen_resolution(void);

#endif



file display.c
Code: [Select]

#include "display.h"

void display_clear_screen(void){
#ifndef COLOR_LCD
clear_screen();
if (!screen_auto_refresh) lcd8544_dma_refresh(); // âûâîä áóôåðà íà ýêðàí ! áåç ýòîãî íè÷åãî âèäíî íå áóäåò !
#else
ClrScrn();
#endif
}


void display_init(void){
#ifndef COLOR_LCD
lcd8544_init(); // èíèöèàëèçàöèÿ ÷¸ðíî-áåëîãî äèñïëåÿ
#else

ILI9163Init();//
/*
drawBMP("2gun.bmp");
vTaskDelay((portTickType)(TIC_FQR*2));
*/

#endif


}


void display_put_string(unsigned char x, unsigned char y, const unsigned char str[]){

#ifndef COLOR_LCD
  lcd8544_putstr(x, y, str, 0); // âûâîä ïåðâîé ñòðîêè
  if (!screen_auto_refresh) lcd8544_dma_refresh(); // âûâîä áóôåðà íà ýêðàí ! áåç ýòîãî íè÷åãî âèäíî íå áóäåò !
#else
static volatile TextParamStruct TS;
if(xSemaphoreTake(xColorLCDSemaphore, (portTickType)(TIC_FQR*2)/*600*/ )== pdTRUE)//åñëè LCD çàíÿò, æäåì 2 ñ
{
while (!(SPI3->SR & SPI_SR_TXE)); // Wait for bus free
while (SPI3->SR & SPI_SR_BSY);
init_spi3();
SB(0x36, Reg); //Set Memory access mode
#if    LCD_MODUL_VERSION == 2
SB((0x08 |(1<<7)|(1<<6)), Dat);
#elif LCD_MODUL_VERSION == 1
SB((0x08 /*|(1<<7)*/), Dat);
#endif
TS.Size = 0;
TS.Font = StdFont;
TS.XPos =  (uint8_t)x;
TS.YPos = (uint8_t)y;
TS.TxtCol = iWhite;//iGreen;//iRed;
TS.BkgCol =iBlack;//iWhite;
PStr(/*"Set BT"*/str, &TS);
xSemaphoreGive(xColorLCDSemaphore);
}
#endif




}



unsigned char display_standard_symbol_height(void){

return (unsigned char) STANDARD_SYMBOL_HEIGHT;

}


unsigned char display_standard_symbol_width(void){
return (unsigned char) STANDARD_SYMBOL_WIDTH;

}


unsigned char display_horizontal_screen_resolution(void){
return HORIZONTAL_SCREEN_RESOLUTION;
}


unsigned char display_vertical_screen_resolution(void){
return VERTICAL_SCREEN_RESOLUTION;
}
« Last Edit: Today at 06:37:20 AM by Pingvin »