WIP: Secondary display in wokwi #43
Generated
+81
@@ -321,6 +321,7 @@ dependencies = [
|
|||||||
"base64 0.23.1",
|
"base64 0.23.1",
|
||||||
"ch1115",
|
"ch1115",
|
||||||
"critical-section",
|
"critical-section",
|
||||||
|
"display-interface",
|
||||||
"display-interface-spi",
|
"display-interface-spi",
|
||||||
"embassy-executor",
|
"embassy-executor",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
@@ -343,6 +344,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"pn532",
|
"pn532",
|
||||||
|
"ssd1306",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -607,6 +609,17 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0"
|
checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "display-interface-i2c"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d964fa85bbbb5a6ecd06e58699407ac5dc3e3ad72dac0ab7e6b0d00a1cd262d"
|
||||||
|
dependencies = [
|
||||||
|
"display-interface",
|
||||||
|
"embedded-hal 1.0.0",
|
||||||
|
"embedded-hal-async",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "display-interface-spi"
|
name = "display-interface-spi"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1942,6 +1955,19 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "maybe-async-cfg"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1e083394889336bc66a4eaf1011ffbfa74893e910f902a9f271fa624c61e1b2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error",
|
||||||
|
"proc-macro2",
|
||||||
|
"pulldown-cmark",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.3"
|
version = "2.8.3"
|
||||||
@@ -2690,6 +2716,30 @@ dependencies = [
|
|||||||
"toml_edit",
|
"toml_edit",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-error-attr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 1.0.109",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro-error-attr"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -2719,6 +2769,17 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "679341d22c78c6c649893cbd6c3278dcbe9fc4faa62fea3a9296ae2b50c14625"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"memchr",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.47"
|
||||||
@@ -3133,6 +3194,20 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0f368519fc6c85fc1afdb769fb5a51123f6158013e143656e25a3485a0d401c"
|
checksum = "a0f368519fc6c85fc1afdb769fb5a51123f6158013e143656e25a3485a0d401c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssd1306"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ea6aac2d078bbc71d9b8ac3f657335311f3b6625e9a1a96ccc29f5abfa77c56"
|
||||||
|
dependencies = [
|
||||||
|
"display-interface",
|
||||||
|
"display-interface-i2c",
|
||||||
|
"display-interface-spi",
|
||||||
|
"embedded-graphics-core",
|
||||||
|
"embedded-hal 1.0.0",
|
||||||
|
"maybe-async-cfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -3400,6 +3475,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69"
|
checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-id-start"
|
name = "unicode-id-start"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ esp-hal-wifimanager = { git = "https://github.com/InvisibleUSA/esp-hal-wifimanag
|
|||||||
] }
|
] }
|
||||||
esp-println = { version = "0.17.0", features = ["esp32s3", "log-04"] }
|
esp-println = { version = "0.17.0", features = ["esp32s3", "log-04"] }
|
||||||
display-interface-spi = "0.5.0"
|
display-interface-spi = "0.5.0"
|
||||||
|
display-interface = "*"
|
||||||
ili9341 = "0.6.0"
|
ili9341 = "0.6.0"
|
||||||
embedded-hal = "1.0.0"
|
embedded-hal = "1.0.0"
|
||||||
embedded-hal-bus = "0.3.0"
|
embedded-hal-bus = "0.3.0"
|
||||||
@@ -47,6 +48,7 @@ embassy-sync = "0.8.0"
|
|||||||
embedded-hal-async = "1.0.0"
|
embedded-hal-async = "1.0.0"
|
||||||
i2c-character-display = "0.5.1"
|
i2c-character-display = "0.5.1"
|
||||||
ch1115 = { version = "0.1.2", features = ["graphics"] }
|
ch1115 = { version = "0.1.2", features = ["graphics"] }
|
||||||
|
ssd1306 = { version = "0.10.0", features = [] }
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|||||||
+1
-1
@@ -141,7 +141,7 @@ async fn main(spawner: Spawner) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn display_shit(oled: &mut SecondaryDisplay<'static>, text: &str) {
|
fn display_shit(oled: &mut SecondaryDisplay<'static>, text: &str) {
|
||||||
oled.clear().unwrap();
|
oled.clear(BinaryColor::Off).unwrap();
|
||||||
|
|
||||||
let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On);
|
||||||
Text::new(text, Point::new(10, 20), text_style)
|
Text::new(text, Point::new(10, 20), text_style)
|
||||||
|
|||||||
+166
-31
@@ -1,6 +1,6 @@
|
|||||||
use alloc::format;
|
use alloc::{format, string::String};
|
||||||
use ch1115::{Ch1115, Size128x64};
|
|
||||||
use display_interface_spi::SPIInterface;
|
use display_interface_spi::SPIInterface;
|
||||||
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
use embedded_hal_bus::spi::AtomicDevice;
|
use embedded_hal_bus::spi::AtomicDevice;
|
||||||
use embedded_hal_bus::util::AtomicCell;
|
use embedded_hal_bus::util::AtomicCell;
|
||||||
use esp_hal::delay::Delay;
|
use esp_hal::delay::Delay;
|
||||||
@@ -8,10 +8,72 @@ use esp_hal::gpio::Output;
|
|||||||
|
|
||||||
use crate::drivers::spi_bus::{DisplaySpiBus, SharedOutput, SharedReset};
|
use crate::drivers::spi_bus::{DisplaySpiBus, SharedOutput, SharedReset};
|
||||||
|
|
||||||
pub type GenericSecondaryDisplay<'a, BUS, RST, DC> =
|
pub struct SecondaryDisplay<'a>(
|
||||||
Ch1115<SPIInterface<AtomicDevice<'a, BUS, Output<'a>, Delay>, DC>, RST, Size128x64>;
|
#[cfg(feature = "wokwi")] ssd1306_aliases::SecondaryDisplay<'a>,
|
||||||
pub type SecondaryDisplay<'a> =
|
#[cfg(not(feature = "wokwi"))] ch1115_aliases::SecondaryDisplay<'a>,
|
||||||
GenericSecondaryDisplay<'a, DisplaySpiBus, SharedReset<'a>, SharedOutput<'a>>;
|
);
|
||||||
|
|
||||||
|
impl<'a> SecondaryDisplay<'a> {
|
||||||
|
pub fn flush(&mut self) -> Result<(), String> {
|
||||||
|
#[cfg(not(feature = "wokwi"))]
|
||||||
|
return self.0.flush().map_err(|e| format!("{e:?}"));
|
||||||
|
#[cfg(feature = "wokwi")]
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> embedded_graphics::geometry::Dimensions for SecondaryDisplay<'a> {
|
||||||
|
fn bounding_box(&self) -> embedded_graphics::primitives::Rectangle {
|
||||||
|
self.0.bounding_box()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> embedded_graphics::draw_target::DrawTarget for SecondaryDisplay<'a> {
|
||||||
|
type Color = BinaryColor;
|
||||||
|
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = embedded_graphics::prelude::Pixel<Self::Color>>,
|
||||||
|
{
|
||||||
|
self.0.draw_iter(pixels).map_err(|e| format!("{e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_contiguous<I>(
|
||||||
|
&mut self,
|
||||||
|
area: &embedded_graphics::primitives::Rectangle,
|
||||||
|
colors: I,
|
||||||
|
) -> Result<(), Self::Error>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Self::Color>,
|
||||||
|
{
|
||||||
|
self.0
|
||||||
|
.fill_contiguous(area, colors)
|
||||||
|
.map_err(|e| format!("{e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_solid(
|
||||||
|
&mut self,
|
||||||
|
area: &embedded_graphics::primitives::Rectangle,
|
||||||
|
color: Self::Color,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
self.0.fill_solid(area, color).map_err(|e| format!("{e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
|
||||||
|
#[cfg(feature = "wokwi")]
|
||||||
|
return self.0.clear(color).map_err(|e| format!("{e:?}"));
|
||||||
|
|
||||||
|
#[cfg(not(feature = "wokwi"))]
|
||||||
|
{
|
||||||
|
let _ = color;
|
||||||
|
self.0.clear().map_err(|e| format!("{e:?}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const SECONDARY_DISPLAY_COUNT: usize = 3;
|
||||||
|
|
||||||
pub struct SecondaryDisplayPinConfiguration<'a> {
|
pub struct SecondaryDisplayPinConfiguration<'a> {
|
||||||
pub spi_bus: &'a AtomicCell<DisplaySpiBus>,
|
pub spi_bus: &'a AtomicCell<DisplaySpiBus>,
|
||||||
@@ -21,39 +83,112 @@ pub struct SecondaryDisplayPinConfiguration<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SecondaryDisplayPinConfiguration<'a> {
|
impl<'a> SecondaryDisplayPinConfiguration<'a> {
|
||||||
pub fn build(self) -> [SecondaryDisplay<'a>; 3] {
|
pub fn build(self) -> [SecondaryDisplay<'a>; SECONDARY_DISPLAY_COUNT] {
|
||||||
let mut displays = self.cs_pins.map(|cs| {
|
#[cfg(feature = "wokwi")]
|
||||||
let spi_dev = AtomicDevice::new(self.spi_bus, cs, Delay::new()).unwrap();
|
type Displays<'b> = [ssd1306_aliases::SecondaryDisplay<'b>; SECONDARY_DISPLAY_COUNT];
|
||||||
let interface = SPIInterface::new(spi_dev, self.dc.clone());
|
#[cfg(not(feature = "wokwi"))]
|
||||||
|
type Displays<'b> = [ch1115_aliases::SecondaryDisplay<'b>; SECONDARY_DISPLAY_COUNT];
|
||||||
|
|
||||||
Ch1115::new(interface, self.res.clone(), Size128x64)
|
let reset_pin = self.res.clone();
|
||||||
});
|
let mut displays = Displays::new(self);
|
||||||
|
displays.reset(reset_pin);
|
||||||
|
|
||||||
init_single_reset(&mut displays, self.res);
|
displays.map(SecondaryDisplay)
|
||||||
|
|
||||||
displays
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init_single_reset<'a: 'b, 'b>(
|
trait MultiDisplay<'a> {
|
||||||
displays: impl IntoIterator<Item = &'b mut SecondaryDisplay<'a>>,
|
fn new(config: SecondaryDisplayPinConfiguration<'a>) -> Self;
|
||||||
shared_reset: SharedReset<'a>,
|
fn reset(&mut self, shared_reset: SharedReset<'a>);
|
||||||
) {
|
}
|
||||||
let mut displays = displays.into_iter().peekable();
|
|
||||||
|
|
||||||
// Reset all secondaries via shared reset
|
mod ch1115_aliases {
|
||||||
let Some(any_display) = displays.peek_mut() else {
|
use super::*;
|
||||||
// no display to initialize
|
use ch1115::{Ch1115, Size128x64};
|
||||||
return;
|
|
||||||
|
type GenericSecondaryDisplay<'a, BUS, RST, DC> =
|
||||||
|
Ch1115<SPIInterface<AtomicDevice<'a, BUS, Output<'a>, Delay>, DC>, RST, Size128x64>;
|
||||||
|
pub type SecondaryDisplay<'a> =
|
||||||
|
GenericSecondaryDisplay<'a, DisplaySpiBus, SharedReset<'a>, SharedOutput<'a>>;
|
||||||
|
|
||||||
|
impl<'a> MultiDisplay<'a> for [SecondaryDisplay<'a>; SECONDARY_DISPLAY_COUNT] {
|
||||||
|
fn new(config: SecondaryDisplayPinConfiguration<'a>) -> Self {
|
||||||
|
config.cs_pins.map(|cs| {
|
||||||
|
let spi_dev = AtomicDevice::new(config.spi_bus, cs, Delay::new()).unwrap();
|
||||||
|
let interface = SPIInterface::new(spi_dev, config.dc.clone());
|
||||||
|
|
||||||
|
SecondaryDisplay::new(interface, config.res.clone(), Size128x64)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, shared_reset: SharedReset<'a>) {
|
||||||
|
let mut displays = self.iter_mut().peekable();
|
||||||
|
|
||||||
|
// Reset all secondaries via shared reset
|
||||||
|
let Some(any_display) = displays.peek_mut() else {
|
||||||
|
// no display to initialize
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
any_display.hard_reset(&mut Delay::new()).unwrap();
|
||||||
|
|
||||||
|
shared_reset.disable(true);
|
||||||
|
|
||||||
|
for (i, display) in displays.enumerate() {
|
||||||
|
display
|
||||||
|
.init(&mut Delay::new())
|
||||||
|
.map_err(|e| format!("Failed to init secondary display {}: {e:?}", i + 1))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod ssd1306_aliases {
|
||||||
|
use super::*;
|
||||||
|
use ssd1306::{
|
||||||
|
Ssd1306, mode::BufferedGraphicsMode, rotation::DisplayRotation, size::DisplaySize128x64,
|
||||||
};
|
};
|
||||||
any_display.hard_reset(&mut Delay::new()).unwrap();
|
|
||||||
|
|
||||||
shared_reset.disable(true);
|
type GenericSecondaryDisplay<'a, BUS, DC> = Ssd1306<
|
||||||
|
SPIInterface<AtomicDevice<'a, BUS, Output<'a>, Delay>, DC>,
|
||||||
|
DisplaySize128x64,
|
||||||
|
BufferedGraphicsMode<DisplaySize128x64>,
|
||||||
|
>;
|
||||||
|
pub type SecondaryDisplay<'a> = GenericSecondaryDisplay<'a, DisplaySpiBus, SharedOutput<'a>>;
|
||||||
|
|
||||||
for (i, display) in displays.enumerate() {
|
impl<'a> MultiDisplay<'a> for [SecondaryDisplay<'a>; SECONDARY_DISPLAY_COUNT] {
|
||||||
display
|
fn new(config: SecondaryDisplayPinConfiguration<'a>) -> Self {
|
||||||
.init(&mut Delay::new())
|
config.cs_pins.map(|cs| {
|
||||||
.map_err(|e| format!("Failed to init secondary display {}: {e:?}", i + 1))
|
let spi_dev = AtomicDevice::new(config.spi_bus, cs, Delay::new()).unwrap();
|
||||||
.unwrap();
|
let interface = SPIInterface::new(spi_dev, config.dc.clone());
|
||||||
|
|
||||||
|
Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0)
|
||||||
|
.into_buffered_graphics_mode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, mut shared_reset: SharedReset<'a>) {
|
||||||
|
use ssd1306::mode::DisplayConfig as _;
|
||||||
|
|
||||||
|
let mut displays = self.iter_mut().peekable();
|
||||||
|
|
||||||
|
// Reset all secondaries via shared reset
|
||||||
|
let Some(any_display) = displays.peek_mut() else {
|
||||||
|
// no display to initialize
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
any_display
|
||||||
|
.reset(&mut shared_reset, &mut Delay::new())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
shared_reset.disable(true);
|
||||||
|
|
||||||
|
for (i, display) in displays.enumerate() {
|
||||||
|
display
|
||||||
|
.init()
|
||||||
|
.map_err(|e| format!("Failed to init secondary display {}: {e:?}", i + 1))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use embedded_graphics::{
|
use embedded_graphics::{
|
||||||
draw_target::DrawTarget as _,
|
draw_target::DrawTarget as _, pixelcolor::{BinaryColor, Rgb565, RgbColor as _},
|
||||||
pixelcolor::{Rgb565, RgbColor as _},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::drivers::{
|
use crate::drivers::{
|
||||||
@@ -19,9 +18,9 @@ pub struct Outputs {
|
|||||||
impl Outputs {
|
impl Outputs {
|
||||||
pub fn clear_screens(&mut self) {
|
pub fn clear_screens(&mut self) {
|
||||||
self.primary_display.clear(Rgb565::BLACK).unwrap();
|
self.primary_display.clear(Rgb565::BLACK).unwrap();
|
||||||
self.secondary_display_1.clear().unwrap();
|
self.secondary_display_1.clear(BinaryColor::Off).unwrap();
|
||||||
// self.secondary_display_2.clear().unwrap();
|
self.secondary_display_2.clear(BinaryColor::Off).unwrap();
|
||||||
// self.secondary_display_3.clear().unwrap();
|
self.secondary_display_3.clear(BinaryColor::Off).unwrap();
|
||||||
self.tertiary_display.clear().unwrap();
|
self.tertiary_display.clear().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user