Improve the blinker animations and state transitions when switching blinkers
This commit is contained in:
parent
ef5415303b
commit
54c4b99ab6
|
@ -11,13 +11,13 @@ use embedded_alloc::Heap;
|
||||||
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin};
|
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin};
|
||||||
use fixed::types::I16F16;
|
use fixed::types::I16F16;
|
||||||
use fugit::RateExtU32;
|
use fugit::RateExtU32;
|
||||||
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, State, FPS, UI};
|
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, FPS, UI};
|
||||||
use panic_halt as _;
|
use panic_halt as _;
|
||||||
use rp_pico::{
|
use rp_pico::{
|
||||||
entry,
|
entry,
|
||||||
hal::{
|
hal::{
|
||||||
clocks::init_clocks_and_plls,
|
clocks::init_clocks_and_plls,
|
||||||
gpio::{FunctionSio, Pin, PinId, PullDown, SioInput},
|
gpio::{FunctionSio, Pin, PinId, PullUp, SioInput},
|
||||||
pac::{CorePeripherals, Peripherals},
|
pac::{CorePeripherals, Peripherals},
|
||||||
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
|
||||||
watchdog::Watchdog,
|
watchdog::Watchdog,
|
||||||
|
@ -30,7 +30,8 @@ use rp_pico::{
|
||||||
static HEAP: Heap = Heap::empty();
|
static HEAP: Heap = Heap::empty();
|
||||||
|
|
||||||
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
|
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
|
||||||
const GLOBAL_BRIGHTESS: u8 = 1;
|
const DASHBOARD_BRIGHTESS: u8 = 1;
|
||||||
|
const BODY_BRIGHTNESS: u8 = 8;
|
||||||
|
|
||||||
struct BikeUI<
|
struct BikeUI<
|
||||||
D: SpiDevice,
|
D: SpiDevice,
|
||||||
|
@ -41,10 +42,10 @@ struct BikeUI<
|
||||||
NextId: PinId,
|
NextId: PinId,
|
||||||
> {
|
> {
|
||||||
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
spi: RefCell<Spi<Enabled, D, P, 8>>,
|
||||||
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullDown>,
|
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
|
||||||
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullDown>,
|
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||||
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullDown>,
|
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||||
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullDown>,
|
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<
|
impl<
|
||||||
|
@ -58,10 +59,10 @@ impl<
|
||||||
{
|
{
|
||||||
fn new(
|
fn new(
|
||||||
spi: Spi<Enabled, D, P, 8>,
|
spi: Spi<Enabled, D, P, 8>,
|
||||||
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullDown>,
|
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
|
||||||
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullDown>,
|
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
|
||||||
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullDown>,
|
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
|
||||||
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullDown>,
|
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
spi: RefCell::new(spi),
|
spi: RefCell::new(spi),
|
||||||
|
@ -83,13 +84,13 @@ impl<
|
||||||
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId>
|
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId>
|
||||||
{
|
{
|
||||||
fn check_event(&self) -> Option<Event> {
|
fn check_event(&self) -> Option<Event> {
|
||||||
if self.left_blinker_button.is_high().unwrap_or(false) {
|
if self.left_blinker_button.is_low().unwrap_or(false) {
|
||||||
Some(Event::LeftBlinker)
|
Some(Event::LeftBlinker)
|
||||||
} else if self.right_blinker_button.is_high().unwrap_or(false) {
|
} else if self.right_blinker_button.is_low().unwrap_or(false) {
|
||||||
Some(Event::RightBlinker)
|
Some(Event::RightBlinker)
|
||||||
} else if self.previous_animation_button.is_high().unwrap_or(false) {
|
} else if self.previous_animation_button.is_low().unwrap_or(false) {
|
||||||
Some(Event::PreviousPattern)
|
Some(Event::PreviousPattern)
|
||||||
} else if self.next_animation_button.is_high().unwrap_or(false) {
|
} else if self.next_animation_button.is_low().unwrap_or(false) {
|
||||||
Some(Event::NextPattern)
|
Some(Event::NextPattern)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -98,14 +99,12 @@ impl<
|
||||||
|
|
||||||
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
|
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
|
||||||
let mut lights: [u8; 20] = [0; 20];
|
let mut lights: [u8; 20] = [0; 20];
|
||||||
// Check https://www.pololu.com/product/3089 for the end frame calculations. It is not what
|
|
||||||
// I thought.
|
|
||||||
lights[16] = 0xff;
|
lights[16] = 0xff;
|
||||||
lights[17] = 0xff;
|
lights[17] = 0xff;
|
||||||
lights[18] = 0xff;
|
lights[18] = 0xff;
|
||||||
lights[19] = 0xff;
|
lights[19] = 0xff;
|
||||||
for (idx, rgb) in dashboard_lights.iter().enumerate() {
|
for (idx, rgb) in dashboard_lights.iter().enumerate() {
|
||||||
lights[(idx + 1) * 4 + 0] = 0xe0 + GLOBAL_BRIGHTESS;
|
lights[(idx + 1) * 4 + 0] = 0xe0 + DASHBOARD_BRIGHTESS;
|
||||||
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
|
||||||
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
|
||||||
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
|
||||||
|
@ -159,10 +158,10 @@ fn main() -> ! {
|
||||||
embedded_hal::spi::MODE_1,
|
embedded_hal::spi::MODE_1,
|
||||||
);
|
);
|
||||||
|
|
||||||
let left_blinker_button = pins.gpio18.into_pull_down_input();
|
let left_blinker_button = pins.gpio18.into_pull_up_input();
|
||||||
let right_blinker_button = pins.gpio19.into_function();
|
let right_blinker_button = pins.gpio19.into_pull_up_input();
|
||||||
let previous_animation_button = pins.gpio20.into_function();
|
let previous_animation_button = pins.gpio20.into_pull_up_input();
|
||||||
let next_animation_button = pins.gpio21.into_pull_down_input();
|
let next_animation_button = pins.gpio21.into_pull_up_input();
|
||||||
|
|
||||||
let ui = BikeUI::new(
|
let ui = BikeUI::new(
|
||||||
spi,
|
spi,
|
||||||
|
|
|
@ -169,6 +169,7 @@ impl Animation for Fade {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FadeDirection {
|
pub enum FadeDirection {
|
||||||
|
Transition,
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
}
|
}
|
||||||
|
@ -179,6 +180,7 @@ pub enum BlinkerDirection {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Blinker {
|
pub struct Blinker {
|
||||||
|
transition: Fade,
|
||||||
fade_in: Fade,
|
fade_in: Fade,
|
||||||
fade_out: Fade,
|
fade_out: Fade,
|
||||||
direction: FadeDirection,
|
direction: FadeDirection,
|
||||||
|
@ -191,10 +193,12 @@ impl Blinker {
|
||||||
fn new(
|
fn new(
|
||||||
starting_dashboard: DashboardPattern,
|
starting_dashboard: DashboardPattern,
|
||||||
starting_body: BodyPattern,
|
starting_body: BodyPattern,
|
||||||
|
home_dashboard: DashboardPattern,
|
||||||
|
home_body: BodyPattern,
|
||||||
direction: BlinkerDirection,
|
direction: BlinkerDirection,
|
||||||
time: Instant,
|
time: Instant,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut ending_dashboard = starting_dashboard.clone();
|
let mut ending_dashboard = home_dashboard.clone();
|
||||||
|
|
||||||
match direction {
|
match direction {
|
||||||
BlinkerDirection::Left => {
|
BlinkerDirection::Left => {
|
||||||
|
@ -209,7 +213,7 @@ impl Blinker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ending_body = starting_body.clone();
|
let mut ending_body = home_body.clone();
|
||||||
match direction {
|
match direction {
|
||||||
BlinkerDirection::Left => {
|
BlinkerDirection::Left => {
|
||||||
for i in 0..30 {
|
for i in 0..30 {
|
||||||
|
@ -228,7 +232,7 @@ impl Blinker {
|
||||||
}
|
}
|
||||||
|
|
||||||
Blinker {
|
Blinker {
|
||||||
fade_in: Fade::new(
|
transition: Fade::new(
|
||||||
starting_dashboard.clone(),
|
starting_dashboard.clone(),
|
||||||
starting_body.clone(),
|
starting_body.clone(),
|
||||||
ending_dashboard.clone(),
|
ending_dashboard.clone(),
|
||||||
|
@ -236,15 +240,23 @@ impl Blinker {
|
||||||
BLINKER_FRAMES,
|
BLINKER_FRAMES,
|
||||||
time,
|
time,
|
||||||
),
|
),
|
||||||
|
fade_in: Fade::new(
|
||||||
|
home_dashboard.clone(),
|
||||||
|
home_body.clone(),
|
||||||
|
ending_dashboard.clone(),
|
||||||
|
ending_body.clone(),
|
||||||
|
BLINKER_FRAMES,
|
||||||
|
time,
|
||||||
|
),
|
||||||
fade_out: Fade::new(
|
fade_out: Fade::new(
|
||||||
ending_dashboard.clone(),
|
ending_dashboard.clone(),
|
||||||
ending_body.clone(),
|
ending_body.clone(),
|
||||||
starting_dashboard.clone(),
|
home_dashboard.clone(),
|
||||||
starting_body.clone(),
|
home_body.clone(),
|
||||||
BLINKER_FRAMES,
|
BLINKER_FRAMES,
|
||||||
time,
|
time,
|
||||||
),
|
),
|
||||||
direction: FadeDirection::FadeIn,
|
direction: FadeDirection::Transition,
|
||||||
start_time: time,
|
start_time: time,
|
||||||
frames: BLINKER_FRAMES,
|
frames: BLINKER_FRAMES,
|
||||||
}
|
}
|
||||||
|
@ -256,6 +268,10 @@ impl Animation for Blinker {
|
||||||
let frames = calculate_frames(self.start_time.0, time.0);
|
let frames = calculate_frames(self.start_time.0, time.0);
|
||||||
if frames > self.frames {
|
if frames > self.frames {
|
||||||
match self.direction {
|
match self.direction {
|
||||||
|
FadeDirection::Transition => {
|
||||||
|
self.direction = FadeDirection::FadeOut;
|
||||||
|
self.fade_out.start_time = time;
|
||||||
|
}
|
||||||
FadeDirection::FadeIn => {
|
FadeDirection::FadeIn => {
|
||||||
self.direction = FadeDirection::FadeOut;
|
self.direction = FadeDirection::FadeOut;
|
||||||
self.fade_out.start_time = time;
|
self.fade_out.start_time = time;
|
||||||
|
@ -269,6 +285,7 @@ impl Animation for Blinker {
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.direction {
|
match self.direction {
|
||||||
|
FadeDirection::Transition => self.transition.tick(time),
|
||||||
FadeDirection::FadeIn => self.fade_in.tick(time),
|
FadeDirection::FadeIn => self.fade_in.tick(time),
|
||||||
FadeDirection::FadeOut => self.fade_out.tick(time),
|
FadeDirection::FadeOut => self.fade_out.tick(time),
|
||||||
}
|
}
|
||||||
|
@ -285,9 +302,45 @@ pub enum Event {
|
||||||
RightBlinker,
|
RightBlinker,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub enum Pattern {
|
||||||
|
GayPride,
|
||||||
|
TransPride,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pattern {
|
||||||
|
fn previous(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::GayPride => Pattern::TransPride,
|
||||||
|
Pattern::TransPride => Pattern::GayPride,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&self) -> Pattern {
|
||||||
|
match self {
|
||||||
|
Pattern::GayPride => Pattern::TransPride,
|
||||||
|
Pattern::TransPride => Pattern::GayPride,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dashboard(&self) -> DashboardPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::GayPride => PRIDE_DASHBOARD,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_DASHBOARD,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn body(&self) -> BodyPattern {
|
||||||
|
match self {
|
||||||
|
Pattern::GayPride => PRIDE_BODY,
|
||||||
|
Pattern::TransPride => TRANS_PRIDE_BODY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Pattern(u8),
|
Pattern(Pattern),
|
||||||
Brake,
|
Brake,
|
||||||
LeftBlinker,
|
LeftBlinker,
|
||||||
RightBlinker,
|
RightBlinker,
|
||||||
|
@ -298,7 +351,7 @@ pub enum State {
|
||||||
pub struct App {
|
pub struct App {
|
||||||
ui: Box<dyn UI>,
|
ui: Box<dyn UI>,
|
||||||
state: State,
|
state: State,
|
||||||
home_state: State,
|
home_pattern: Pattern,
|
||||||
current_animation: Box<dyn Animation>,
|
current_animation: Box<dyn Animation>,
|
||||||
dashboard_lights: DashboardPattern,
|
dashboard_lights: DashboardPattern,
|
||||||
lights: BodyPattern,
|
lights: BodyPattern,
|
||||||
|
@ -308,8 +361,8 @@ impl App {
|
||||||
pub fn new(ui: Box<dyn UI>) -> Self {
|
pub fn new(ui: Box<dyn UI>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
ui,
|
ui,
|
||||||
state: State::Pattern(0),
|
state: State::Pattern(Pattern::GayPride),
|
||||||
home_state: State::Pattern(0),
|
home_pattern: Pattern::GayPride,
|
||||||
current_animation: Box::new(DefaultAnimation {}),
|
current_animation: Box::new(DefaultAnimation {}),
|
||||||
dashboard_lights: OFF_DASHBOARD,
|
dashboard_lights: OFF_DASHBOARD,
|
||||||
lights: OFF_BODY,
|
lights: OFF_BODY,
|
||||||
|
@ -318,27 +371,16 @@ impl App {
|
||||||
|
|
||||||
fn update_animation(&mut self, time: Instant) {
|
fn update_animation(&mut self, time: Instant) {
|
||||||
match self.state {
|
match self.state {
|
||||||
State::Pattern(0) => {
|
State::Pattern(ref pattern) => {
|
||||||
self.current_animation = Box::new(Fade::new(
|
self.current_animation = Box::new(Fade::new(
|
||||||
self.dashboard_lights.clone(),
|
self.dashboard_lights.clone(),
|
||||||
self.lights.clone(),
|
self.lights.clone(),
|
||||||
PRIDE_DASHBOARD,
|
pattern.dashboard(),
|
||||||
PRIDE_BODY,
|
pattern.body(),
|
||||||
DEFAULT_FRAMES,
|
DEFAULT_FRAMES,
|
||||||
time,
|
time,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
State::Pattern(1) => {
|
|
||||||
self.current_animation = Box::new(Fade::new(
|
|
||||||
self.dashboard_lights.clone(),
|
|
||||||
self.lights.clone(),
|
|
||||||
TRANS_PRIDE_DASHBOARD,
|
|
||||||
TRANS_PRIDE_BODY,
|
|
||||||
DEFAULT_FRAMES,
|
|
||||||
time,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
State::Pattern(_) => {}
|
|
||||||
State::Brake => {
|
State::Brake => {
|
||||||
self.current_animation = Box::new(Fade::new(
|
self.current_animation = Box::new(Fade::new(
|
||||||
self.dashboard_lights.clone(),
|
self.dashboard_lights.clone(),
|
||||||
|
@ -353,6 +395,8 @@ impl App {
|
||||||
self.current_animation = Box::new(Blinker::new(
|
self.current_animation = Box::new(Blinker::new(
|
||||||
self.dashboard_lights.clone(),
|
self.dashboard_lights.clone(),
|
||||||
self.lights.clone(),
|
self.lights.clone(),
|
||||||
|
self.home_pattern.dashboard(),
|
||||||
|
self.home_pattern.body(),
|
||||||
BlinkerDirection::Left,
|
BlinkerDirection::Left,
|
||||||
time,
|
time,
|
||||||
));
|
));
|
||||||
|
@ -361,6 +405,8 @@ impl App {
|
||||||
self.current_animation = Box::new(Blinker::new(
|
self.current_animation = Box::new(Blinker::new(
|
||||||
self.dashboard_lights.clone(),
|
self.dashboard_lights.clone(),
|
||||||
self.lights.clone(),
|
self.lights.clone(),
|
||||||
|
self.home_pattern.dashboard(),
|
||||||
|
self.home_pattern.body(),
|
||||||
BlinkerDirection::Right,
|
BlinkerDirection::Right,
|
||||||
time,
|
time,
|
||||||
));
|
));
|
||||||
|
@ -374,41 +420,36 @@ impl App {
|
||||||
match event {
|
match event {
|
||||||
Event::Brake => {
|
Event::Brake => {
|
||||||
if self.state == State::Brake {
|
if self.state == State::Brake {
|
||||||
self.state = self.home_state.clone();
|
self.state = State::Pattern(self.home_pattern);
|
||||||
} else {
|
} else {
|
||||||
self.state = State::Brake;
|
self.state = State::Brake;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::BrakeRelease => self.state = self.home_state.clone(),
|
Event::BrakeRelease => self.state = State::Pattern(self.home_pattern),
|
||||||
Event::LeftBlinker => match self.state {
|
Event::LeftBlinker => match self.state {
|
||||||
State::Brake => self.state = State::BrakeLeftBlinker,
|
State::Brake => self.state = State::BrakeLeftBlinker,
|
||||||
State::BrakeLeftBlinker => self.state = State::Brake,
|
State::BrakeLeftBlinker => self.state = State::Brake,
|
||||||
State::LeftBlinker => self.state = self.home_state.clone(),
|
State::LeftBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
_ => self.state = State::LeftBlinker,
|
_ => self.state = State::LeftBlinker,
|
||||||
},
|
},
|
||||||
Event::NextPattern => match self.state {
|
Event::NextPattern => match self.state {
|
||||||
State::Pattern(i) => {
|
State::Pattern(ref pattern) => {
|
||||||
let next = i + 1;
|
self.home_pattern = pattern.next();
|
||||||
self.state = State::Pattern(if next > 1 { 0 } else { next });
|
self.state = State::Pattern(self.home_pattern);
|
||||||
self.home_state = self.state.clone();
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
Event::PreviousPattern => match self.state {
|
Event::PreviousPattern => match self.state {
|
||||||
State::Pattern(i) => {
|
State::Pattern(ref pattern) => {
|
||||||
if i == 0 {
|
self.home_pattern = pattern.previous();
|
||||||
self.state = State::Pattern(1);
|
self.state = State::Pattern(self.home_pattern);
|
||||||
} else {
|
|
||||||
self.state = State::Pattern(i - 1);
|
|
||||||
}
|
|
||||||
self.home_state = self.state.clone();
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
},
|
||||||
Event::RightBlinker => match self.state {
|
Event::RightBlinker => match self.state {
|
||||||
State::Brake => self.state = State::BrakeRightBlinker,
|
State::Brake => self.state = State::BrakeRightBlinker,
|
||||||
State::BrakeRightBlinker => self.state = State::Brake,
|
State::BrakeRightBlinker => self.state = State::Brake,
|
||||||
State::RightBlinker => self.state = self.home_state.clone(),
|
State::RightBlinker => self.state = State::Pattern(self.home_pattern),
|
||||||
_ => self.state = State::RightBlinker,
|
_ => self.state = State::RightBlinker,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ pub const BRAKES_RED: RGB<I8F8> = RGB {
|
||||||
|
|
||||||
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
|
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
|
||||||
r: I8F8::lit("1"),
|
r: I8F8::lit("1"),
|
||||||
g: I8F8::lit("0.74"),
|
g: I8F8::lit("0.15"),
|
||||||
b: I8F8::lit("0"),
|
b: I8F8::lit("0"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue