mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2025-08-12 08:27:38 +00:00
Add radio settings, new app icon, and other UI improvements (#2732)
This commit is contained in:
@@ -27,39 +27,23 @@ __attribute__((section(".external_app.app_battleship.application_information"),
|
|||||||
|
|
||||||
/*.app_name = */ "Battleship",
|
/*.app_name = */ "Battleship",
|
||||||
/*.bitmap_data = */ {
|
/*.bitmap_data = */ {
|
||||||
// Ship icon
|
// Pirate galleon - 16x16
|
||||||
0x00,
|
0x80, 0x00, // ........#.......
|
||||||
0x00,
|
0x80, 0x00, // ........#.......
|
||||||
0x00,
|
0x80, 0x00, // ........#.......
|
||||||
0x00,
|
0xC0, 0x01, // .......###......
|
||||||
0x00,
|
0xE0, 0x03, // ......#####.....
|
||||||
0x00,
|
0xF0, 0x07, // .....#######....
|
||||||
0x00,
|
0xF8, 0x0F, // ....#########...
|
||||||
0x00,
|
0xFC, 0x1F, // ...###########..
|
||||||
0x1C,
|
0xFE, 0x3F, // ..#############.
|
||||||
0x38,
|
0x00, 0x01, // .......#........
|
||||||
0x3E,
|
0x00, 0x01, // .......#........
|
||||||
0x7C,
|
0x00, 0x01, // .......#........
|
||||||
0x7F,
|
0xFC, 0x3F, // ..############..
|
||||||
0xFE,
|
0xFE, 0x7F, // .##############.
|
||||||
0xFF,
|
0xFF, 0xFF, // ################
|
||||||
0xFF,
|
0xFC, 0x3F, // ..############..
|
||||||
0xFF,
|
|
||||||
0xFF,
|
|
||||||
0x7F,
|
|
||||||
0xFE,
|
|
||||||
0x3F,
|
|
||||||
0xFC,
|
|
||||||
0x1F,
|
|
||||||
0xF8,
|
|
||||||
0x0F,
|
|
||||||
0xF0,
|
|
||||||
0x07,
|
|
||||||
0xE0,
|
|
||||||
0x03,
|
|
||||||
0xC0,
|
|
||||||
0x01,
|
|
||||||
0x80,
|
|
||||||
},
|
},
|
||||||
/*.icon_color = */ ui::Color::blue().v,
|
/*.icon_color = */ ui::Color::blue().v,
|
||||||
/*.menu_location = */ app_location_t::GAMES,
|
/*.menu_location = */ app_location_t::GAMES,
|
||||||
|
@@ -26,32 +26,97 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
|||||||
: nav_{nav} {
|
: nav_{nav} {
|
||||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||||
|
|
||||||
add_children({&rssi,
|
add_children({&text_title, &text_subtitle,
|
||||||
&field_frequency,
|
&rect_radio_settings, &label_radio, &button_frequency,
|
||||||
&text_status,
|
&label_rf_amp, &checkbox_rf_amp,
|
||||||
&text_score,
|
&label_lna, &field_lna,
|
||||||
&button_red_team,
|
&label_vga, &field_vga,
|
||||||
&button_blue_team,
|
&label_tx_gain, &field_tx_gain,
|
||||||
&button_rotate,
|
&rect_audio_settings, &label_audio,
|
||||||
&button_place,
|
&checkbox_sound, &label_volume, &field_volume,
|
||||||
&button_fire,
|
&rect_team_selection, &label_team,
|
||||||
&button_menu});
|
&button_red_team, &button_blue_team,
|
||||||
|
&rssi, &text_status, &text_score,
|
||||||
|
&button_rotate, &button_place, &button_fire, &button_menu});
|
||||||
|
|
||||||
|
// Hide in-game elements
|
||||||
|
rssi.hidden(true);
|
||||||
|
text_status.hidden(true);
|
||||||
text_score.hidden(true);
|
text_score.hidden(true);
|
||||||
button_rotate.hidden(true);
|
button_rotate.hidden(true);
|
||||||
button_place.hidden(true);
|
button_place.hidden(true);
|
||||||
button_fire.hidden(true);
|
button_fire.hidden(true);
|
||||||
button_menu.hidden(true);
|
button_menu.hidden(true);
|
||||||
|
|
||||||
field_frequency.set_value(DEFAULT_FREQUENCY);
|
// Configure frequency button
|
||||||
field_frequency.on_change = [this](rf::Frequency freq) {
|
button_frequency.set_text("<" + to_string_short_freq(tx_frequency) + ">");
|
||||||
tx_frequency = freq;
|
|
||||||
rx_frequency = freq;
|
button_frequency.on_select = [this, &nav](ButtonWithEncoder& button) {
|
||||||
|
auto new_view = nav_.push<FrequencyKeypadView>(tx_frequency);
|
||||||
|
new_view->on_changed = [this, &button](rf::Frequency f) {
|
||||||
|
tx_frequency = f;
|
||||||
|
rx_frequency = f;
|
||||||
|
button_frequency.set_text("<" + to_string_short_freq(tx_frequency) + ">");
|
||||||
|
if (!is_transmitting) {
|
||||||
|
receiver_model.set_target_frequency(rx_frequency);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
button_frequency.on_change = [this]() {
|
||||||
|
int64_t def_step = 25000;
|
||||||
|
int64_t new_freq = static_cast<int64_t>(tx_frequency) + (button_frequency.get_encoder_delta() * def_step);
|
||||||
|
|
||||||
|
if (new_freq < 1) new_freq = 1;
|
||||||
|
if (new_freq > 7200000000LL) new_freq = 7200000000LL;
|
||||||
|
|
||||||
|
tx_frequency = static_cast<uint32_t>(new_freq);
|
||||||
|
rx_frequency = tx_frequency;
|
||||||
|
button_frequency.set_encoder_delta(0);
|
||||||
|
button_frequency.set_text("<" + to_string_short_freq(tx_frequency) + ">");
|
||||||
if (!is_transmitting) {
|
if (!is_transmitting) {
|
||||||
receiver_model.set_target_frequency(rx_frequency);
|
receiver_model.set_target_frequency(rx_frequency);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Radio controls
|
||||||
|
checkbox_rf_amp.set_value(rf_amp_enabled);
|
||||||
|
checkbox_rf_amp.on_select = [this](Checkbox&, bool v) {
|
||||||
|
rf_amp_enabled = v;
|
||||||
|
transmitter_model.set_rf_amp(v);
|
||||||
|
receiver_model.set_rf_amp(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
field_lna.set_value(lna_gain);
|
||||||
|
field_lna.on_change = [this](int32_t v) {
|
||||||
|
lna_gain = v;
|
||||||
|
receiver_model.set_lna(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
field_vga.set_value(vga_gain);
|
||||||
|
field_vga.on_change = [this](int32_t v) {
|
||||||
|
vga_gain = v;
|
||||||
|
receiver_model.set_vga(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
field_tx_gain.set_value(tx_gain);
|
||||||
|
field_tx_gain.on_change = [this](int32_t v) {
|
||||||
|
tx_gain = v;
|
||||||
|
transmitter_model.set_tx_gain(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Audio controls
|
||||||
|
checkbox_sound.set_value(sound_enabled);
|
||||||
|
checkbox_sound.on_select = [this](Checkbox&, bool v) {
|
||||||
|
sound_enabled = v;
|
||||||
|
if (sound_enabled) {
|
||||||
|
audio::output::unmute();
|
||||||
|
} else {
|
||||||
|
audio::output::mute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Team selection
|
||||||
button_red_team.on_select = [this](Button&) {
|
button_red_team.on_select = [this](Button&) {
|
||||||
start_team(true);
|
start_team(true);
|
||||||
};
|
};
|
||||||
@@ -60,6 +125,7 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
|||||||
start_team(false);
|
start_team(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// In-game controls
|
||||||
button_rotate.on_select = [this](Button&) {
|
button_rotate.on_select = [this](Button&) {
|
||||||
placing_horizontal = !placing_horizontal;
|
placing_horizontal = !placing_horizontal;
|
||||||
set_dirty();
|
set_dirty();
|
||||||
@@ -77,6 +143,28 @@ BattleshipView::BattleshipView(NavigationView& nav)
|
|||||||
reset_game();
|
reset_game();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set proper rectangles for layout
|
||||||
|
button_frequency.set_parent_rect({17, 65, 11 * 8, 20});
|
||||||
|
checkbox_rf_amp.set_parent_rect({55, 90, 24, 24});
|
||||||
|
field_lna.set_parent_rect({50, 118, 32, 16});
|
||||||
|
field_vga.set_parent_rect({125, 118, 32, 16});
|
||||||
|
field_tx_gain.set_parent_rect({185, 118, 32, 16});
|
||||||
|
checkbox_sound.set_parent_rect({17, 187, 80, 24});
|
||||||
|
field_volume.set_parent_rect({165, 187, 32, 16});
|
||||||
|
button_red_team.set_parent_rect({25, 242, 85, 45});
|
||||||
|
button_blue_team.set_parent_rect({130, 242, 85, 45});
|
||||||
|
|
||||||
|
// Make menu elements focusable
|
||||||
|
button_frequency.set_focusable(true);
|
||||||
|
checkbox_rf_amp.set_focusable(true);
|
||||||
|
field_lna.set_focusable(true);
|
||||||
|
field_vga.set_focusable(true);
|
||||||
|
field_tx_gain.set_focusable(true);
|
||||||
|
checkbox_sound.set_focusable(true);
|
||||||
|
field_volume.set_focusable(true);
|
||||||
|
button_red_team.set_focusable(true);
|
||||||
|
button_blue_team.set_focusable(true);
|
||||||
|
|
||||||
set_focusable(true);
|
set_focusable(true);
|
||||||
init_game();
|
init_game();
|
||||||
}
|
}
|
||||||
@@ -90,7 +178,7 @@ BattleshipView::~BattleshipView() {
|
|||||||
|
|
||||||
void BattleshipView::focus() {
|
void BattleshipView::focus() {
|
||||||
if (game_state == GameState::MENU) {
|
if (game_state == GameState::MENU) {
|
||||||
button_red_team.focus();
|
button_frequency.focus();
|
||||||
} else {
|
} else {
|
||||||
View::focus();
|
View::focus();
|
||||||
}
|
}
|
||||||
@@ -131,20 +219,54 @@ void BattleshipView::reset_game() {
|
|||||||
|
|
||||||
current_status = "Choose your team!";
|
current_status = "Choose your team!";
|
||||||
update_score();
|
update_score();
|
||||||
text_status.hidden(false);
|
|
||||||
text_score.hidden(true);
|
|
||||||
field_frequency.hidden(false);
|
|
||||||
button_red_team.hidden(false);
|
|
||||||
button_blue_team.hidden(false);
|
|
||||||
button_rotate.hidden(true);
|
|
||||||
button_place.hidden(true);
|
|
||||||
button_fire.hidden(true);
|
|
||||||
button_menu.hidden(true);
|
|
||||||
|
|
||||||
button_red_team.set_focusable(true);
|
// Toggle visibility
|
||||||
button_blue_team.set_focusable(true);
|
bool menu_vis = true;
|
||||||
button_red_team.focus();
|
bool game_vis = false;
|
||||||
|
|
||||||
|
text_title.hidden(!menu_vis);
|
||||||
|
text_subtitle.hidden(!menu_vis);
|
||||||
|
rect_radio_settings.hidden(!menu_vis);
|
||||||
|
label_radio.hidden(!menu_vis);
|
||||||
|
button_frequency.hidden(!menu_vis);
|
||||||
|
label_rf_amp.hidden(!menu_vis);
|
||||||
|
checkbox_rf_amp.hidden(!menu_vis);
|
||||||
|
label_lna.hidden(!menu_vis);
|
||||||
|
field_lna.hidden(!menu_vis);
|
||||||
|
label_vga.hidden(!menu_vis);
|
||||||
|
field_vga.hidden(!menu_vis);
|
||||||
|
label_tx_gain.hidden(!menu_vis);
|
||||||
|
field_tx_gain.hidden(!menu_vis);
|
||||||
|
rect_audio_settings.hidden(!menu_vis);
|
||||||
|
label_audio.hidden(!menu_vis);
|
||||||
|
checkbox_sound.hidden(!menu_vis);
|
||||||
|
label_volume.hidden(!menu_vis);
|
||||||
|
field_volume.hidden(!menu_vis);
|
||||||
|
rect_team_selection.hidden(!menu_vis);
|
||||||
|
label_team.hidden(!menu_vis);
|
||||||
|
button_red_team.hidden(!menu_vis);
|
||||||
|
button_blue_team.hidden(!menu_vis);
|
||||||
|
|
||||||
|
rssi.hidden(!game_vis);
|
||||||
|
text_status.hidden(!game_vis);
|
||||||
|
text_score.hidden(!game_vis);
|
||||||
|
button_rotate.hidden(!game_vis);
|
||||||
|
button_place.hidden(!game_vis);
|
||||||
|
button_fire.hidden(!game_vis);
|
||||||
|
button_menu.hidden(!game_vis);
|
||||||
|
|
||||||
|
// Restore focusability
|
||||||
|
button_frequency.set_focusable(menu_vis);
|
||||||
|
checkbox_rf_amp.set_focusable(menu_vis);
|
||||||
|
field_lna.set_focusable(menu_vis);
|
||||||
|
field_vga.set_focusable(menu_vis);
|
||||||
|
field_tx_gain.set_focusable(menu_vis);
|
||||||
|
checkbox_sound.set_focusable(menu_vis);
|
||||||
|
field_volume.set_focusable(menu_vis);
|
||||||
|
button_red_team.set_focusable(menu_vis);
|
||||||
|
button_blue_team.set_focusable(menu_vis);
|
||||||
|
|
||||||
|
button_frequency.focus();
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,16 +282,40 @@ void BattleshipView::start_team(bool red) {
|
|||||||
is_red_team = red;
|
is_red_team = red;
|
||||||
game_state = GameState::PLACING_SHIPS;
|
game_state = GameState::PLACING_SHIPS;
|
||||||
|
|
||||||
field_frequency.hidden(true);
|
// Toggle visibility
|
||||||
button_red_team.hidden(true);
|
bool menu_vis = false;
|
||||||
button_blue_team.hidden(true);
|
bool game_vis = true;
|
||||||
|
|
||||||
|
text_title.hidden(!menu_vis);
|
||||||
|
text_subtitle.hidden(!menu_vis);
|
||||||
|
rect_radio_settings.hidden(!menu_vis);
|
||||||
|
label_radio.hidden(!menu_vis);
|
||||||
|
button_frequency.hidden(!menu_vis);
|
||||||
|
label_rf_amp.hidden(!menu_vis);
|
||||||
|
checkbox_rf_amp.hidden(!menu_vis);
|
||||||
|
label_lna.hidden(!menu_vis);
|
||||||
|
field_lna.hidden(!menu_vis);
|
||||||
|
label_vga.hidden(!menu_vis);
|
||||||
|
field_vga.hidden(!menu_vis);
|
||||||
|
label_tx_gain.hidden(!menu_vis);
|
||||||
|
field_tx_gain.hidden(!menu_vis);
|
||||||
|
rect_audio_settings.hidden(!menu_vis);
|
||||||
|
label_audio.hidden(!menu_vis);
|
||||||
|
checkbox_sound.hidden(!menu_vis);
|
||||||
|
label_volume.hidden(!menu_vis);
|
||||||
|
field_volume.hidden(!menu_vis);
|
||||||
|
rect_team_selection.hidden(!menu_vis);
|
||||||
|
label_team.hidden(!menu_vis);
|
||||||
|
button_red_team.hidden(!menu_vis);
|
||||||
|
button_blue_team.hidden(!menu_vis);
|
||||||
|
|
||||||
|
rssi.hidden(!game_vis);
|
||||||
|
text_status.hidden(true);
|
||||||
|
text_score.hidden(true);
|
||||||
button_rotate.hidden(false);
|
button_rotate.hidden(false);
|
||||||
button_place.hidden(false);
|
button_place.hidden(false);
|
||||||
button_menu.hidden(false);
|
button_menu.hidden(false);
|
||||||
|
|
||||||
text_status.hidden(true);
|
|
||||||
text_score.hidden(true);
|
|
||||||
|
|
||||||
current_status = "Place carrier (5)";
|
current_status = "Place carrier (5)";
|
||||||
|
|
||||||
button_rotate.set_focusable(false);
|
button_rotate.set_focusable(false);
|
||||||
@@ -177,10 +323,8 @@ void BattleshipView::start_team(bool red) {
|
|||||||
button_menu.set_focusable(false);
|
button_menu.set_focusable(false);
|
||||||
|
|
||||||
focus();
|
focus();
|
||||||
|
|
||||||
is_transmitting = true;
|
is_transmitting = true;
|
||||||
configure_radio_rx();
|
configure_radio_rx();
|
||||||
|
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,8 +344,8 @@ void BattleshipView::configure_radio_tx() {
|
|||||||
transmitter_model.set_target_frequency(tx_frequency);
|
transmitter_model.set_target_frequency(tx_frequency);
|
||||||
transmitter_model.set_sampling_rate(2280000);
|
transmitter_model.set_sampling_rate(2280000);
|
||||||
transmitter_model.set_baseband_bandwidth(1750000);
|
transmitter_model.set_baseband_bandwidth(1750000);
|
||||||
transmitter_model.set_rf_amp(false);
|
transmitter_model.set_rf_amp(rf_amp_enabled);
|
||||||
transmitter_model.set_tx_gain(35);
|
transmitter_model.set_tx_gain(tx_gain);
|
||||||
|
|
||||||
is_transmitting = true;
|
is_transmitting = true;
|
||||||
}
|
}
|
||||||
@@ -210,30 +354,29 @@ void BattleshipView::configure_radio_rx() {
|
|||||||
if (is_transmitting) {
|
if (is_transmitting) {
|
||||||
transmitter_model.disable();
|
transmitter_model.disable();
|
||||||
baseband::shutdown();
|
baseband::shutdown();
|
||||||
|
|
||||||
chThdSleepMilliseconds(100);
|
chThdSleepMilliseconds(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||||
|
|
||||||
chThdSleepMilliseconds(100);
|
chThdSleepMilliseconds(100);
|
||||||
|
|
||||||
receiver_model.set_target_frequency(rx_frequency);
|
receiver_model.set_target_frequency(rx_frequency);
|
||||||
receiver_model.set_sampling_rate(3072000);
|
receiver_model.set_sampling_rate(3072000);
|
||||||
receiver_model.set_baseband_bandwidth(1750000);
|
receiver_model.set_baseband_bandwidth(1750000);
|
||||||
receiver_model.set_rf_amp(false);
|
receiver_model.set_rf_amp(rf_amp_enabled);
|
||||||
receiver_model.set_lna(24);
|
receiver_model.set_lna(lna_gain);
|
||||||
receiver_model.set_vga(24);
|
receiver_model.set_vga(vga_gain);
|
||||||
|
|
||||||
baseband::set_pocsag();
|
baseband::set_pocsag();
|
||||||
|
|
||||||
receiver_model.enable();
|
receiver_model.enable();
|
||||||
|
|
||||||
audio::set_rate(audio::Rate::Hz_24000);
|
audio::set_rate(audio::Rate::Hz_24000);
|
||||||
audio::output::start();
|
|
||||||
|
if (sound_enabled) {
|
||||||
|
audio::output::start();
|
||||||
|
}
|
||||||
|
|
||||||
is_transmitting = false;
|
is_transmitting = false;
|
||||||
|
|
||||||
current_status = "RX Ready";
|
current_status = "RX Ready";
|
||||||
set_dirty();
|
set_dirty();
|
||||||
}
|
}
|
||||||
@@ -242,22 +385,59 @@ void BattleshipView::paint(Painter& painter) {
|
|||||||
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
|
painter.fill_rectangle({0, 0, 240, 320}, Color::black());
|
||||||
|
|
||||||
if (game_state == GameState::MENU) {
|
if (game_state == GameState::MENU) {
|
||||||
auto style_title = *ui::Theme::getInstance()->fg_light;
|
draw_menu_screen(painter);
|
||||||
painter.draw_string({60, 20}, style_title, "BATTLESHIP");
|
|
||||||
painter.draw_string({40, 80}, style_title, "Choose your team:");
|
// Custom paint team buttons
|
||||||
painter.draw_string({10, 180}, *ui::Theme::getInstance()->fg_medium, "Set same freq on both!");
|
if (!button_red_team.hidden()) {
|
||||||
|
Rect r = button_red_team.screen_rect();
|
||||||
|
painter.fill_rectangle(r, Color::dark_red());
|
||||||
|
painter.draw_rectangle(r, Color::red());
|
||||||
|
|
||||||
|
if (button_red_team.has_focus()) {
|
||||||
|
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto style_white = Style{
|
||||||
|
.font = ui::font::fixed_8x16,
|
||||||
|
.background = Color::dark_red(),
|
||||||
|
.foreground = Color::white()};
|
||||||
|
painter.draw_string(r.center() - Point(24, 16), style_white, "RED");
|
||||||
|
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!button_blue_team.hidden()) {
|
||||||
|
Rect r = button_blue_team.screen_rect();
|
||||||
|
painter.fill_rectangle(r, Color::dark_blue());
|
||||||
|
painter.draw_rectangle(r, Color::blue());
|
||||||
|
|
||||||
|
if (button_blue_team.has_focus()) {
|
||||||
|
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto style_white = Style{
|
||||||
|
.font = ui::font::fixed_8x16,
|
||||||
|
.background = Color::dark_blue(),
|
||||||
|
.foreground = Color::white()};
|
||||||
|
painter.draw_string(r.center() - Point(24, 16), style_white, "BLUE");
|
||||||
|
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Color team_color = is_red_team ? Color::red() : Color::blue();
|
Color team_color = is_red_team ? Color::red() : Color::blue();
|
||||||
painter.fill_rectangle({0, 5, 240, 16}, team_color);
|
painter.fill_rectangle({0, 5, 240, 16}, team_color);
|
||||||
|
|
||||||
auto style_white = Style{
|
auto style_white = Style{
|
||||||
.font = ui::font::fixed_8x16,
|
.font = ui::font::fixed_8x16,
|
||||||
.background = team_color,
|
.background = team_color,
|
||||||
.foreground = Color::white()};
|
.foreground = Color::white()};
|
||||||
painter.draw_string({85, 5}, style_white, is_red_team ? "RED TEAM" : "BLUE TEAM");
|
painter.draw_string({85, 5}, style_white, is_red_team ? "RED TEAM" : "BLUE TEAM");
|
||||||
|
|
||||||
auto style_status = *ui::Theme::getInstance()->fg_light;
|
auto style_status = Style{
|
||||||
|
.font = ui::font::fixed_8x16,
|
||||||
|
.background = Color::black(),
|
||||||
|
.foreground = Color::white()};
|
||||||
painter.fill_rectangle({0, 21, 240, 16}, Color::black());
|
painter.fill_rectangle({0, 21, 240, 16}, Color::black());
|
||||||
painter.draw_string({10, 21}, style_status, current_status);
|
painter.draw_string({10, 21}, style_status, current_status);
|
||||||
|
|
||||||
@@ -284,6 +464,32 @@ void BattleshipView::paint(Painter& painter) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BattleshipView::draw_menu_screen(Painter& painter) {
|
||||||
|
painter.draw_hline({12, 38}, 216, Color::dark_cyan());
|
||||||
|
|
||||||
|
painter.fill_rectangle({13, 41, 214, 116}, Color::dark_grey());
|
||||||
|
painter.draw_hline({12, 40}, 216, Color::cyan());
|
||||||
|
painter.draw_hline({12, 157}, 216, Color::cyan());
|
||||||
|
|
||||||
|
painter.fill_rectangle({13, 165, 214, 43}, Color::dark_grey());
|
||||||
|
painter.draw_hline({12, 164}, 216, Color::cyan());
|
||||||
|
painter.draw_hline({12, 208}, 216, Color::cyan());
|
||||||
|
|
||||||
|
painter.fill_rectangle({13, 218, 214, 73}, Color::dark_grey());
|
||||||
|
painter.draw_hline({12, 217}, 216, Color::cyan());
|
||||||
|
painter.draw_hline({12, 291}, 216, Color::cyan());
|
||||||
|
|
||||||
|
// Radio status indicator
|
||||||
|
Point indicator_pos{220, 53};
|
||||||
|
if (is_transmitting) {
|
||||||
|
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::red());
|
||||||
|
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||||
|
} else {
|
||||||
|
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::green());
|
||||||
|
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BattleshipView::draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
|
void BattleshipView::draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
|
||||||
painter.fill_rectangle({grid_x, grid_y, GRID_SIZE * CELL_SIZE, GRID_SIZE * CELL_SIZE},
|
painter.fill_rectangle({grid_x, grid_y, GRID_SIZE * CELL_SIZE, GRID_SIZE * CELL_SIZE},
|
||||||
Color::dark_blue());
|
Color::dark_blue());
|
||||||
@@ -497,16 +703,13 @@ void BattleshipView::send_message(const GameMessage& msg) {
|
|||||||
|
|
||||||
configure_radio_tx();
|
configure_radio_tx();
|
||||||
|
|
||||||
// Use POCSAG encoding
|
|
||||||
uint32_t target_address = is_red_team ? BLUE_TEAM_ADDRESS : RED_TEAM_ADDRESS;
|
uint32_t target_address = is_red_team ? BLUE_TEAM_ADDRESS : RED_TEAM_ADDRESS;
|
||||||
|
|
||||||
std::vector<uint32_t> codewords;
|
std::vector<uint32_t> codewords;
|
||||||
BCHCode BCH_code{{1, 0, 1, 0, 0, 1}, 5, 31, 21, 2};
|
BCHCode BCH_code{{1, 0, 1, 0, 0, 1}, 5, 31, 21, 2};
|
||||||
|
|
||||||
// Use the pocsag namespace to access ALPHANUMERIC
|
|
||||||
pocsag::pocsag_encode(pocsag::MessageType::ALPHANUMERIC, BCH_code, 0, message, target_address, codewords);
|
pocsag::pocsag_encode(pocsag::MessageType::ALPHANUMERIC, BCH_code, 0, message, target_address, codewords);
|
||||||
|
|
||||||
// Copy codewords to shared memory
|
|
||||||
uint8_t* data_ptr = shared_memory.bb_data.data;
|
uint8_t* data_ptr = shared_memory.bb_data.data;
|
||||||
size_t bi = 0;
|
size_t bi = 0;
|
||||||
|
|
||||||
@@ -518,12 +721,11 @@ void BattleshipView::send_message(const GameMessage& msg) {
|
|||||||
data_ptr[bi++] = codeword & 0xFF;
|
data_ptr[bi++] = codeword & 0xFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set baseband FSK data
|
|
||||||
baseband::set_fsk_data(
|
baseband::set_fsk_data(
|
||||||
codewords.size() * 32, // Total bits
|
codewords.size() * 32,
|
||||||
2280000 / 1200, // Bit duration (1200 baud)
|
2280000 / 1200,
|
||||||
4500, // Deviation
|
4500,
|
||||||
64); // Packet repeat
|
64);
|
||||||
|
|
||||||
transmitter_model.set_baseband_bandwidth(1750000);
|
transmitter_model.set_baseband_bandwidth(1750000);
|
||||||
transmitter_model.enable();
|
transmitter_model.enable();
|
||||||
@@ -537,13 +739,11 @@ void BattleshipView::on_pocsag_packet(const POCSAGPacketMessage* message) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode POCSAG message
|
|
||||||
pocsag_state.codeword_index = 0;
|
pocsag_state.codeword_index = 0;
|
||||||
pocsag_state.errors = 0;
|
pocsag_state.errors = 0;
|
||||||
|
|
||||||
while (pocsag::pocsag_decode_batch(message->packet, pocsag_state)) {
|
while (pocsag::pocsag_decode_batch(message->packet, pocsag_state)) {
|
||||||
if (pocsag_state.out_type == pocsag::MESSAGE) {
|
if (pocsag_state.out_type == pocsag::MESSAGE) {
|
||||||
// Check if message is for our team
|
|
||||||
uint32_t expected_address = is_red_team ? RED_TEAM_ADDRESS : BLUE_TEAM_ADDRESS;
|
uint32_t expected_address = is_red_team ? RED_TEAM_ADDRESS : BLUE_TEAM_ADDRESS;
|
||||||
if (pocsag_state.address == expected_address) {
|
if (pocsag_state.address == expected_address) {
|
||||||
process_message(pocsag_state.output);
|
process_message(pocsag_state.output);
|
||||||
@@ -783,6 +983,18 @@ bool BattleshipView::on_encoder(const EncoderEvent delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool BattleshipView::on_key(const KeyEvent key) {
|
bool BattleshipView::on_key(const KeyEvent key) {
|
||||||
|
if (game_state == GameState::MENU) {
|
||||||
|
if (key == KeyEvent::Up || key == KeyEvent::Down ||
|
||||||
|
key == KeyEvent::Left || key == KeyEvent::Right) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (key == KeyEvent::Select || key == KeyEvent::Back) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game state key handling
|
||||||
if (key == KeyEvent::Select) {
|
if (key == KeyEvent::Select) {
|
||||||
if (game_state == GameState::PLACING_SHIPS) {
|
if (game_state == GameState::PLACING_SHIPS) {
|
||||||
place_ship();
|
place_ship();
|
||||||
|
@@ -18,25 +18,17 @@
|
|||||||
#include "baseband_api.hpp"
|
#include "baseband_api.hpp"
|
||||||
#include "string_format.hpp"
|
#include "string_format.hpp"
|
||||||
#include "audio.hpp"
|
#include "audio.hpp"
|
||||||
#include "portapack.hpp"
|
|
||||||
#include "message.hpp"
|
#include "message.hpp"
|
||||||
#include "pocsag.hpp"
|
#include "pocsag.hpp"
|
||||||
#include "portapack_shared_memory.hpp"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace ui::external_app::battleship {
|
namespace ui::external_app::battleship {
|
||||||
|
|
||||||
using namespace portapack;
|
|
||||||
|
|
||||||
constexpr uint8_t GRID_SIZE = 10;
|
constexpr uint8_t GRID_SIZE = 10;
|
||||||
constexpr uint8_t CELL_SIZE = 24;
|
constexpr uint8_t CELL_SIZE = 24;
|
||||||
constexpr uint8_t GRID_OFFSET_X = 0;
|
constexpr uint8_t GRID_OFFSET_X = 0;
|
||||||
constexpr uint8_t GRID_OFFSET_Y = 32;
|
constexpr uint8_t GRID_OFFSET_Y = 32;
|
||||||
constexpr uint16_t BUTTON_Y = 280;
|
|
||||||
constexpr uint32_t DEFAULT_FREQUENCY = 433920000;
|
|
||||||
|
|
||||||
enum class ShipType : uint8_t {
|
enum class ShipType : uint8_t {
|
||||||
CARRIER = 5,
|
CARRIER = 5,
|
||||||
@@ -112,30 +104,36 @@ class BattleshipView : public View {
|
|||||||
NavigationView& nav_;
|
NavigationView& nav_;
|
||||||
|
|
||||||
RxRadioState rx_radio_state_{
|
RxRadioState rx_radio_state_{
|
||||||
DEFAULT_FREQUENCY /* frequency */,
|
433920000 /* frequency */,
|
||||||
1750000 /* bandwidth */,
|
1750000 /* bandwidth */,
|
||||||
2280000 /* sampling rate */
|
2280000 /* sampling rate */
|
||||||
};
|
};
|
||||||
|
|
||||||
TxRadioState tx_radio_state_{
|
TxRadioState tx_radio_state_{
|
||||||
DEFAULT_FREQUENCY /* frequency */,
|
433920000 /* frequency */,
|
||||||
1750000 /* bandwidth */,
|
1750000 /* bandwidth */,
|
||||||
2280000 /* sampling rate */
|
2280000 /* sampling rate */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
bool sound_enabled{true};
|
||||||
|
bool rf_amp_enabled{false};
|
||||||
|
uint8_t lna_gain{24};
|
||||||
|
uint8_t vga_gain{24};
|
||||||
|
uint8_t tx_gain{35};
|
||||||
|
|
||||||
app_settings::SettingsManager settings_{
|
app_settings::SettingsManager settings_{
|
||||||
"battleship",
|
"battleship",
|
||||||
app_settings::Mode::RX_TX,
|
app_settings::Mode::RX_TX,
|
||||||
{{"rx_freq"sv, &rx_frequency},
|
{{"rx_freq"sv, &rx_frequency},
|
||||||
{"tx_freq"sv, &tx_frequency},
|
{"tx_freq"sv, &tx_frequency},
|
||||||
{"wins"sv, &wins},
|
{"rf_amp"sv, &rf_amp_enabled}}};
|
||||||
{"losses"sv, &losses}}};
|
|
||||||
|
|
||||||
GameState game_state{GameState::MENU};
|
GameState game_state{GameState::MENU};
|
||||||
bool is_red_team{false};
|
bool is_red_team{false};
|
||||||
bool opponent_ready{false};
|
bool opponent_ready{false};
|
||||||
uint32_t wins{0};
|
uint8_t wins{0};
|
||||||
uint32_t losses{0};
|
uint8_t losses{0};
|
||||||
|
|
||||||
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> my_grid{};
|
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> my_grid{};
|
||||||
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> enemy_grid{};
|
std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE> enemy_grid{};
|
||||||
@@ -155,8 +153,8 @@ class BattleshipView : public View {
|
|||||||
uint8_t target_y{0};
|
uint8_t target_y{0};
|
||||||
bool touch_enabled{true};
|
bool touch_enabled{true};
|
||||||
|
|
||||||
uint32_t tx_frequency{DEFAULT_FREQUENCY};
|
uint32_t tx_frequency{433920000};
|
||||||
uint32_t rx_frequency{DEFAULT_FREQUENCY};
|
uint32_t rx_frequency{433920000};
|
||||||
bool is_transmitting{false};
|
bool is_transmitting{false};
|
||||||
|
|
||||||
// POCSAG decoding state
|
// POCSAG decoding state
|
||||||
@@ -164,48 +162,53 @@ class BattleshipView : public View {
|
|||||||
pocsag::POCSAGState pocsag_state{&ecc};
|
pocsag::POCSAGState pocsag_state{&ecc};
|
||||||
uint32_t last_address{0};
|
uint32_t last_address{0};
|
||||||
|
|
||||||
RSSI rssi{
|
// UI Elements - Menu/Settings Screen
|
||||||
{21 * 8, 0, 6 * 8, 4}};
|
Text text_title{{60, 2, 120, 24}, "BATTLESHIP"};
|
||||||
|
Text text_subtitle{{40, 20, 160, 16}, "Naval Combat Game"};
|
||||||
|
|
||||||
FrequencyField field_frequency{
|
Rectangle rect_radio_settings{{12, 40, 216, 118}, Color::dark_grey()};
|
||||||
{10, 50}};
|
Text label_radio{{17, 45, 100, 16}, "RADIO SETUP"};
|
||||||
|
ButtonWithEncoder button_frequency{{17, 65, 11 * 8, 20}, ""};
|
||||||
|
|
||||||
Text text_status{
|
// Radio controls
|
||||||
{10, 16, 220, 16},
|
Text label_rf_amp{{17, 90, 35, 16}, "AMP:"};
|
||||||
"Choose your team!"};
|
Checkbox checkbox_rf_amp{{55, 90}, 3, "", false};
|
||||||
|
|
||||||
Text text_score{
|
Text label_lna{{17, 118, 30, 16}, "LNA:"};
|
||||||
{170, 16, 60, 16},
|
NumberField field_lna{{50, 118}, 2, {0, 40}, 8, ' '};
|
||||||
"W:0 L:0"};
|
|
||||||
|
|
||||||
Button button_red_team{
|
Text label_vga{{90, 118, 30, 16}, "VGA:"};
|
||||||
{20, 100, 90, 40},
|
NumberField field_vga{{125, 118}, 2, {0, 62}, 8, ' '};
|
||||||
"RED TEAM"};
|
|
||||||
|
|
||||||
Button button_blue_team{
|
Text label_tx_gain{{155, 118, 25, 16}, "TX:"};
|
||||||
{130, 100, 90, 40},
|
NumberField field_tx_gain{{185, 118}, 2, {0, 47}, 8, ' '};
|
||||||
"BLUE TEAM"};
|
|
||||||
|
|
||||||
Button button_rotate{
|
Rectangle rect_audio_settings{{12, 164, 216, 45}, Color::dark_grey()};
|
||||||
{10, BUTTON_Y, 60, 32},
|
Text label_audio{{17, 169, 80, 16}, "AUDIO"};
|
||||||
"Rotate"};
|
Checkbox checkbox_sound{{17, 187}, 8, "Sound On", true};
|
||||||
|
Text label_volume{{110, 187, 50, 16}, "Volume:"};
|
||||||
|
AudioVolumeField field_volume{{165, 187}};
|
||||||
|
|
||||||
Button button_place{
|
Rectangle rect_team_selection{{12, 217, 216, 75}, Color::dark_grey()};
|
||||||
{80, BUTTON_Y, 60, 32},
|
Text label_team{{17, 222, 110, 16}, "SELECT TEAM"};
|
||||||
"Place"};
|
Button button_red_team{{25, 242, 85, 45}, "RED\nTEAM"};
|
||||||
|
Button button_blue_team{{130, 242, 85, 45}, "BLUE\nTEAM"};
|
||||||
|
|
||||||
Button button_fire{
|
// In-game UI elements
|
||||||
{80, BUTTON_Y, 60, 32},
|
RSSI rssi{{21 * 8, 0, 6 * 8, 4}};
|
||||||
"Fire!"};
|
Text text_status{{10, 16, 220, 16}, ""};
|
||||||
|
Text text_score{{170, 16, 60, 16}, ""};
|
||||||
Button button_menu{
|
Button button_rotate{{10, 265, 65, 32}, "Rotate"};
|
||||||
{150, BUTTON_Y, 60, 32},
|
Button button_place{{82, 265, 65, 32}, "Place"};
|
||||||
"Menu"};
|
Button button_fire{{82, 265, 65, 32}, "Fire!"};
|
||||||
|
Button button_menu{{155, 265, 65, 32}, "Menu"};
|
||||||
|
|
||||||
|
// Methods
|
||||||
void init_game();
|
void init_game();
|
||||||
void reset_game();
|
void reset_game();
|
||||||
void start_team(bool red);
|
void start_team(bool red);
|
||||||
void setup_ships();
|
void setup_ships();
|
||||||
|
void draw_menu_screen(Painter& painter);
|
||||||
void draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
|
void draw_grid(Painter& painter, uint8_t grid_x, uint8_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid = false);
|
||||||
void draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
|
void draw_cell(Painter& painter, uint8_t cell_x, uint8_t cell_y, CellState state, bool show_ships, bool is_offense_grid, bool is_cursor);
|
||||||
void draw_ship_preview(Painter& painter);
|
void draw_ship_preview(Painter& painter);
|
||||||
|
Reference in New Issue
Block a user