Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
co2ampel
ampel-firmware
Commits
ea7407e0
Commit
ea7407e0
authored
May 15, 2021
by
Eric Duminil
Browse files
Bare necessities for webconfig
parent
f94b3115
Changes
5
Hide whitespace changes
Inline
Side-by-side
ampel-firmware/ampel-firmware.ino
View file @
ea7407e0
...
...
@@ -67,6 +67,34 @@ void setup() {
web_config
::
initialize
();
web_config
::
setWifiConnectionCallback
([]()
{
//TODO: Move somewhere else
led_effects
::
showKITTWheel
(
color
::
green
);
Serial
.
println
();
Serial
.
print
(
F
(
"WiFi - Connected! IP address: "
));
IPAddress
address
=
WiFi
.
localIP
();
snprintf
(
wifi
::
local_ip
,
sizeof
(
wifi
::
local_ip
),
"%d.%d.%d.%d"
,
address
[
0
],
address
[
1
],
address
[
2
],
address
[
3
]);
ntp
::
initialize
();
# ifdef AMPEL_MQTT
mqtt
::
initialize
(
ampel
.
sensorId
);
# endif
Serial
.
println
(
wifi
::
local_ip
);
Serial
.
print
(
F
(
"You can access this sensor via http://"
));
Serial
.
print
(
ampel
.
sensorId
);
Serial
.
print
(
F
(
".local (might be unstable) or http://"
));
Serial
.
println
(
WiFi
.
localIP
());
});
web_config
::
setWifiConnectionFailedCallback
([]()
{
led_effects
::
showKITTWheel
(
color
::
red
);
Serial
.
println
(
F
(
"Connection to WiFi failed"
));
});
void
setWifiConnectionFailedCallback
(
void
(
*
function
)());
pinMode
(
0
,
INPUT
);
// Flash button (used for forced calibration)
Serial
.
println
();
...
...
@@ -88,7 +116,9 @@ void setup() {
#endif
#ifdef AMPEL_WIFI // Structure doesn't make sense anymore
wifi
::
defineCommands
();
# ifdef AMPEL_HTTP
// web_server::define_pages();
//TODO: Rename. Not just web_server
// web_server::initialize();
# endif
...
...
ampel-firmware/web_config.cpp
View file @
ea7407e0
#include
"web_config.h"
namespace
config
{
// Values should be defined in config.h
#ifdef HTTP_USER
const
char
*
http_user
=
HTTP_USER
;
#else
const
char
*
http_user
=
""
;
#endif
#ifdef HTTP_PASSWORD
const
char
*
http_password
=
HTTP_PASSWORD
;
#else
const
char
*
http_password
=
""
;
#endif
}
namespace
web_config
{
const
char
*
header_template
;
const
char
*
body_template
;
const
char
*
script_template
;
void
handleWebServerRoot
();
void
handlePageNotFound
();
void
handleWebServerCommand
();
#ifdef AMPEL_CSV
void
handleDeleteCSV
();
void
handleWebServerCSV
();
#endif
#if defined(ESP8266)
ESP8266WebServer
http
(
80
);
// Create a webserver object that listens for HTTP request on port 80
#elif defined(ESP32)
WebServer
http
(
80
);
#endif
DNSServer
dnsServer
;
DNSServer
dnsServer
;
//TODO: Check if needed
IotWebConf
*
iotWebConf
;
...
...
@@ -68,6 +40,17 @@ namespace web_config {
iotWebConf
->
doLoop
();
// Listen for HTTP requests from clients
}
void
setWifiConnectionCallback
(
void
(
*
function
)())
{
iotWebConf
->
setWifiConnectionCallback
(
function
);
}
void
setWifiConnectionFailedCallback
(
void
(
*
function
)())
{
iotWebConf
->
setWifiConnectionFailedHandler
([
&
function
]()
->
iotwebconf
::
WifiAuthInfo
*
{
function
();
return
NULL
;
});
}
void
initialize
()
{
iotWebConf
=
new
IotWebConf
(
ampel
.
sensorId
,
&
dnsServer
,
&
http
,
HTTP_PASSWORD
,
config_version
);
...
...
@@ -94,40 +77,6 @@ namespace web_config {
iotWebConf
->
addParameterGroup
(
&
group1
);
iotWebConf
->
addParameterGroup
(
&
group2
);
iotWebConf
->
setWifiConnectionCallback
([]()
{
led_effects
::
showKITTWheel
(
color
::
green
);
Serial
.
println
();
Serial
.
print
(
F
(
"WiFi - Connected! IP address: "
));
IPAddress
address
=
WiFi
.
localIP
();
snprintf
(
wifi
::
local_ip
,
sizeof
(
wifi
::
local_ip
),
"%d.%d.%d.%d"
,
address
[
0
],
address
[
1
],
address
[
2
],
address
[
3
]);
ntp
::
initialize
();
//FIXME: Somehow already started
// if (MDNS.begin(ampel.sensorId)) { // Start the mDNS responder for SENSOR_ID.local
// MDNS.addService("http", "tcp", 80);
// Serial.println(F("mDNS responder started"));
// } else {
// Serial.println(F("Error setting up MDNS responder!"));
// }
# ifdef AMPEL_MQTT
mqtt
::
initialize
(
ampel
.
sensorId
);
# endif
Serial
.
println
(
wifi
::
local_ip
);
Serial
.
print
(
F
(
"You can access this sensor via http://"
));
Serial
.
print
(
ampel
.
sensorId
);
Serial
.
print
(
F
(
".local (might be unstable) or http://"
));
Serial
.
println
(
WiFi
.
localIP
());
});
iotWebConf
->
setWifiConnectionFailedHandler
([]()
->
iotwebconf
::
WifiAuthInfo
*
{
led_effects
::
showKITTWheel
(
color
::
red
);
Serial
.
println
(
F
(
"Connection to WiFi failed"
));
return
NULL
;
});
iotWebConf
->
skipApStartup
();
//TODO: Add callbacks
//TODO: Add LED effects
...
...
@@ -158,270 +107,15 @@ namespace web_config {
Serial
.
println
(
F
(
"Done!"
));
},
F
(
"(resets the complete IotWeb config)"
));
header_template
=
PSTR
(
"<!doctype html><html lang=en>"
"<head>
\n
"
"<title>%d ppm - CO2 SENSOR - %s - %s</title>
\n
"
"<meta charset='UTF-8'>
\n
"
// HfT Favicon
"<link rel='icon' type='image/png' sizes='16x16' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAHtJREFUOE9jvMnA+5+BAsBIFQMkl85h+P3kGcOb8jqwW+TPH2H4de0GA29UGNxtfx49YWCRk0HwHz5iuKegwwB2AS4DkA2F6VR6cAWsEQbgBqDY9vARw/ejJ+Au+LxsFcPz6BSwHpwGYPMCSS6gyAAKYhESiKMGjPgwAADopHVhn5ynEwAAAABJRU5ErkJggg=='/>
\n
"
// Responsive grid:
"<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/pure-min.css'>
\n
"
"<link rel='stylesheet' href='https://unpkg.com/purecss@2.0.3/build/grids-responsive-min.css'>
\n
"
// JS Graphs:
"<script src='https://cdn.plot.ly/plotly-basic-1.58.2.min.js'></script>
\n
"
// Fullscreen
"<meta name='viewport' content='width=device-width, initial-scale=1'>
\n
"
// Refresh after every measurement.
// "<meta http-equiv='refresh' content='%d'>\n"
"</head>
\n
"
"<body>
\n
"
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> Ampel</p></div></div>
\n
"
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>
\n
"
"<li class='pure-menu-item'><a href='/config' class='pure-menu-link'>Config</a></li>
\n
"
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>
\n
"
#ifdef AMPEL_CSV
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>
\n
"
"<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>
\n
"
"<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>
\n
"
#endif
"<li class='pure-menu-item' id='led'>⬤</li>
\n
"
// LED
"</ul></div></div>
\n
"
"<script>
\n
"
// Show a colored dot on the webpage, with a similar color than on LED Ring.
"hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;
\n
"
"document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');
\n
"
"</script>
\n
"
"<div class='pure-g'>
\n
"
"<div class='pure-u-1' id='graph'></div>
\n
"
// Graph placeholder
"</div>
\n
"
"<div class='pure-g'>
\n
"
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>
\n
"
);
body_template
=
PSTR
(
"<tr><th colspan='2'>%s</th></tr>
\n
"
"<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>
\n
"
"<tr><td>Temperature</td><td>%.1f℃</td></tr>
\n
"
"<tr><td>Humidity</td><td>%.1f%%</td></tr>
\n
"
"<tr><td>Last measurement</td><td>%s</td></tr>
\n
"
"<tr><td>Measurement timestep</td><td>%5d s</td></tr>
\n
"
#ifdef AMPEL_CSV
"<tr><th colspan='2'>CSV</th></tr>
\n
"
"<tr><td>Last write</td><td>%s</td></tr>
\n
"
"<tr><td>Timestep</td><td>%5d s</td></tr>
\n
"
"<tr><td>Available drive space</td><td>%d kB</td></tr>
\n
"
#endif
#ifdef AMPEL_MQTT
"<tr><th colspan='2'>MQTT</th></tr>
\n
"
"<tr><td>Connected?</td><td>%s</td></tr>
\n
"
"<tr><td>Last publish</td><td>%s</td></tr>
\n
"
"<tr><td>Timestep</td><td>%5d s</td></tr>
\n
"
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
"<tr><th colspan='2'>LoRaWAN</th></tr>
\n
"
"<tr><td>Connected?</td><td>%s</td></tr>
\n
"
"<tr><td>Frequency</td><td>%s MHz</td></tr>
\n
"
"<tr><td>Last transmission</td><td>%s</td></tr>
\n
"
"<tr><td>Timestep</td><td>%5d s</td></tr>
\n
"
#endif
"<tr><th colspan='2'>Sensor</th></tr>
\n
"
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>
\n
"
//TODO: Read it from sensor?
"<tr><td>Auto-calibration?</td><td>%s</td></tr>
\n
"
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>
\n
"
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>
\n
"
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>
\n
"
"<tr><td>Largest heap block</td><td>%6d bytes</td></tr>
\n
"
"<tr><td>Max loop duration</td><td>%5d ms</td></tr>
\n
"
"<tr><td>Board</td><td>%s</td></tr>
\n
"
"<tr><td>Ampel firmware</td><td>%s</td></tr>
\n
"
"<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>
\n
"
"</table>
\n
"
"<div id='log' class='pure-u-1 pure-u-md-1-2'></div>
\n
"
"<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>
\n
"
#ifdef AMPEL_CSV
"<form action='/delete_csv' method='POST' onsubmit=
\"
return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');
\"
>"
"<input type='submit' value='Delete CSV'/>"
"</form>
\n
"
#endif
"</div>
\n
"
);
script_template
=
PSTR
(
"<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware' target='_blank'>Source code</a>
\n
"
"<a href='https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation' target='_blank'>Documentation</a>
\n
"
#ifdef AMPEL_CSV
"<script>
\n
"
"document.body.style.cursor = 'default';
\n
"
"fetch('%s',{credentials:'include'})
\n
"
".then(response=>response.text())
\n
"
".then(csvText=>csvToTable(csvText))
\n
"
".then(htmlTable=>addLogTableToPage(htmlTable))
\n
"
".then(_=>Plotly.newPlot('graph',data,layout,{displaylogo:false}))
\n
"
".catch(e=>console.error(e));
\n
"
"xs=[];
\n
"
"data=[{x:xs,y:[],type:'scatter',name:'CO<sub>2</sub>',line:{color:'#2ca02c'}},
\n
"
"{x:xs,y:[],type:'scatter',name:'Temperature',yaxis:'y2',line:{color:'#ff7f0e',dash:'dot'}},
\n
"
"{x:xs,y:[],type:'scatter',name:'Humidity',yaxis:'y3',line:{color:'#1f77b4',dash:'dot'}}];
\n
"
"layout={height:600,title:'%s',legend:{xanchor:'right',x:0.2,y:1.0},
\n
"
"xaxis:{domain:[0.0,0.85]},yaxis:{ticksuffix:'ppm',range:[0,2000],dtick:200},
\n
"
"yaxis2:{overlaying:'y',side:'right',ticksuffix:'°C',position:0.9,anchor:'free',range:[0,30],dtick:3},
\n
"
"yaxis3:{overlaying:'y',side:'right',ticksuffix:'%%',position:0.95,anchor:'free',range:[0,100],dtick:10}
\n
"
"};
\n
"
"function csvToTable(csvText) {
\n
"
"csvText=csvText.trim();
\n
"
"lines=csvText.split('
\\
n');
\n
"
"table=document.createElement('table');
\n
"
"table.className='pure-table-striped';
\n
"
"n=lines.length;
\n
"
"lines.forEach((line,i)=>{
\n
"
"fields=line.split(';');
\n
"
"xs.push(fields[0]);
\n
"
"data[0]['y'].push(fields[1]);
\n
"
"data[1]['y'].push(fields[2]);
\n
"
"data[2]['y'].push(fields[3]);
\n
"
"if(i>4 && i<n-12){if(i==5){fields=['...','...','...','...']}else{return;}}
\n
"
"row=document.createElement('tr');
\n
"
"fields.forEach((field,index)=>{
\n
"
"cell=document.createElement(i<2?'th':'td');
\n
"
"cell.appendChild(document.createTextNode(field));
\n
"
"row.appendChild(cell);});
\n
"
"table.appendChild(row);});
\n
"
"return table;}
\n
"
"function addLogTableToPage(table){document.getElementById('log').appendChild(table);}
\n
"
"</script>
\n
"
#endif
"</body>
\n
"
"</html>"
);
// Web-server
http
.
on
(
"/"
,
handleWebServerRoot
);
http
.
on
(
"/command"
,
handleWebServerCommand
);
#ifdef AMPEL_CSV
http
.
on
(
csv_writer
::
filename
,
handleWebServerCSV
);
//NOTE: csv_writer should have been initialized first.
http
.
on
(
"/delete_csv"
,
HTTP_POST
,
handleDeleteCSV
);
#endif
//TODO: Authenticate only if required?
//TODO: / captive fast return?
http
.
on
(
"/config"
,
[]
{
iotWebConf
->
handleConfig
();
});
http
.
onNotFound
([]()
{
iotWebConf
->
handleNotFound
();
});
//TODO: Only once wifi connected
}
// Allow access if http_user or http_password are empty, or if provided credentials match
bool
shouldBeAllowed
()
{
return
strcmp
(
config
::
http_user
,
""
)
==
0
||
strcmp
(
config
::
http_password
,
""
)
==
0
||
http
.
authenticate
(
config
::
http_user
,
config
::
http_password
);
}
void
handleWebServerRoot
()
{
if
(
iotWebConf
->
handleCaptivePortal
())
{
// -- Captive portal requests were already served.
return
;
}
if
(
!
shouldBeAllowed
())
{
return
http
.
requestAuthentication
(
DIGEST_AUTH
);
}
unsigned
long
ss
=
seconds
();
uint8_t
dd
=
ss
/
86400
;
ss
-=
dd
*
86400
;
unsigned
int
hh
=
ss
/
3600
;
ss
-=
hh
*
3600
;
uint8_t
mm
=
ss
/
60
;
ss
-=
mm
*
60
;
//NOTE: Splitting in multiple parts in order to use less RAM
char
content
[
2000
];
// Update if needed
// INFO - Header size : 1767 - Body size : 1812 - Script size : 1909
snprintf_P
(
content
,
sizeof
(
content
),
header_template
,
sensor
::
co2
,
ampel
.
sensorId
,
wifi
::
local_ip
#ifdef AMPEL_CSV
,
csv_writer
::
filename
#endif
);
// Serial.print(F("INFO - Header size : "));
// Serial.print(strlen(content));
http
.
setContentLength
(
CONTENT_LENGTH_UNKNOWN
);
http
.
send_P
(
200
,
PSTR
(
"text/html"
),
content
);
// Body
snprintf_P
(
content
,
sizeof
(
content
),
body_template
,
ampel
.
sensorId
,
sensor
::
co2
,
sensor
::
temperature
,
sensor
::
humidity
,
sensor
::
timestamp
,
config
::
measurement_timestep
,
#ifdef AMPEL_CSV
csv_writer
::
last_successful_write
,
config
::
csv_interval
,
csv_writer
::
getAvailableSpace
()
/
1024
,
#endif
#ifdef AMPEL_MQTT
mqtt
::
connected
?
"Yes"
:
"No"
,
mqtt
::
last_successful_publish
,
config
::
mqtt_sending_interval
,
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
lorawan
::
connected
?
"Yes"
:
"No"
,
LMIC_FREQUENCY_PLAN
,
lorawan
::
last_transmission
,
config
::
lorawan_sending_interval
,
#endif
config
::
temperature_offset
,
config
::
auto_calibrate_sensor
?
"Yes"
:
"No"
,
ampel
.
sensorId
,
ampel
.
sensorId
,
wifi
::
local_ip
,
wifi
::
local_ip
,
ESP
.
getFreeHeap
(),
esp_get_max_free_block_size
(),
ampel
.
max_loop_duration
,
ampel
.
board
,
ampel
.
version
,
dd
,
hh
,
mm
,
ss
);
// Serial.print(F(" - Body size : "));
// Serial.print(strlen(content));
http
.
sendContent
(
content
);
// Script
snprintf_P
(
content
,
sizeof
(
content
),
script_template
#ifdef AMPEL_CSV
,
csv_writer
::
filename
,
ampel
.
sensorId
#endif
);
// Serial.print(F(" - Script size : "));
// Serial.println(strlen(content));
http
.
sendContent
(
content
);
}
#ifdef AMPEL_CSV
void
handleWebServerCSV
()
{
if
(
!
shouldBeAllowed
())
{
return
http
.
requestAuthentication
(
DIGEST_AUTH
);
}
if
(
FS_LIB
.
exists
(
csv_writer
::
filename
))
{
fs
::
File
csv_file
=
FS_LIB
.
open
(
csv_writer
::
filename
,
"r"
);
char
csv_size
[
10
];
snprintf
(
csv_size
,
sizeof
(
csv_size
),
"%d"
,
csv_file
.
size
());
http
.
sendHeader
(
"Content-Length"
,
csv_size
);
http
.
streamFile
(
csv_file
,
F
(
"text/csv"
));
csv_file
.
close
();
}
else
{
http
.
send
(
204
,
F
(
"text/html"
),
F
(
"No data available."
));
}
}
void
handleDeleteCSV
()
{
if
(
!
shouldBeAllowed
())
{
return
http
.
requestAuthentication
(
DIGEST_AUTH
);
}
Serial
.
print
(
F
(
"Removing CSV file..."
));
FS_LIB
.
remove
(
csv_writer
::
filename
);
Serial
.
println
(
F
(
" Done!"
));
http
.
sendHeader
(
"Location"
,
"/"
);
http
.
send
(
303
);
}
#endif
void
handleWebServerCommand
()
{
if
(
!
shouldBeAllowed
())
{
return
http
.
requestAuthentication
(
DIGEST_AUTH
);
}
http
.
sendHeader
(
"Location"
,
"/"
);
http
.
send
(
303
);
sensor_console
::
execute
(
http
.
arg
(
"send"
).
c_str
());
}
void
handlePageNotFound
()
{
http
.
send
(
404
,
F
(
"text/plain"
),
F
(
"404: Not found"
));
}
}
ampel-firmware/web_config.h
View file @
ea7407e0
...
...
@@ -12,11 +12,13 @@
#include
<IotWebConf.h>
#include
<IotWebConfTParameter.h>
//
#include "util.h"
#include
"util.h"
//#include "sensor_console.h"
namespace
web_config
{
void
initialize
();
void
setWifiConnectionCallback
(
void
(
*
function
)());
void
setWifiConnectionFailedCallback
(
void
(
*
function
)());
void
update
();
}
#endif
ampel-firmware/wifi_util.cpp
View file @
ea7407e0
...
...
@@ -51,47 +51,9 @@ namespace wifi {
Serial
.
print
(
F
(
"WiFi - SSID : "
));
Serial
.
println
(
WIFI_SSID
);
}
bool
connected
()
{
return
WiFi
.
status
()
==
WL_CONNECTED
;
}
// Initialize Wi-Fi
void
connect
(
const
char
*
hostname
)
{
void
defineCommands
()
{
sensor_console
::
defineCommand
(
"wifi_scan"
,
scanNetworks
,
F
(
"(Scans available WiFi networks)"
));
sensor_console
::
defineCommand
(
"local_ip"
,
showLocalIp
,
F
(
"(Displays local IP and current SSID)"
));
//NOTE: WiFi Multi could allow multiple SSID and passwords.
WiFi
.
persistent
(
false
);
// Don't write user & password to Flash.
WiFi
.
mode
(
WIFI_STA
);
// Set ESP to be a WiFi-client only
#if defined(ESP8266)
WiFi
.
hostname
(
hostname
);
#elif defined(ESP32)
WiFi
.
setHostname
(
hostname
);
#endif
Serial
.
print
(
F
(
"WiFi - Connecting to "
));
Serial
.
println
(
config
::
wifi_ssid
);
WiFi
.
begin
(
config
::
wifi_ssid
,
config
::
wifi_password
);
// Wait for connection, at most wifi_timeout seconds
for
(
int
i
=
0
;
i
<=
config
::
wifi_timeout
&&
(
WiFi
.
status
()
!=
WL_CONNECTED
);
i
++
)
{
led_effects
::
showRainbowWheel
();
Serial
.
print
(
"."
);
}
if
(
connected
())
{
led_effects
::
showKITTWheel
(
color
::
green
);
Serial
.
println
();
Serial
.
print
(
F
(
"WiFi - Connected! IP address: "
));
IPAddress
address
=
WiFi
.
localIP
();
snprintf
(
local_ip
,
sizeof
(
local_ip
),
"%d.%d.%d.%d"
,
address
[
0
],
address
[
1
],
address
[
2
],
address
[
3
]);
Serial
.
println
(
local_ip
);
}
else
{
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
led_effects
::
showKITTWheel
(
color
::
red
);
Serial
.
print
(
F
(
"Connection to WiFi failed! Status : "
));
Serial
.
println
(
WiFi
.
status
());
}
}
}
ampel-firmware/wifi_util.h
View file @
ea7407e0
...
...
@@ -3,8 +3,7 @@
namespace
wifi
{
extern
char
local_ip
[
16
];
void
connect
(
const
char
*
hostname
);
bool
connected
();
void
defineCommands
();
}
#endif
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment