almost
This commit is contained in:
parent
a03e890349
commit
14d8f7f683
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -701,6 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e98f0f58453dd2ce08d99228fc8757fad39d05dfd26643665d1093b8844f42cc"
|
checksum = "e98f0f58453dd2ce08d99228fc8757fad39d05dfd26643665d1093b8844f42cc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
|
"log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -792,6 +793,7 @@ dependencies = [
|
|||||||
"esp-println",
|
"esp-println",
|
||||||
"esp-wifi",
|
"esp-wifi",
|
||||||
"heapless 0.8.0",
|
"heapless 0.8.0",
|
||||||
|
"log",
|
||||||
"mqttrust",
|
"mqttrust",
|
||||||
"rand",
|
"rand",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
|
13
Cargo.toml
13
Cargo.toml
@ -8,7 +8,7 @@ authors = [ "Marvin Drescher <m@sparv.in>" ]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
embassy-executor = { version = "0.5.0", features = ["nightly", "integrated-timers", "arch-riscv32", "executor-thread"] }
|
embassy-executor = { version = "0.5.0", features = ["nightly", "integrated-timers", "arch-riscv32", "executor-thread"] }
|
||||||
embassy-net = { version = "0.4.0", features = ["dhcpv4", "dhcpv4-hostname", "dns", "medium-ip", "proto-ipv4", "proto-ipv6", "tcp"] }
|
embassy-net = { version = "0.4.0", features = ["dhcpv4", "dhcpv4-hostname", "dns", "medium-ip", "proto-ipv4", "proto-ipv6", "tcp", "udp"] }
|
||||||
embassy-sync = "0.5.0"
|
embassy-sync = "0.5.0"
|
||||||
embassy-time = { version = "0.3.0" }
|
embassy-time = { version = "0.3.0" }
|
||||||
embedded-io-async = "0.6.1"
|
embedded-io-async = "0.6.1"
|
||||||
@ -17,9 +17,10 @@ esp-alloc = "0.3.0"
|
|||||||
esp-backtrace = { version = "0.11.0", features = ["esp32c3", "exception-handler", "panic-handler", "println"] }
|
esp-backtrace = { version = "0.11.0", features = ["esp32c3", "exception-handler", "panic-handler", "println"] }
|
||||||
esp-hal = { version = "0.16.1", features = ["embassy", "embassy-time-timg0", "esp32c3"] }
|
esp-hal = { version = "0.16.1", features = ["embassy", "embassy-time-timg0", "esp32c3"] }
|
||||||
esp-hal-smartled = { version = "0.9.0", features = ["esp32c3"] }
|
esp-hal-smartled = { version = "0.9.0", features = ["esp32c3"] }
|
||||||
esp-println = { version = "0.9.1", features = ["esp32c3", "uart"] }
|
esp-println = { version = "0.9.1", features = ["esp32c3", "log", "uart"] }
|
||||||
esp-wifi = { version = "0.4.0", features = ["embassy-net", "esp32c3", "wifi"] }
|
esp-wifi = { version = "0.4.0", features = ["embassy-net", "esp32c3", "wifi"] }
|
||||||
heapless = { version = "0.8.0", features = ["portable-atomic", "portable-atomic-unsafe-assume-single-core"] }
|
heapless = { version = "0.8.0", features = ["portable-atomic", "portable-atomic-unsafe-assume-single-core"] }
|
||||||
|
log = "0.4.21"
|
||||||
mqttrust = "0.6.0"
|
mqttrust = "0.6.0"
|
||||||
rand = { version = "0.8.5", default-features = false, features = ["std_rng"] }
|
rand = { version = "0.8.5", default-features = false, features = ["std_rng"] }
|
||||||
rand_core = "0.6.4"
|
rand_core = "0.6.4"
|
||||||
@ -28,4 +29,10 @@ smart-leds = "0.4.0"
|
|||||||
static_cell = { version = "2.0.0", features = ["nightly"] }
|
static_cell = { version = "2.0.0", features = ["nightly"] }
|
||||||
|
|
||||||
[profile.dev.package.esp-wifi]
|
[profile.dev.package.esp-wifi]
|
||||||
opt-level = 2
|
opt-level = 3
|
||||||
|
[profile.dev.package.embedded-tls]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "s"
|
||||||
|
lto = true
|
||||||
|
53
src/main.rs
53
src/main.rs
@ -33,10 +33,12 @@ use esp_hal::{adc::AdcConfig, clock::ClockControl};
|
|||||||
use esp_hal::{embassy, prelude::*, rmt, Rmt, Rng, Rtc, IO};
|
use esp_hal::{embassy, prelude::*, rmt, Rmt, Rng, Rtc, IO};
|
||||||
use esp_hal_smartled::SmartLedsAdapter;
|
use esp_hal_smartled::SmartLedsAdapter;
|
||||||
use esp_hal_smartled::*;
|
use esp_hal_smartled::*;
|
||||||
|
use esp_println::logger::init_logger;
|
||||||
use esp_println::println;
|
use esp_println::println;
|
||||||
use esp_wifi::wifi::{get_random, ClientConfiguration, Configuration};
|
use esp_wifi::wifi::{get_random, ClientConfiguration, Configuration};
|
||||||
use esp_wifi::wifi::{WifiController, WifiDevice, WifiEvent, WifiStaDevice, WifiState};
|
use esp_wifi::wifi::{WifiController, WifiDevice, WifiEvent, WifiStaDevice, WifiState};
|
||||||
use esp_wifi::{initialize as initialize_wifi, EspWifiInitFor};
|
use esp_wifi::{initialize as initialize_wifi, EspWifiInitFor};
|
||||||
|
use log::{debug, error, info, trace};
|
||||||
use smart_leds::{SmartLedsWrite, RGB8};
|
use smart_leds::{SmartLedsWrite, RGB8};
|
||||||
use static_cell::{make_static, StaticCell};
|
use static_cell::{make_static, StaticCell};
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ impl<TX: rmt::TxChannel, const BUFFER_SIZE: usize> MySmartLed
|
|||||||
{
|
{
|
||||||
fn set_color(&mut self, color: RGB8) {
|
fn set_color(&mut self, color: RGB8) {
|
||||||
if let Err(err) = self.write([color].into_iter()) {
|
if let Err(err) = self.write([color].into_iter()) {
|
||||||
esp_println::println!("{err:?}");
|
error!("{err:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,7 +82,6 @@ pub static DATA: Mutex<CriticalSectionRawMutex, BTreeMap<Cow<'static, str>, Stri
|
|||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
async fn blink(mut led: Box<dyn MySmartLed>) {
|
async fn blink(mut led: Box<dyn MySmartLed>) {
|
||||||
loop {
|
loop {
|
||||||
esp_println::println!("Bing!");
|
|
||||||
let scale = 4;
|
let scale = 4;
|
||||||
for r in 0..(255 / scale) {
|
for r in 0..(255 / scale) {
|
||||||
Timer::after(Duration::from_millis(1)).await;
|
Timer::after(Duration::from_millis(1)).await;
|
||||||
@ -100,17 +101,25 @@ async fn blink(mut led: Box<dyn MySmartLed>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! nb_await {
|
||||||
|
($ex:expr, $interval:expr) => {
|
||||||
|
loop {
|
||||||
|
match $ex {
|
||||||
|
Err(nb::Error::WouldBlock) => Timer::after($interval).await,
|
||||||
|
Err(nb::Error::Other(e)) => break Err(e),
|
||||||
|
Ok(val) => break Ok(val),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($ex:expr) => {
|
||||||
|
$crate::nb_await!($ex, Duration::from_millis(10))
|
||||||
|
};
|
||||||
|
}
|
||||||
async fn async_read<T, E>(
|
async fn async_read<T, E>(
|
||||||
mut fun: impl FnMut() -> nb::Result<T, E>,
|
mut fun: impl FnMut() -> nb::Result<T, E>,
|
||||||
interval: Duration,
|
interval: Duration,
|
||||||
) -> Result<T, E> {
|
) -> Result<T, E> {
|
||||||
loop {
|
nb_await!(fun(), interval)
|
||||||
match fun() {
|
|
||||||
Err(nb::Error::WouldBlock) => Timer::after(interval).await,
|
|
||||||
Err(nb::Error::Other(e)) => return Err(e),
|
|
||||||
Ok(val) => return Ok(val),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32c3/api-reference/peripherals/adc.html
|
/// https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32c3/api-reference/peripherals/adc.html
|
||||||
@ -148,7 +157,7 @@ async fn moisture(
|
|||||||
let moisture = ((milli_volt.checked_sub(submerged_in_water).unwrap_or(0))
|
let moisture = ((milli_volt.checked_sub(submerged_in_water).unwrap_or(0))
|
||||||
<< 8 / (dry - submerged_in_water))
|
<< 8 / (dry - submerged_in_water))
|
||||||
>> 8;
|
>> 8;
|
||||||
esp_println::println!("moisture: {moisture}%, v_s: {milli_volt}");
|
info!("moisture: {moisture}%, v_s: {milli_volt}");
|
||||||
{
|
{
|
||||||
DATA.lock()
|
DATA.lock()
|
||||||
.await
|
.await
|
||||||
@ -174,7 +183,7 @@ async fn battery_monitor(
|
|||||||
);
|
);
|
||||||
// account for 50:50 voltage divider
|
// account for 50:50 voltage divider
|
||||||
let v_bat = v_out * 2;
|
let v_bat = v_out * 2;
|
||||||
println!("V_bat: {}", v_bat);
|
info!("V_bat: {}", v_bat);
|
||||||
{
|
{
|
||||||
DATA.lock()
|
DATA.lock()
|
||||||
.await
|
.await
|
||||||
@ -187,7 +196,7 @@ static EXECUTOR: StaticCell<Executor> = StaticCell::new();
|
|||||||
|
|
||||||
#[entry]
|
#[entry]
|
||||||
fn main() -> ! {
|
fn main() -> ! {
|
||||||
esp_println::println!("Init!");
|
init_logger(log::LevelFilter::Info);
|
||||||
init_heap();
|
init_heap();
|
||||||
|
|
||||||
let peripherals = Peripherals::take();
|
let peripherals = Peripherals::take();
|
||||||
@ -270,7 +279,7 @@ fn main() -> ! {
|
|||||||
spawner.spawn(net_task(&stack)).unwrap();
|
spawner.spawn(net_task(&stack)).unwrap();
|
||||||
spawner.spawn(ip_task(&stack)).unwrap();
|
spawner.spawn(ip_task(&stack)).unwrap();
|
||||||
}
|
}
|
||||||
spawner.spawn(blink(led)).unwrap();
|
// spawner.spawn(blink(led)).unwrap();
|
||||||
spawner
|
spawner
|
||||||
.spawn(moisture(pin, adc1, moisture_sensor_suppy_pin.into()))
|
.spawn(moisture(pin, adc1, moisture_sensor_suppy_pin.into()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -285,8 +294,8 @@ async fn wifi_connection(
|
|||||||
ssid: &'static str,
|
ssid: &'static str,
|
||||||
psk: &'static str,
|
psk: &'static str,
|
||||||
) {
|
) {
|
||||||
println!("start connection task");
|
trace!("start wifi connection task");
|
||||||
println!("Device capabilities: {:?}", controller.get_capabilities());
|
debug!("Device capabilities: {:?}", controller.get_capabilities());
|
||||||
loop {
|
loop {
|
||||||
match esp_wifi::wifi::get_wifi_state() {
|
match esp_wifi::wifi::get_wifi_state() {
|
||||||
WifiState::StaConnected => {
|
WifiState::StaConnected => {
|
||||||
@ -303,16 +312,16 @@ async fn wifi_connection(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
controller.set_configuration(&client_config).unwrap();
|
controller.set_configuration(&client_config).unwrap();
|
||||||
println!("Starting wifi");
|
trace!("starting wifi");
|
||||||
controller.start().await.unwrap();
|
controller.start().await.unwrap();
|
||||||
println!("Wifi started!");
|
trace!("Wifi started!");
|
||||||
}
|
}
|
||||||
println!("About to connect...");
|
trace!("About to connect...");
|
||||||
|
|
||||||
match controller.connect().await {
|
match controller.connect().await {
|
||||||
Ok(_) => println!("Wifi connected!"),
|
Ok(_) => info!("Wifi connected!"),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Failed to connect to wifi: {e:?}");
|
error!("Failed to connect to wifi: {e:?}");
|
||||||
Timer::after(Duration::from_millis(5000)).await
|
Timer::after(Duration::from_millis(5000)).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -327,10 +336,10 @@ async fn ip_task(stack: NetworkStack) {
|
|||||||
}
|
}
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
}
|
}
|
||||||
println!("Waiting to get IP address...");
|
debug!("Waiting to get IP address...");
|
||||||
loop {
|
loop {
|
||||||
if let Some(config) = stack.config_v4() {
|
if let Some(config) = stack.config_v4() {
|
||||||
println!("Got IP: {}", config.address);
|
info!("Got IP: {}", config.address);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
Timer::after(Duration::from_millis(500)).await;
|
||||||
|
45
src/mqtt.rs
45
src/mqtt.rs
@ -1,8 +1,10 @@
|
|||||||
use embassy_net::tcp::TcpSocket;
|
use embassy_net::tcp::TcpSocket;
|
||||||
use embassy_net::{dns::Error as DnsError, tcp::ConnectError};
|
use embassy_net::{dns::Error as DnsError, tcp::ConnectError};
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{with_timeout, Duration, TimeoutError, Timer};
|
||||||
use embedded_tls::{Aes128GcmSha256, NoVerify, TlsConfig, TlsConnection, TlsContext, TlsError};
|
use embedded_tls::{Aes128GcmSha256, NoVerify, TlsConfig, TlsConnection, TlsContext, TlsError};
|
||||||
|
use esp_backtrace as _;
|
||||||
use esp_println::println;
|
use esp_println::println;
|
||||||
|
use log::{debug, error, info, trace};
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::{CryptoRng, RngCore, SeedableRng};
|
use rand::{CryptoRng, RngCore, SeedableRng};
|
||||||
use rust_mqtt::client::client::MqttClient;
|
use rust_mqtt::client::client::MqttClient;
|
||||||
@ -19,6 +21,7 @@ pub enum SendError {
|
|||||||
Tls(TlsError),
|
Tls(TlsError),
|
||||||
Connect(ConnectError),
|
Connect(ConnectError),
|
||||||
MqttReason(ReasonCode),
|
MqttReason(ReasonCode),
|
||||||
|
Timeout(TimeoutError),
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! from_impl {
|
macro_rules! from_impl {
|
||||||
@ -35,6 +38,7 @@ from_impl!(DnsError, Dns);
|
|||||||
from_impl!(TlsError, Tls);
|
from_impl!(TlsError, Tls);
|
||||||
from_impl!(ConnectError, Connect);
|
from_impl!(ConnectError, Connect);
|
||||||
from_impl!(ReasonCode, MqttReason);
|
from_impl!(ReasonCode, MqttReason);
|
||||||
|
from_impl!(TimeoutError, Timeout);
|
||||||
|
|
||||||
const MQTT_SERVER_HOSTNAME: &str = "mqtt.shimun.net";
|
const MQTT_SERVER_HOSTNAME: &str = "mqtt.shimun.net";
|
||||||
const MQTT_SERVER_PORT: u16 = 8883;
|
const MQTT_SERVER_PORT: u16 = 8883;
|
||||||
@ -43,23 +47,32 @@ pub async fn send_message(
|
|||||||
stack: NetworkStack,
|
stack: NetworkStack,
|
||||||
mut messages: impl Iterator<Item = (&str, &[u8])>,
|
mut messages: impl Iterator<Item = (&str, &[u8])>,
|
||||||
mut rng: impl CryptoRng + RngCore,
|
mut rng: impl CryptoRng + RngCore,
|
||||||
|
) -> core::result::Result<(), SendError> {
|
||||||
|
async fn inner(
|
||||||
|
stack: NetworkStack,
|
||||||
|
mut messages: impl Iterator<Item = (&str, &[u8])>,
|
||||||
|
mut rng: impl CryptoRng + RngCore,
|
||||||
) -> core::result::Result<(), SendError> {
|
) -> core::result::Result<(), SendError> {
|
||||||
let dns_resp = stack
|
let dns_resp = stack
|
||||||
.dns_query(MQTT_SERVER_HOSTNAME, embassy_net::dns::DnsQueryType::A)
|
.dns_query(MQTT_SERVER_HOSTNAME, embassy_net::dns::DnsQueryType::A)
|
||||||
.await
|
.await
|
||||||
.map_err(SendError::Dns)?;
|
.map_err(SendError::Dns)?;
|
||||||
let mut rx_buffer = [0; 4096];
|
const TCP_BUFLEN: usize = 2048 << 2;
|
||||||
let mut tx_buffer = [0; 4096];
|
let mut rx_buffer = [0; TCP_BUFLEN];
|
||||||
|
let mut tx_buffer = [0; TCP_BUFLEN];
|
||||||
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
|
||||||
socket.set_timeout(Some(Duration::from_secs(10)));
|
socket.set_timeout(Some(Duration::from_secs(30)));
|
||||||
let socket_addr = dns_resp
|
let socket_addr = dns_resp
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|addr| (addr, MQTT_SERVER_PORT))
|
.map(|addr| (addr, MQTT_SERVER_PORT))
|
||||||
.next()
|
.next()
|
||||||
.ok_or(SendError::NXDomain(MQTT_SERVER_HOSTNAME))?;
|
.ok_or(SendError::NXDomain(MQTT_SERVER_HOSTNAME))?;
|
||||||
|
|
||||||
|
debug!("got address {socket_addr:?}");
|
||||||
|
|
||||||
// establish TCP connection
|
// establish TCP connection
|
||||||
socket.connect(socket_addr).await?;
|
socket.connect(socket_addr).await?;
|
||||||
|
trace!("connected");
|
||||||
|
|
||||||
// seed mqtt rng from rng
|
// seed mqtt rng from rng
|
||||||
let mut mqtt_config = ClientConfig::<5, _>::new(
|
let mut mqtt_config = ClientConfig::<5, _>::new(
|
||||||
@ -73,13 +86,11 @@ pub async fn send_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TLS layer
|
// TLS layer
|
||||||
const TLS_BUF_LEN: usize = 1 << 12;
|
const TLS_BUF_LEN: usize = 4096;
|
||||||
let mut tls_read_record_buffer = [0; TLS_BUF_LEN];
|
let mut tls_read_record_buffer = [0; TLS_BUF_LEN];
|
||||||
let mut tls_write_record_buffer = [0; TLS_BUF_LEN];
|
let mut tls_write_record_buffer = [0; TLS_BUF_LEN];
|
||||||
let mut tls = {
|
let mut tls = {
|
||||||
let config = TlsConfig::new()
|
let config = TlsConfig::new();
|
||||||
.with_server_name(MQTT_SERVER_HOSTNAME)
|
|
||||||
.with_max_fragment_length(embedded_tls::MaxFragmentLength::Bits12);
|
|
||||||
|
|
||||||
let mut tls = TlsConnection::new(
|
let mut tls = TlsConnection::new(
|
||||||
socket,
|
socket,
|
||||||
@ -91,11 +102,14 @@ pub async fn send_message(
|
|||||||
.await?;
|
.await?;
|
||||||
tls
|
tls
|
||||||
};
|
};
|
||||||
|
debug!("tls handshake succeeded");
|
||||||
|
|
||||||
const BUF_LEN: usize = 1 << 13;
|
const BUF_LEN: usize = 1024;
|
||||||
let mut recv_buffer = [0; BUF_LEN];
|
let mut recv_buffer = [0; BUF_LEN];
|
||||||
let mut buffer = [0; BUF_LEN];
|
let mut buffer = [0; BUF_LEN];
|
||||||
|
|
||||||
|
mqtt_config.add_client_id("esp32c3");
|
||||||
|
mqtt_config.max_packet_size = (BUF_LEN - 128) as _;
|
||||||
// MQTT Layer
|
// MQTT Layer
|
||||||
let mut mqtt_client = MqttClient::new(
|
let mut mqtt_client = MqttClient::new(
|
||||||
tls,
|
tls,
|
||||||
@ -106,6 +120,7 @@ pub async fn send_message(
|
|||||||
mqtt_config,
|
mqtt_config,
|
||||||
);
|
);
|
||||||
mqtt_client.connect_to_broker().await?;
|
mqtt_client.connect_to_broker().await?;
|
||||||
|
info!("connected to broker");
|
||||||
for (topic, message) in messages {
|
for (topic, message) in messages {
|
||||||
mqtt_client
|
mqtt_client
|
||||||
.send_message(
|
.send_message(
|
||||||
@ -118,12 +133,19 @@ pub async fn send_message(
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
with_timeout(Duration::from_secs(60), inner(stack, messages, rng)).await?
|
||||||
|
}
|
||||||
|
|
||||||
#[embassy_executor::task]
|
#[embassy_executor::task]
|
||||||
pub async fn publish_data(stack: NetworkStack, rng_seed: [u8; 32], interval: Duration) {
|
pub async fn publish_data(stack: NetworkStack, rng_seed: [u8; 32], interval: Duration) {
|
||||||
let mut rng = StdRng::from_seed(rng_seed);
|
let mut rng = StdRng::from_seed(rng_seed);
|
||||||
loop {
|
loop {
|
||||||
Timer::after(interval).await;
|
Timer::after(interval / 10).await;
|
||||||
|
if stack.is_config_up() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop {
|
||||||
let mut data = DATA.lock().await;
|
let mut data = DATA.lock().await;
|
||||||
let res = send_message(
|
let res = send_message(
|
||||||
stack,
|
stack,
|
||||||
@ -133,8 +155,9 @@ pub async fn publish_data(stack: NetworkStack, rng_seed: [u8; 32], interval: Dur
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
println!("Oh no: {e:?}");
|
error!("Oh no: {e:?}");
|
||||||
};
|
};
|
||||||
data.clear();
|
data.clear();
|
||||||
|
Timer::after(interval).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user