Compare commits

...
Author SHA1 Message Date
David f4fe2d48c1 Improve SubGhz proto filter handling
Build Dev Firmware / build (push) Has been cancelled
2026-07-15 12:03:08 +02:00
d4rks1d33 bc59edff17 FlipperGB is more fluid and stable, tutorial ask on discord
Build Dev Firmware / build (push) Has been cancelled
2026-07-15 00:56:04 -03:00
d4rks1d33 63d951dc38 Update ProtoPirate
Build Dev Firmware / build (push) Has been cancelled
2026-07-13 22:15:44 -03:00
d4rks1d33 f1422cb701 Full dpad emulation on receiver scene not need to save the signal to use all the buttons 2026-07-13 22:14:08 -03:00
David f7b04f1382 Fix saved signal editor command and counter updates
Build Dev Firmware / build (push) Has been cancelled
2026-07-12 23:55:11 +02:00
David 7286b5c529 Avoid in-place FastFAP objcopy 2026-07-12 21:29:36 +02:00
David 6a49fd89a1 Fix saved signal editor availability 2026-07-12 21:21:28 +02:00
David c4378c2e20 Approve SubGhz custom button API symbol 2026-07-12 19:36:51 +02:00
David 6e5ba9ec22 Rework saved signal editor and duplicate warning 2026-07-12 16:31:22 +02:00
David 502570a18b Show signal editor from saved file menu 2026-07-12 14:22:44 +02:00
David e606c5b24f Add saved signal editor and duplicate detection 2026-07-12 13:58:10 +02:00
David b34a73b08a Keep protocol encoders running while held
Build Dev Firmware / build (push) Has been cancelled
2026-07-12 11:54:06 +02:00
David 069a42d697 Repeat car emulate TX while held
Build Dev Firmware / build (push) Has been cancelled
2026-07-12 11:24:21 +02:00
David 2a182a2f5c Allow saved KIA V5 captures to reopen
Build Dev Firmware / build (push) Has been cancelled
2026-07-11 18:33:15 +02:00
David b13f42f8bc Fix KeeLoq and KIA V5 replay handling
Build Dev Firmware / build (push) Has been cancelled
2026-07-11 18:00:58 +02:00
82 changed files with 2558 additions and 1098 deletions
@@ -15,6 +15,7 @@ typedef enum {
SubGhzCustomEventSceneReceiverInfoTxStart,
SubGhzCustomEventSceneReceiverInfoTxStop,
SubGhzCustomEventSceneReceiverInfoSave,
SubGhzCustomEventSceneReceiverInfoTxFullDpad,
SubGhzCustomEventSceneSaveName,
SubGhzCustomEventSceneSignalSettings,
SubGhzCustomEventSceneSaveSuccess,
@@ -361,6 +361,49 @@ SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat*
return ret;
}
bool subghz_txrx_rebuild_from_fff(SubGhzTxRx* instance, FlipperFormat* flipper_format) {
furi_assert(instance);
furi_assert(flipper_format);
subghz_txrx_stop(instance);
bool rebuilt = false;
FuriString* temp_str = furi_string_alloc();
SubGhzTransmitter* transmitter = NULL;
do {
if(!flipper_format_rewind(flipper_format)) {
FURI_LOG_E(TAG, "Rewind error");
break;
}
if(!flipper_format_read_string(flipper_format, "Protocol", temp_str)) {
FURI_LOG_E(TAG, "Missing Protocol");
break;
}
transmitter =
subghz_transmitter_alloc_init(instance->environment, furi_string_get_cstr(temp_str));
if(!transmitter) {
FURI_LOG_E(TAG, "Protocol not found");
break;
}
rebuilt =
subghz_transmitter_deserialize(transmitter, flipper_format) == SubGhzProtocolStatusOk;
if(!rebuilt) {
FURI_LOG_E(TAG, "Protocol rebuild failed");
break;
}
} while(false);
if(transmitter) {
subghz_transmitter_free(transmitter);
}
furi_string_free(temp_str);
return rebuilt;
}
void subghz_txrx_rx_start(SubGhzTxRx* instance) {
furi_assert(instance);
subghz_txrx_stop(instance);
@@ -105,6 +105,15 @@ void subghz_txrx_get_frequency_and_modulation(
*/
SubGhzTxRxStartTxState subghz_txrx_tx_start(SubGhzTxRx* instance, FlipperFormat* flipper_format);
/**
* Rebuild protocol data without starting TX.
*
* @param instance Pointer to a SubGhzTxRx
* @param flipper_format Pointer to a FlipperFormat
* @return bool True when encoder deserialization updated protocol data successfully
*/
bool subghz_txrx_rebuild_from_fff(SubGhzTxRx* instance, FlipperFormat* flipper_format);
/**
* Start RX CC1101
*
@@ -34,7 +34,7 @@ typedef struct {
bool stop_pending; /* stop requested before MIN_TX_TICKS elapsed */
uint32_t tx_start_tick;
/* Pending button key (InputKey) decoded from the packed custom event */
/* Resolved custom button repeated while the physical key is held */
uint8_t pending_button;
} CarEmulateState;
@@ -252,12 +252,34 @@ static bool car_emulate_start_tx(SubGhz* subghz, uint8_t custom_btn_id) {
/** Stop an active transmission. */
static void car_emulate_stop_tx(SubGhz* subghz) {
subghz_block_generic_global.endless_tx = false;
subghz_txrx_stop(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
FURI_LOG_I(TAG, "TX stopped");
}
static bool car_emulate_restart_tx(SubGhz* subghz) {
furi_assert(s_state);
car_emulate_update_fff(subghz, s_state->current_counter);
subghz_block_generic_global.endless_tx = true;
if(car_emulate_start_tx(subghz, s_state->pending_button)) {
s_state->is_transmitting = true;
subghz->state_notifications = SubGhzNotificationStateTx;
notification_message(subghz->notifications, &sequence_blink_magenta_10);
FURI_LOG_D(TAG, "TX restarted while button is held");
return true;
}
subghz_block_generic_global.endless_tx = false;
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_error);
return false;
}
/* ═══════════════════════════════════════════════════════════════════════════
* View callback (fired from the View's input handler)
* ═════════════════════════════════════════════════════════════════════════*/
@@ -293,6 +315,7 @@ void subghz_scene_car_emulate_on_enter(void* context) {
s_state = malloc(sizeof(CarEmulateState));
furi_check(s_state);
memset(s_state, 0, sizeof(CarEmulateState));
subghz_block_generic_global.endless_tx = false;
/* ── Read metadata from the loaded fff_data ── */
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
@@ -401,8 +424,11 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
s_state->tx_start_tick = (uint32_t)furi_get_tick();
uint8_t cur_btn = subghz_custom_btn_get();
s_state->pending_button = cur_btn;
subghz_block_generic_global.endless_tx = true;
if(!car_emulate_start_tx(subghz, cur_btn)) {
s_state->is_transmitting = false;
subghz_block_generic_global.endless_tx = false;
notification_message(subghz->notifications, &sequence_error);
}
@@ -411,11 +437,13 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
/* ── Stop ── */
} else if(event.event == SubGhzCustomEventCarEmulateStop) {
if(s_state->is_transmitting &&
subghz->state_notifications == SubGhzNotificationStateTx) {
if(s_state->is_transmitting) {
subghz_block_generic_global.endless_tx = false;
uint32_t elapsed = (uint32_t)furi_get_tick() - s_state->tx_start_tick;
if(elapsed >= MIN_TX_TICKS) {
if(
elapsed >= MIN_TX_TICKS &&
subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
s_state->is_transmitting = false;
s_state->stop_pending = false;
@@ -431,6 +459,7 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
if(subghz->state_notifications == SubGhzNotificationStateTx) {
car_emulate_stop_tx(subghz);
}
subghz_block_generic_global.endless_tx = false;
scene_manager_search_and_switch_to_previous_scene(
subghz->scene_manager, SubGhzSceneSavedMenu);
consumed = true;
@@ -450,6 +479,8 @@ bool subghz_scene_car_emulate_on_event(void* context, SceneManagerEvent event) {
s_state->is_transmitting = false;
s_state->stop_pending = false;
notification_message(subghz->notifications, &sequence_blink_stop);
} else {
car_emulate_restart_tx(subghz);
}
} else {
/* Still transmitting blink LED */
@@ -485,6 +516,7 @@ void subghz_scene_car_emulate_on_exit(void* context) {
car_emulate_stop_tx(subghz);
}
subghz_block_generic_global.endless_tx = false;
subghz->state_notifications = SubGhzNotificationStateIDLE;
notification_message(subghz->notifications, &sequence_blink_stop);
@@ -187,12 +187,15 @@ bool subghz_scene_keeloq_decrypt_on_event(void* context, SceneManagerEvent event
if(!subghz->keeloq_keys_manager) {
subghz->keeloq_keys_manager = subghz_keeloq_keys_alloc();
}
uint16_t learning_type = ctx->recovered_type ?
ctx->recovered_type :
KEELOQ_LEARNING_SIMPLE;
char key_name[24];
snprintf(key_name, sizeof(key_name), "BF_%07lX", ctx->serial);
subghz_keeloq_keys_add(
subghz->keeloq_keys_manager,
ctx->recovered_mfkey,
KEELOQ_LEARNING_SIMPLE,
learning_type,
key_name);
subghz_keeloq_keys_save(subghz->keeloq_keys_manager);
@@ -202,7 +205,7 @@ bool subghz_scene_keeloq_decrypt_on_event(void* context, SceneManagerEvent event
SubGhzKey* entry = SubGhzKeyArray_push_raw(*env_arr);
entry->name = furi_string_alloc_set(key_name);
entry->key = ctx->recovered_mfkey;
entry->type = KEELOQ_LEARNING_SIMPLE;
entry->type = learning_type;
return true;
} else if(event.event == KL_DECRYPT_EVENT_DONE) {
@@ -1,105 +1,68 @@
#include "../subghz_i.h"
#include <lib/subghz/subghz_protocol_registry.h>
#include <stdlib.h>
#define TAG "SubGhzSceneProtocolList"
/* ── helpers ──────────────────────────────────────────────────────────────── */
typedef struct {
SubGhz* subghz;
const char* protocol_name;
} SubGhzProtocolListItemContext;
static bool proto_filter_contains(const char* filter, const char* name) {
const char* p = filter;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(len == strlen(name) && strncmp(p, name, len) == 0) return true;
if(!comma) break;
p = comma + 1;
}
return false;
static SubGhzProtocolListItemContext* protocol_list_item_contexts = NULL;
static void subghz_scene_protocol_list_free_contexts(void) {
free(protocol_list_item_contexts);
protocol_list_item_contexts = NULL;
}
static void proto_filter_toggle(char* filter, size_t filter_size, const char* name) {
if(proto_filter_contains(filter, name)) {
/* remove it */
char tmp[256] = {0};
const char* p = filter;
bool first = true;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(!(len == strlen(name) && strncmp(p, name, len) == 0)) {
if(!first) strncat(tmp, ",", sizeof(tmp) - strlen(tmp) - 1);
strncat(tmp, p, len < sizeof(tmp) - strlen(tmp) - 1 ? len : 0);
first = false;
}
if(!comma) break;
p = comma + 1;
}
strncpy(filter, tmp, filter_size - 1);
filter[filter_size - 1] = '\0';
} else {
/* add it */
if(filter[0] != '\0') strncat(filter, ",", filter_size - strlen(filter) - 1);
strncat(filter, name, filter_size - strlen(filter) - 1);
}
}
/* ── callbacks ────────────────────────────────────────────────────────────── */
static void subghz_scene_protocol_list_item_changed(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t value_index = variable_item_get_current_value_index(item);
SubGhzProtocolListItemContext* item_context = variable_item_get_context(item);
if(!item_context || !item_context->subghz || !item_context->protocol_name) return;
uint32_t proto_idx =
scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneProtocolList);
size_t selected =
variable_item_list_get_selected_item_index(subghz->variable_item_list);
UNUSED(proto_idx);
SubGhz* subghz = item_context->subghz;
bool should_disable = variable_item_get_current_value_index(item) == 1;
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, selected);
if(!protocol) return;
bool changed = subghz_last_settings_protocol_filter_set(
subghz->last_settings, item_context->protocol_name, should_disable);
bool is_disabled = subghz_last_settings_protocol_filter_contains(
subghz->last_settings, item_context->protocol_name);
const char* name = protocol->name;
bool currently_in =
proto_filter_contains(subghz->last_settings->protocol_filter, name);
if((value_index == 1) != currently_in) {
proto_filter_toggle(
subghz->last_settings->protocol_filter,
sizeof(subghz->last_settings->protocol_filter),
name);
}
variable_item_set_current_value_text(item, value_index ? "ONLY" : "---");
subghz_last_settings_save(subghz->last_settings);
variable_item_set_current_value_index(item, is_disabled ? 1 : 0);
variable_item_set_current_value_text(item, is_disabled ? "OFF" : "ON");
if(changed) subghz_last_settings_save(subghz->last_settings);
}
/* ── scene callbacks ──────────────────────────────────────────────────────── */
void subghz_scene_protocol_list_on_enter(void* context) {
SubGhz* subghz = context;
VariableItemList* list = subghz->variable_item_list;
variable_item_list_reset(list);
subghz_scene_protocol_list_free_contexts();
size_t protocol_count = subghz_protocol_registry_count(&subghz_protocol_registry);
protocol_list_item_contexts =
malloc(sizeof(SubGhzProtocolListItemContext) * protocol_count);
furi_check(protocol_list_item_contexts);
for(size_t i = 0; i < protocol_count; i++) {
const SubGhzProtocol* protocol =
subghz_protocol_registry_get_by_index(&subghz_protocol_registry, i);
if(!protocol) continue;
protocol_list_item_contexts[i].subghz = subghz;
protocol_list_item_contexts[i].protocol_name = protocol->name;
VariableItem* item = variable_item_list_add(
list,
protocol->name,
2,
subghz_scene_protocol_list_item_changed,
subghz);
&protocol_list_item_contexts[i]);
bool enabled = proto_filter_contains(
subghz->last_settings->protocol_filter, protocol->name);
variable_item_set_current_value_index(item, enabled ? 1 : 0);
variable_item_set_current_value_text(item, enabled ? "ONLY" : "---");
bool is_disabled =
subghz_last_settings_protocol_filter_contains(subghz->last_settings, protocol->name);
variable_item_set_current_value_index(item, is_disabled ? 1 : 0);
variable_item_set_current_value_text(item, is_disabled ? "OFF" : "ON");
}
variable_item_list_set_selected_item(
@@ -132,4 +95,5 @@ void subghz_scene_protocol_list_on_exit(void* context) {
SubGhzSceneProtocolList,
variable_item_list_get_selected_item_index(subghz->variable_item_list));
variable_item_list_reset(subghz->variable_item_list);
subghz_scene_protocol_list_free_contexts();
}
@@ -105,27 +105,10 @@ static void subghz_scene_add_to_history_callback(
SubGhz* subghz = context;
// The check can be moved to /lib/subghz/receiver.c, but may result in false positives
/* Protocol name allowlist filter — if non-empty, drop anything not in the list */
if(subghz->last_settings->protocol_filter[0] != '\0') {
const char* proto_name = decoder_base->protocol->name;
const char* filter = subghz->last_settings->protocol_filter;
bool allowed = false;
/* Walk the comma-separated list */
const char* p = filter;
while(*p) {
const char* comma = strchr(p, ',');
size_t len = comma ? (size_t)(comma - p) : strlen(p);
if(len == strlen(proto_name) && strncmp(p, proto_name, len) == 0) {
allowed = true;
break;
}
if(!comma) break;
p = comma + 1;
}
if(!allowed) {
FURI_LOG_D(TAG, "%s filtered by allowlist", proto_name);
return;
}
if(subghz_last_settings_protocol_filter_contains(
subghz->last_settings, decoder_base->protocol->name)) {
FURI_LOG_D(TAG, "%s filtered by protocol OFF list", decoder_base->protocol->name);
return;
}
if((decoder_base->protocol->flag & subghz->ignore_filter) == 0) {
@@ -679,15 +679,13 @@ void subghz_scene_receiver_config_on_enter(void* context) {
1,
NULL,
subghz);
if(subghz->last_settings->protocol_filter[0] == '\0') {
variable_item_set_current_value_text(item, "All");
uint8_t protocol_filter_count =
subghz_last_settings_protocol_filter_count(subghz->last_settings);
if(protocol_filter_count == 0) {
variable_item_set_current_value_text(item, "All ON");
} else {
uint8_t count = 1;
for(const char* p = subghz->last_settings->protocol_filter; *p; p++) {
if(*p == ',') count++;
}
static char filter_count_str[8];
snprintf(filter_count_str, sizeof(filter_count_str), "%u set", count);
snprintf(filter_count_str, sizeof(filter_count_str), "%u OFF", protocol_filter_count);
variable_item_set_current_value_text(item, filter_count_str);
}
@@ -1,6 +1,7 @@
#include "../subghz_i.h"
#include <lib/subghz/blocks/custom_btn.h>
#include <flipper_format/flipper_format_i.h>
#include "applications/main/subghz/helpers/subghz_txrx_i.h"
#include <lib/subghz/blocks/generic.h>
@@ -20,6 +21,9 @@ void subghz_scene_receiver_info_callback(GuiButtonType result, InputType type, v
} else if((result == GuiButtonTypeRight) && (type == InputTypeShort)) {
view_dispatcher_send_custom_event(
subghz->view_dispatcher, SubGhzCustomEventSceneReceiverInfoSave);
} else if((result == GuiButtonTypeLeft) && (type == InputTypeShort)) {
view_dispatcher_send_custom_event(
subghz->view_dispatcher, SubGhzCustomEventSceneReceiverInfoTxFullDpad);
}
}
@@ -29,7 +33,6 @@ static bool subghz_scene_receiver_info_update_parser(void* context) {
if(subghz_txrx_load_decoder_by_name_protocol(
subghz->txrx,
subghz_history_get_protocol_name(subghz->history, subghz->idx_menu_chosen))) {
// we are trying to deserialize without checking for errors, since it is assumed that we just received this chignal
subghz_protocol_decoder_base_deserialize(
subghz_txrx_get_decoder(subghz->txrx),
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen));
@@ -37,7 +40,6 @@ static bool subghz_scene_receiver_info_update_parser(void* context) {
SubGhzRadioPreset* preset =
subghz_history_get_radio_preset(subghz->history, subghz->idx_menu_chosen);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset->data, preset->data_size, subghz->tx_power);
subghz_txrx_set_preset(
@@ -93,7 +95,7 @@ void subghz_scene_receiver_info_draw_widget(SubGhz* subghz) {
subghz_scene_receiver_info_callback,
subghz);
}
// Removed static check
if(subghz_txrx_protocol_is_transmittable(subghz->txrx, false)) {
widget_add_button_element(
subghz->widget,
@@ -101,9 +103,14 @@ void subghz_scene_receiver_info_draw_widget(SubGhz* subghz) {
"Send",
subghz_scene_receiver_info_callback,
subghz);
widget_add_button_element(
subghz->widget,
GuiButtonTypeLeft,
"Full",
subghz_scene_receiver_info_callback,
subghz);
}
} else {
// [NO_DOLPHIN] widget_add_icon_element(subghz->widget, 83, 22, &I_WarningDolphinFlip_45x42);
widget_add_string_element(
subghz->widget, 13, 8, AlignLeft, AlignBottom, FontSecondary, "Error history parse.");
}
@@ -131,12 +138,7 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
if(!subghz_scene_receiver_info_update_parser(subghz)) {
return false;
}
//CC1101 Stop RX -> Start TX
subghz_txrx_hopper_pause(subghz->txrx);
// key concept: we start endless TX until user release OK button, and after this we send last
// protocols repeats - this guarantee that one press OK will
// be guarantee send the required minimum protocol data packets
// for all of this we use subghz_block_generic_global.endless_tx in protocols _yield function.
subghz->state_notifications = SubGhzNotificationStateTx;
subghz_block_generic_global.endless_tx = true;
if(!subghz_tx_start(
@@ -146,37 +148,51 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
subghz_txrx_hopper_unpause(subghz->txrx);
subghz->state_notifications = SubGhzNotificationStateRx;
subghz_block_generic_global.endless_tx = false;
return true;
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxStop) {
//CC1101 Stop Tx -> next tick event Start RX
// user release OK
// we switch off endless_tx - that mean protocols yield finish endless transmission,
// send upload "repeat=xx" times, and after will be stoped by the tick event down in this code
subghz->state_notifications = SubGhzNotificationStateTxWait;
subghz_block_generic_global.endless_tx = false;
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoSave) {
//CC1101 Stop RX -> Save
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_hopper_set_state(subghz->txrx, SubGhzHopperStateOFF);
subghz_txrx_stop(subghz->txrx);
if(!subghz_scene_receiver_info_update_parser(subghz)) {
return false;
}
if(subghz_txrx_protocol_is_serializable(subghz->txrx)) {
subghz_file_name_clear(subghz);
subghz->save_datetime =
subghz_history_get_datetime(subghz->history, subghz->idx_menu_chosen);
subghz->save_datetime_set = true;
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneSaveName);
}
return true;
} else if(event.event == SubGhzCustomEventSceneReceiverInfoTxFullDpad) {
if(!subghz_scene_receiver_info_update_parser(subghz)) {
return false;
}
FlipperFormat* fff_history =
subghz_history_get_raw_data(subghz->history, subghz->idx_menu_chosen);
FlipperFormat* fff_data = subghz_txrx_get_fff_data(subghz->txrx);
Stream* src = flipper_format_get_raw_stream(fff_history);
Stream* dst = flipper_format_get_raw_stream(fff_data);
stream_seek(src, 0, StreamOffsetFromStart);
stream_clean(dst);
stream_copy_full(src, dst);
stream_seek(dst, 0, StreamOffsetFromStart);
scene_manager_next_scene(subghz->scene_manager, SubGhzSceneTransmitter);
return true;
}
} else if(event.type == SceneManagerEventTypeTick) {
if(subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF) {
subghz_txrx_hopper_update(subghz->txrx, subghz->last_settings->hopping_threshold);
@@ -193,18 +209,15 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
subghz->state_notifications = SubGhzNotificationStateRx;
break;
case SubGhzNotificationStateTxWait:
// we wait until hardware TX finished and after stop TX and start RX, else just blink led
if(!subghz_devices_is_async_complete_tx(subghz->txrx->radio_device)) {
notification_message(subghz->notifications, &sequence_blink_magenta_10);
} else {
subghz_txrx_stop(subghz->txrx);
// update screen
widget_reset(subghz->widget);
subghz_scene_receiver_info_draw_widget(subghz);
subghz->state_notifications = SubGhzNotificationStateIDLE;
if(!scene_manager_has_previous_scene(subghz->scene_manager, SubGhzSceneDecodeRAW)) {
if(!scene_manager_has_previous_scene(
subghz->scene_manager, SubGhzSceneDecodeRAW)) {
subghz_txrx_rx_start(subghz->txrx);
subghz_txrx_hopper_unpause(subghz->txrx);
if(!subghz_history_get_text_space_left(subghz->history, NULL)) {
@@ -222,7 +235,6 @@ bool subghz_scene_receiver_info_on_event(void* context, SceneManagerEvent event)
void subghz_scene_receiver_info_on_exit(void* context) {
SubGhz* subghz = context;
widget_reset(subghz->widget);
subghz_txrx_reset_dynamic_and_custom_btns(subghz->txrx);
}
@@ -110,6 +110,7 @@ bool subghz_scene_save_name_on_event(void* context, SceneManagerEvent event) {
} else if(event.type == SceneManagerEventTypeCustom) {
if(event.event == SubGhzCustomEventSceneSaveName) {
if(strcmp(subghz->file_name_tmp, "") != 0) {
furi_string_reset(subghz->error_str);
furi_string_cat_printf(
subghz->file_path,
"/%s%s",
@@ -12,8 +12,14 @@ void subghz_scene_save_success_on_enter(void* context) {
// Setup view
Popup* popup = subghz->popup;
// [NO_DOLPHIN] popup_set_icon(popup, 36, 5, &I_DolphinSaved_92x58);
popup_set_header(popup, "Saved", 15, 19, AlignLeft, AlignBottom);
popup_set_timeout(popup, 1500);
if(furi_string_size(subghz->error_str)) {
popup_set_header(popup, "Saved", 15, 4, AlignLeft, AlignTop);
popup_set_text(popup, furi_string_get_cstr(subghz->error_str), 4, 18, AlignLeft, AlignTop);
popup_set_timeout(popup, 2500);
} else {
popup_set_header(popup, "Saved", 15, 19, AlignLeft, AlignBottom);
popup_set_timeout(popup, 1500);
}
popup_set_context(popup, subghz);
popup_set_callback(popup, subghz_scene_save_success_popup_callback);
popup_enable_timeout(popup);
@@ -72,4 +78,5 @@ void subghz_scene_save_success_on_exit(void* context) {
Popup* popup = subghz->popup;
popup_reset(popup);
furi_string_reset(subghz->error_str);
}
@@ -2,10 +2,10 @@
enum SubmenuIndex {
SubmenuIndexEmulate,
SubmenuIndexSignalSettings,
SubmenuIndexPsaDecrypt,
SubmenuIndexEdit,
SubmenuIndexDelete,
SubmenuIndexSignalSettings,
SubmenuIndexCounterBf, /* <-- comma was missing here */
SubmenuIndexCarEmulateSettings,
};
@@ -20,17 +20,20 @@ void subghz_scene_saved_menu_on_enter(void* context) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
bool is_psa_encrypted = false;
bool has_signal_editor = false;
bool has_counter = false;
if(fff) {
FuriString* proto = furi_string_alloc();
flipper_format_rewind(fff);
if(flipper_format_read_string(fff, "Protocol", proto)) {
has_signal_editor = !furi_string_equal_str(proto, "RAW");
if(furi_string_equal_str(proto, "PSA GROUP")) {
FuriString* type_str = furi_string_alloc();
flipper_format_rewind(fff);
if(!flipper_format_read_string(fff, "Type", type_str) ||
furi_string_equal_str(type_str, "00")) {
is_psa_encrypted = true;
has_signal_editor = false;
}
furi_string_free(type_str);
}
@@ -53,6 +56,15 @@ void subghz_scene_saved_menu_on_enter(void* context) {
SubmenuIndexEmulate,
subghz_scene_saved_menu_submenu_callback,
subghz);
if(has_signal_editor) {
submenu_add_item(
subghz->submenu,
"Signal Editor",
SubmenuIndexSignalSettings,
subghz_scene_saved_menu_submenu_callback,
subghz);
}
}
if(is_psa_encrypted) {
@@ -85,15 +97,6 @@ void subghz_scene_saved_menu_on_enter(void* context) {
subghz_scene_saved_menu_submenu_callback,
subghz);
if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug)) {
submenu_add_item(
subghz->submenu,
"Signal Settings",
SubmenuIndexSignalSettings,
subghz_scene_saved_menu_submenu_callback,
subghz);
}
if(has_counter) {
submenu_add_item(
subghz->submenu,
@@ -5,6 +5,9 @@
#include <machine/endian.h>
#include <toolbox/strint.h>
#include <lib/subghz/blocks/generic.h>
#include <lib/subghz/blocks/custom_btn_i.h>
#include <string.h>
#include <stdio.h>
#define TAG "SubGhzSceneSignalSettings"
@@ -13,6 +16,8 @@ static uint32_t counter32 = 0x0;
static uint16_t counter16 = 0x0;
static uint8_t cnt_byte_count = 0;
static uint8_t* cnt_byte_ptr = NULL;
static uint32_t counter_value = 0;
static uint32_t counter_mask = 0xffffffffUL;
static FuriString* byte_input_text;
@@ -21,6 +26,26 @@ static uint8_t btn_byte_count = 1;
static uint8_t* btn_byte_ptr = NULL;
static uint8_t submenu_called = 0;
static bool button_uses_custom_btn = false;
static uint8_t button_custom_id = SUBGHZ_CUSTOM_BTN_OK;
static bool subghz_scene_signal_settings_rebuild_save_reload(
SubGhz* subghz,
bool use_custom_btn,
uint8_t custom_btn_id);
enum {
SignalSettingsIndexCounterMode,
SignalSettingsIndexCounter,
SignalSettingsIndexButton,
};
enum {
SignalSettingsCounterStepDown,
SignalSettingsCounterStepValue,
SignalSettingsCounterStepUp,
SignalSettingsCounterStepCount,
};
#define COUNTER_MODE_COUNT 8
static const char* const counter_mode_text[COUNTER_MODE_COUNT] = {
@@ -45,6 +70,65 @@ static const int32_t counter_mode_value[COUNTER_MODE_COUNT] = {
7,
};
static const uint8_t button_value[] = {
SUBGHZ_CUSTOM_BTN_OK,
SUBGHZ_CUSTOM_BTN_UP,
SUBGHZ_CUSTOM_BTN_DOWN,
SUBGHZ_CUSTOM_BTN_LEFT,
SUBGHZ_CUSTOM_BTN_RIGHT,
5,
6,
7,
};
#define BUTTON_VALUE_COUNT (sizeof(button_value) / sizeof(button_value[0]))
static const char* const button_default_labels[BUTTON_VALUE_COUNT] = {
"Original",
"Up",
"Down",
"Left",
"Right",
"Button 5",
"Button 6",
"Button 7",
};
static const char* button_labels[BUTTON_VALUE_COUNT] = {
"Original",
"Up",
"Down",
"Left",
"Right",
"Button 5",
"Button 6",
"Button 7",
};
typedef struct {
const char* protocol;
const char* labels[BUTTON_VALUE_COUNT];
} ProtocolButtonLabels;
static const ProtocolButtonLabels protocol_button_labels[] = {
{"VAG GROUP", {"Original", "Lock", "Unlock", "Trunk", "Panic"}},
{"Porsche AG", {"Original", "Lock", "Unlock", "Trunk", "Open"}},
{"FORD V0", {"Original", "Lock", "Unlock", "Trunk"}},
{"Ford V2", {"Unlock", "Lock", "Trunk", "Panic", "Remote Start"}},
{"PSA GROUP", {"Original", "Lock", "Unlock", "Trunk", "Trunk"}},
{"KIA/HYU V0", {"Original", "Lock", "Unlock", "Trunk", "Horn"}},
{"KIA/HYU V1", {"Original", "Lock", "Unlock", "Trunk", "Panic"}},
{"KIA/HYU V2", {"Original", "Lock", "Unlock", "Trunk", "Panic"}},
{"KIA/HYU V3/V4", {"Original", "Lock", "Unlock", "Trunk", "Panic", "Horn"}},
{"KIA/HYU V5", {"Original", "Unlock", "Lock", "Trunk", "Horn"}},
{"KIA/HYU V6", {"Original", "Lock", "Unlock", "Trunk", "Panic"}},
{"SUBARU", {"Original", "Lock", "Unlock", "Trunk", "Panic", "Extra"}},
{"SUZUKI", {"Original", "Lock", "Unlock", "Trunk", "Panic"}},
{"Star Line", {"Original", "Lock", "Unlock", "Trunk", "Start"}},
{"Scher-Khan", {"Original", "Lock", "Unlock", "Trunk", "Start"}},
{"Sheriff CFM", {"Original", "Lock", "Unlock", "Trunk", "Panic"}},
};
typedef struct {
char* name;
uint8_t mode_count;
@@ -59,7 +143,225 @@ static Protocols protocols[] = {
{"Phoenix_V2", 3},
};
#define PROTOCOLS_COUNT (sizeof(protocols) / sizeof(Protocols));
#define PROTOCOLS_COUNT (sizeof(protocols) / sizeof(Protocols))
static void subghz_scene_signal_settings_reset_button_labels(void) {
for(uint8_t i = 0; i < BUTTON_VALUE_COUNT; i++) {
button_labels[i] = button_default_labels[i];
}
}
static void subghz_scene_signal_settings_apply_button_labels(const char* protocol) {
for(uint8_t i = 0; i < COUNT_OF(protocol_button_labels); i++) {
if(strcmp(protocol, protocol_button_labels[i].protocol) == 0) {
for(uint8_t btn = 0; btn < BUTTON_VALUE_COUNT; btn++) {
if(protocol_button_labels[i].labels[btn]) {
button_labels[btn] = protocol_button_labels[i].labels[btn];
}
}
break;
}
}
}
static const char* subghz_scene_signal_settings_get_button_label(uint8_t custom_btn_id) {
if(custom_btn_id == SUBGHZ_CUSTOM_BTN_OK) {
uint8_t original = subghz_custom_btn_get_original();
if((original != SUBGHZ_CUSTOM_BTN_OK) && (original < BUTTON_VALUE_COUNT)) {
return button_labels[original];
}
}
if(custom_btn_id < BUTTON_VALUE_COUNT) {
return button_labels[custom_btn_id];
}
return "Button";
}
static bool subghz_scene_signal_settings_update_uint32_field(
FlipperFormat* fff,
const char* key,
uint32_t value) {
flipper_format_rewind(fff);
return flipper_format_insert_or_update_uint32(fff, key, &value, 1);
}
static uint32_t subghz_scene_signal_settings_counter_get_mask(void) {
uint8_t bits = subghz_block_generic_global.cnt_length_bit;
if((bits == 0) || (bits >= 32)) {
return 0xffffffffUL;
}
return (1UL << bits) - 1UL;
}
static void subghz_scene_signal_settings_counter_sync_byte_input(void) {
if(cnt_byte_count == 4) {
counter32 = __bswap32(counter_value);
cnt_byte_ptr = (uint8_t*)&counter32;
} else if(cnt_byte_count == 2) {
counter16 = __bswap16((uint16_t)counter_value);
cnt_byte_ptr = (uint8_t*)&counter16;
}
}
static void subghz_scene_signal_settings_counter_set_value(uint32_t value) {
counter_value = value & counter_mask;
subghz_scene_signal_settings_counter_sync_byte_input();
}
static void subghz_scene_signal_settings_counter_format(FuriString* text) {
if(cnt_byte_count == 4) {
furi_string_printf(text, "%lX", (unsigned long)counter_value);
} else {
furi_string_printf(text, "%X", (unsigned int)(counter_value & 0xffffU));
}
}
static void subghz_scene_signal_settings_counter_update_item(VariableItem* item) {
char text[9] = {0};
variable_item_set_current_value_index(item, SignalSettingsCounterStepValue);
if(cnt_byte_count == 4) {
snprintf(text, sizeof(text), "%lX", (unsigned long)counter_value);
} else {
snprintf(text, sizeof(text), "%X", (unsigned int)(counter_value & 0xffffU));
}
variable_item_set_current_value_text(item, text);
}
static bool subghz_scene_signal_settings_counter_save(SubGhz* subghz) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
if(!subghz_scene_signal_settings_update_uint32_field(fff, "Cnt", counter_value)) {
FURI_LOG_E(TAG, "Error update/insert Cnt value");
dialog_message_show_storage_error(subghz->dialogs, "Cannot save\ncounter");
return false;
}
subghz_block_generic_global_counter_override_set(counter_value);
return subghz_scene_signal_settings_rebuild_save_reload(subghz, false, SUBGHZ_CUSTOM_BTN_OK);
}
static bool subghz_scene_signal_settings_hex_digit(char c, uint8_t* value) {
if(c >= '0' && c <= '9') {
*value = c - '0';
return true;
} else if(c >= 'a' && c <= 'f') {
*value = c - 'a' + 10;
return true;
} else if(c >= 'A' && c <= 'F') {
*value = c - 'A' + 10;
return true;
}
return false;
}
static bool subghz_scene_signal_settings_parse_hex_field(
const char* text,
const char* marker,
uint32_t* value,
uint8_t* length_bit) {
const char* field = strstr(text, marker);
if(!field) return false;
field += strlen(marker);
while(*field == ' ' || *field == '\t') {
field++;
}
uint32_t parsed = 0;
uint8_t digits = 0;
uint8_t digit_value = 0;
while(digits < 8 && subghz_scene_signal_settings_hex_digit(*field, &digit_value)) {
parsed = (parsed << 4) | digit_value;
digits++;
field++;
}
if(digits == 0) return false;
*value = parsed;
*length_bit = digits * 4;
return true;
}
static void subghz_scene_signal_settings_apply_text_fallback(FuriString* decoded_text) {
uint32_t value = 0;
uint8_t length_bit = 0;
const char* text = furi_string_get_cstr(decoded_text);
if(!subghz_block_generic_global.cnt_is_available &&
subghz_scene_signal_settings_parse_hex_field(text, "Cnt:", &value, &length_bit)) {
subghz_block_generic_global.cnt_is_available = true;
subghz_block_generic_global.current_cnt = value;
subghz_block_generic_global.cnt_length_bit = length_bit;
}
if(!subghz_block_generic_global.btn_is_available &&
subghz_scene_signal_settings_parse_hex_field(text, "Btn:", &value, &length_bit)) {
subghz_block_generic_global.btn_is_available = true;
subghz_block_generic_global.current_btn = value & 0xFF;
subghz_block_generic_global.btn_length_bit = length_bit > 8 ? 8 : length_bit;
}
}
static void subghz_scene_signal_settings_apply_file_fallback(FlipperFormat* fff) {
uint32_t value = 0;
if(!subghz_block_generic_global.cnt_is_available) {
flipper_format_rewind(fff);
if(flipper_format_read_uint32(fff, "Cnt", &value, 1)) {
subghz_block_generic_global.cnt_is_available = true;
subghz_block_generic_global.current_cnt = value;
subghz_block_generic_global.cnt_length_bit = 32;
}
}
if(!subghz_block_generic_global.btn_is_available) {
flipper_format_rewind(fff);
if(flipper_format_read_uint32(fff, "Btn", &value, 1)) {
subghz_block_generic_global.btn_is_available = true;
subghz_block_generic_global.current_btn = value & 0xFF;
subghz_block_generic_global.btn_length_bit = 8;
}
}
}
static bool subghz_scene_signal_settings_rebuild_save_reload(
SubGhz* subghz,
bool use_custom_btn,
uint8_t custom_btn_id) {
const char* file_path = furi_string_get_cstr(subghz->file_path);
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
bool updated = false;
int32_t counter_mult = furi_hal_subghz_get_rolling_counter_mult();
furi_hal_subghz_set_rolling_counter_mult(0);
if(use_custom_btn) {
subghz_custom_btn_set(custom_btn_id);
}
do {
if(!subghz_txrx_rebuild_from_fff(subghz->txrx, fff)) {
FURI_LOG_E(TAG, "Error rebuilding protocol data");
break;
}
if(!subghz_save_protocol_to_file(subghz, fff, file_path)) {
FURI_LOG_E(TAG, "Error saving edited signal");
break;
}
if(!subghz_key_load(subghz, file_path, false)) {
FURI_LOG_E(TAG, "Error reloading edited signal");
break;
}
updated = true;
} while(false);
furi_hal_subghz_set_rolling_counter_mult(counter_mult);
if(!updated) {
dialog_message_show_storage_error(subghz->dialogs, "Cannot save\nsignal");
}
return updated;
}
void subghz_scene_signal_settings_counter_mode_changed(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
@@ -99,6 +401,44 @@ void subghz_scene_signal_settings_counter_mode_changed(VariableItem* item) {
}
}
void subghz_scene_signal_settings_counter_changed(VariableItem* item) {
if(!cnt_byte_ptr || cnt_byte_count == 0) return;
uint8_t index = variable_item_get_current_value_index(item);
if(index == SignalSettingsCounterStepValue) {
subghz_scene_signal_settings_counter_update_item(item);
return;
}
if(index == SignalSettingsCounterStepUp) {
subghz_scene_signal_settings_counter_set_value(counter_value + 1);
} else {
subghz_scene_signal_settings_counter_set_value(counter_value - 1);
}
subghz_scene_signal_settings_counter_update_item(item);
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
subghz_scene_signal_settings_counter_save(subghz);
}
void subghz_scene_signal_settings_button_changed(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
if(index >= BUTTON_VALUE_COUNT) index = 0;
button_custom_id = button_value[index];
variable_item_set_current_value_text(
item, subghz_scene_signal_settings_get_button_label(button_custom_id));
if(!button_uses_custom_btn) return;
SubGhz* subghz = variable_item_get_context(item);
furi_assert(subghz);
subghz_scene_signal_settings_rebuild_save_reload(subghz, true, button_custom_id);
}
void subghz_scene_signal_settings_byte_input_callback(void* context) {
SubGhz* subghz = context;
view_dispatcher_send_custom_event(subghz->view_dispatcher, SubGhzCustomEventByteInputDone);
@@ -108,8 +448,10 @@ void subghz_scene_signal_settings_variable_item_list_enter_callback(void* contex
SubGhz* subghz = context;
// when we click OK on "Edit counter" item
if(index == 1) {
if(index == SignalSettingsIndexCounter) {
if(!cnt_byte_ptr || cnt_byte_count == 0) return;
submenu_called = 1;
furi_string_set_str(byte_input_text, "Enter ");
furi_string_cat_printf(byte_input_text, "%i", subghz_block_generic_global.cnt_length_bit);
furi_string_cat_str(byte_input_text, "-bits counter in HEX");
@@ -127,8 +469,10 @@ void subghz_scene_signal_settings_variable_item_list_enter_callback(void* contex
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdByteInput);
}
// when we click OK on "Edit button" item
if(index == 2) {
if(index == SignalSettingsIndexButton && !button_uses_custom_btn) {
if(!btn_byte_ptr || btn_byte_count == 0) return;
submenu_called = 2;
furi_string_set_str(byte_input_text, "Enter ");
furi_string_cat_printf(byte_input_text, "%i", subghz_block_generic_global.btn_length_bit);
furi_string_cat_str(byte_input_text, "-bits button in HEX");
@@ -150,6 +494,22 @@ void subghz_scene_signal_settings_variable_item_list_enter_callback(void* contex
void subghz_scene_signal_settings_on_enter(void* context) {
SubGhz* subghz = context;
counter32 = 0;
counter16 = 0;
cnt_byte_count = 0;
cnt_byte_ptr = NULL;
counter_value = 0;
counter_mask = 0xffffffffUL;
button = 0;
btn_byte_count = 1;
btn_byte_ptr = NULL;
submenu_called = 0;
button_uses_custom_btn = false;
button_custom_id = SUBGHZ_CUSTOM_BTN_OK;
subghz_block_generic_global_reset(NULL);
subghz_custom_btns_reset();
subghz_scene_signal_settings_reset_button_labels();
// ### Counter mode section ###
// When we open saved file we do some check and fill up subghz->file_path.
@@ -162,6 +522,7 @@ void subghz_scene_signal_settings_on_enter(void* context) {
Storage* storage = furi_record_open(RECORD_STORAGE);
FlipperFormat* fff_data_file = flipper_format_file_alloc(storage);
FuriString* tmp_text = furi_string_alloc_set_str("");
FuriString* protocol_name = furi_string_alloc();
uint32_t tmp_counter_mode = 0;
counter_mode = 0xff;
@@ -174,8 +535,9 @@ void subghz_scene_signal_settings_on_enter(void* context) {
FURI_LOG_E(TAG, "Error open file %s", file_path);
} else {
flipper_format_read_string(fff_data_file, "Protocol", tmp_text);
furi_string_set(protocol_name, tmp_text);
// compare available protocols names, load CounterMode value from file and setup variable_item_list values_count
for(uint8_t i = 0; i < PROTOCOLS_COUNT i++) {
for(uint8_t i = 0; i < PROTOCOLS_COUNT; i++) {
if(!strcmp(furi_string_get_cstr(tmp_text), protocols[i].name)) {
mode_count = protocols[i].mode_count;
if(flipper_format_read_uint32(fff_data_file, "CounterMode", &tmp_counter_mode, 1)) {
@@ -223,7 +585,10 @@ void subghz_scene_signal_settings_on_enter(void* context) {
// deserialaze and decode loaded sugbhz file and push data to subghz_block_generic_global variable
if(subghz_protocol_decoder_base_deserialize(decoder, subghz_txrx_get_fff_data(subghz->txrx)) ==
SubGhzProtocolStatusOk) {
subghz_scene_signal_settings_apply_button_labels(furi_string_get_cstr(protocol_name));
subghz_protocol_decoder_base_get_string(decoder, tmp_text);
subghz_scene_signal_settings_apply_text_fallback(tmp_text);
subghz_scene_signal_settings_apply_file_fallback(subghz_txrx_get_fff_data(subghz->txrx));
} else {
FURI_LOG_E(TAG, "Cant deserialize this subghz file");
}
@@ -232,55 +597,75 @@ void subghz_scene_signal_settings_on_enter(void* context) {
if(!subghz_block_generic_global.cnt_is_available) {
counter_mode = 0xff;
furi_string_set_str(tmp_text, "-");
FURI_LOG_D(TAG, "Counter mode and edit not available for this protocol");
} else {
counter_not_available = false;
counter_mask = subghz_scene_signal_settings_counter_get_mask();
// Check is there byte_count more than 2 hex bytes long or not
// To show hex value we must correct revert bytes for ByteInput view with __bswapХХ
// ByteInput stores the visible hex value as big-endian bytes.
if(subghz_block_generic_global.cnt_length_bit > 16) {
counter32 = subghz_block_generic_global.current_cnt;
furi_string_printf(tmp_text, "%lX", counter32);
counter32 = __bswap32(counter32);
cnt_byte_ptr = (uint8_t*)&counter32;
cnt_byte_count = 4;
} else {
counter16 = subghz_block_generic_global.current_cnt;
furi_string_printf(tmp_text, "%X", counter16);
counter16 = __bswap16(counter16);
cnt_byte_ptr = (uint8_t*)&counter16;
cnt_byte_count = 2;
}
subghz_scene_signal_settings_counter_set_value(subghz_block_generic_global.current_cnt);
subghz_scene_signal_settings_counter_format(tmp_text);
}
item = variable_item_list_add(variable_item_list, "Edit Counter", 1, NULL, subghz);
variable_item_set_current_value_index(item, 0);
item = variable_item_list_add(
variable_item_list,
"Edit Counter",
counter_not_available ? 1 : SignalSettingsCounterStepCount,
counter_not_available ? NULL : subghz_scene_signal_settings_counter_changed,
subghz);
variable_item_set_current_value_index(
item, counter_not_available ? 0 : SignalSettingsCounterStepValue);
variable_item_set_current_value_text(item, furi_string_get_cstr(tmp_text));
variable_item_set_locked(item, (counter_not_available), "Not available\nfor this\nprotocol !");
//
// ### Button edit section ###
if(!subghz_block_generic_global.btn_is_available) {
if(subghz_custom_btn_is_allowed()) {
uint8_t max_custom_btn = subghz_custom_btn_get_max();
uint8_t custom_button_count = max_custom_btn + 1;
if(custom_button_count > BUTTON_VALUE_COUNT) custom_button_count = BUTTON_VALUE_COUNT;
button_uses_custom_btn = custom_button_count > 1;
}
if(button_uses_custom_btn) {
button_not_available = false;
furi_string_set_str(
tmp_text, subghz_scene_signal_settings_get_button_label(SUBGHZ_CUSTOM_BTN_OK));
} else if(!subghz_block_generic_global.btn_is_available) {
furi_string_set_str(tmp_text, "-");
FURI_LOG_D(TAG, "Button edit not available for this protocol");
} else {
button_not_available = false;
button = subghz_block_generic_global.current_btn;
furi_string_printf(tmp_text, "%X", button);
btn_byte_ptr = (uint8_t*)&button;
furi_string_printf(tmp_text, "%X", button);
}
item = variable_item_list_add(variable_item_list, "Edit Button", 1, NULL, subghz);
uint8_t button_count = 1;
if(button_uses_custom_btn) {
button_count = subghz_custom_btn_get_max() + 1;
if(button_count > BUTTON_VALUE_COUNT) button_count = BUTTON_VALUE_COUNT;
}
item = variable_item_list_add(
variable_item_list,
button_uses_custom_btn ? "Button" : "Edit Button",
button_count,
button_uses_custom_btn ? subghz_scene_signal_settings_button_changed : NULL,
subghz);
variable_item_set_current_value_index(item, 0);
variable_item_set_current_value_text(item, furi_string_get_cstr(tmp_text));
variable_item_set_locked(item, (button_not_available), "Not available\nfor this\nprotocol !");
//
furi_assert(cnt_byte_ptr);
furi_assert(cnt_byte_count > 0);
furi_assert(btn_byte_ptr);
furi_string_free(tmp_text);
furi_string_free(protocol_name);
view_dispatcher_switch_to_view(subghz->view_dispatcher, SubGhzViewIdVariableItemList);
}
@@ -290,23 +675,21 @@ bool subghz_scene_signal_settings_on_event(void* context, SceneManagerEvent even
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == SubGhzCustomEventByteInputDone) {
FlipperFormat* fff = subghz_txrx_get_fff_data(subghz->txrx);
switch(submenu_called) {
// edit counter
case 1:
switch(cnt_byte_count) {
case 2:
// set new cnt value and override_flag to global variable and call transmit to generate and save subghz signal
counter16 = __bswap16(counter16);
subghz_block_generic_global_counter_override_set(counter16);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
subghz_scene_signal_settings_counter_set_value(counter16);
subghz_scene_signal_settings_counter_save(subghz);
break;
case 4:
// the same for 32 bit Counter
counter32 = __bswap32(counter32);
subghz_block_generic_global_counter_override_set(counter32);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
subghz_scene_signal_settings_counter_set_value(counter32);
subghz_scene_signal_settings_counter_save(subghz);
break;
default:
break;
@@ -314,14 +697,10 @@ bool subghz_scene_signal_settings_on_event(void* context, SceneManagerEvent even
break;
// edit button
case 2:
subghz_scene_signal_settings_update_uint32_field(fff, "Btn", button);
subghz_block_generic_global_button_override_set(button);
// save counter mult to rewrite subghz singnal without changing counter
int32_t tmp_counter = furi_hal_subghz_get_rolling_counter_mult();
furi_hal_subghz_set_rolling_counter_mult(0);
subghz_tx_start(subghz, subghz_txrx_get_fff_data(subghz->txrx));
subghz_txrx_stop(subghz->txrx);
// restore counter mult
furi_hal_subghz_set_rolling_counter_mult(tmp_counter);
subghz_scene_signal_settings_rebuild_save_reload(
subghz, false, SUBGHZ_CUSTOM_BTN_OK);
break;
default:
@@ -330,13 +709,10 @@ bool subghz_scene_signal_settings_on_event(void* context, SceneManagerEvent even
scene_manager_previous_scene(subghz->scene_manager);
return true;
} else {
if(event.type == SceneManagerEventTypeBack) {
scene_manager_previous_scene(subghz->scene_manager);
return true;
}
}
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_previous_scene(subghz->scene_manager);
return true;
}
return false;
}
@@ -352,4 +728,5 @@ void subghz_scene_signal_settings_on_exit(void* context) {
byte_input_set_result_callback(subghz->byte_input, NULL, NULL, NULL, NULL, 0);
byte_input_set_header_text(subghz->byte_input, "");
furi_string_free(byte_input_text);
subghz_custom_btns_reset();
}
@@ -23,7 +23,7 @@
#define SUBGHZ_LAST_SETTING_FIELD_LED_AND_POWER_AMP "LedAndPowerAmp"
#define SUBGHZ_LAST_SETTING_FIELD_TX_POWER "TXPower"
#define SUBGHZ_LAST_SETTING_FIELD_CUSTOM_CAR_EMULATE "CustomCarEmulate"
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER "ProtocolFilter"
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILTER "ProtocolFilterOff"
SubGhzLastSettings* subghz_last_settings_alloc(void) {
SubGhzLastSettings* instance = malloc(sizeof(SubGhzLastSettings));
@@ -187,6 +187,7 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
flipper_format_rewind(fff_data_file);
}
furi_string_free(filter_str);
subghz_last_settings_protocol_filter_normalize(instance);
} while(0);
} else {
+190 -1
View File
@@ -3,6 +3,7 @@
#include <furi_hal.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <storage/storage.h>
#include <lib/subghz/types.h>
@@ -13,6 +14,7 @@
#define SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY 433920000
#define SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_FEEDBACK_LEVEL 2
#define SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD (-80.0f)
#define SUBGHZ_LAST_SETTINGS_PROTOCOL_FILTER_SIZE 1024
typedef struct {
uint32_t frequency;
@@ -31,9 +33,196 @@ typedef struct {
bool leds_and_amp;
uint8_t tx_power;
bool custom_car_emulate;
char protocol_filter[256]; /* comma-separated allowlist, empty = disabled */
char protocol_filter[SUBGHZ_LAST_SETTINGS_PROTOCOL_FILTER_SIZE]; /* comma-separated disabled protocols, empty = all enabled */
} SubGhzLastSettings;
static inline void subghz_last_settings_protocol_filter_next_token(
const char** cursor,
const char** token,
size_t* token_len) {
const char* start = *cursor;
while((*start == ',') || (*start == ' ') || (*start == '\t')) {
start++;
}
const char* end = start;
while((*end != '\0') && (*end != ',')) {
end++;
}
const char* trim_end = end;
while((trim_end > start) && ((trim_end[-1] == ' ') || (trim_end[-1] == '\t'))) {
trim_end--;
}
*token = start;
*token_len = (size_t)(trim_end - start);
*cursor = (*end == ',') ? end + 1 : end;
}
static inline bool subghz_last_settings_protocol_filter_token_matches(
const char* token,
size_t token_len,
const char* name,
size_t name_len) {
return (token_len == name_len) && (strncmp(token, name, token_len) == 0);
}
static inline bool subghz_last_settings_protocol_filter_contains_token(
const char* filter,
const char* token,
size_t token_len) {
const char* cursor = filter;
const char* current = NULL;
size_t current_len = 0;
while(*cursor != '\0') {
subghz_last_settings_protocol_filter_next_token(&cursor, &current, &current_len);
if((current_len != 0) &&
subghz_last_settings_protocol_filter_token_matches(
current, current_len, token, token_len)) {
return true;
}
}
return false;
}
static inline bool subghz_last_settings_protocol_filter_append_token(
char* filter,
size_t filter_size,
const char* token,
size_t token_len) {
if(token_len == 0) return true;
size_t filter_len = strlen(filter);
size_t separator_len = filter_len == 0 ? 0 : 1;
if((filter_len + separator_len + token_len) >= filter_size) {
return false;
}
if(separator_len != 0) {
filter[filter_len++] = ',';
}
memcpy(&filter[filter_len], token, token_len);
filter[filter_len + token_len] = '\0';
return true;
}
static inline bool subghz_last_settings_protocol_filter_contains(
const SubGhzLastSettings* instance,
const char* protocol) {
if((instance == NULL) || (protocol == NULL) || (protocol[0] == '\0')) return false;
return subghz_last_settings_protocol_filter_contains_token(
instance->protocol_filter, protocol, strlen(protocol));
}
static inline bool subghz_last_settings_protocol_filter_normalize(
SubGhzLastSettings* instance) {
if(instance == NULL) return false;
char normalized[SUBGHZ_LAST_SETTINGS_PROTOCOL_FILTER_SIZE] = {0};
const char* cursor = instance->protocol_filter;
const char* token = NULL;
size_t token_len = 0;
while(*cursor != '\0') {
subghz_last_settings_protocol_filter_next_token(&cursor, &token, &token_len);
if((token_len == 0) ||
subghz_last_settings_protocol_filter_contains_token(normalized, token, token_len)) {
continue;
}
if(!subghz_last_settings_protocol_filter_append_token(
normalized, sizeof(normalized), token, token_len)) {
return false;
}
}
bool changed = strcmp(instance->protocol_filter, normalized) != 0;
if(changed) {
memcpy(instance->protocol_filter, normalized, sizeof(instance->protocol_filter));
}
return changed;
}
static inline bool subghz_last_settings_protocol_filter_set(
SubGhzLastSettings* instance,
const char* protocol,
bool disabled) {
if((instance == NULL) || (protocol == NULL) || (protocol[0] == '\0')) return false;
char updated[SUBGHZ_LAST_SETTINGS_PROTOCOL_FILTER_SIZE] = {0};
const char* cursor = instance->protocol_filter;
const char* token = NULL;
size_t token_len = 0;
const size_t protocol_len = strlen(protocol);
bool protocol_written = false;
while(*cursor != '\0') {
subghz_last_settings_protocol_filter_next_token(&cursor, &token, &token_len);
if(token_len == 0) continue;
bool is_target = subghz_last_settings_protocol_filter_token_matches(
token, token_len, protocol, protocol_len);
if(is_target) {
if(disabled && !protocol_written) {
if(!subghz_last_settings_protocol_filter_append_token(
updated, sizeof(updated), protocol, protocol_len)) {
return false;
}
protocol_written = true;
}
continue;
}
if(!subghz_last_settings_protocol_filter_contains_token(updated, token, token_len)) {
if(!subghz_last_settings_protocol_filter_append_token(
updated, sizeof(updated), token, token_len)) {
return false;
}
}
}
if(disabled && !protocol_written) {
if(!subghz_last_settings_protocol_filter_append_token(
updated, sizeof(updated), protocol, protocol_len)) {
return false;
}
}
bool changed = strcmp(instance->protocol_filter, updated) != 0;
if(changed) {
memcpy(instance->protocol_filter, updated, sizeof(instance->protocol_filter));
}
return changed;
}
static inline uint8_t subghz_last_settings_protocol_filter_count(
const SubGhzLastSettings* instance) {
if(instance == NULL) return 0;
char seen[SUBGHZ_LAST_SETTINGS_PROTOCOL_FILTER_SIZE] = {0};
const char* cursor = instance->protocol_filter;
const char* token = NULL;
size_t token_len = 0;
uint8_t count = 0;
while(*cursor != '\0') {
subghz_last_settings_protocol_filter_next_token(&cursor, &token, &token_len);
if((token_len == 0) ||
subghz_last_settings_protocol_filter_contains_token(seen, token, token_len)) {
continue;
}
if(!subghz_last_settings_protocol_filter_append_token(seen, sizeof(seen), token, token_len)) {
break;
}
count++;
}
return count;
}
SubGhzLastSettings* subghz_last_settings_alloc(void);
void subghz_last_settings_free(SubGhzLastSettings* instance);
+57 -15
View File
@@ -50,7 +50,7 @@ Navigate with **Up/Down**, activate with **OK**, close with **Back**.
| **Press START** | Sends a Start press to the game (pause menus, "PRESS START" screens) and resumes |
| **Press SELECT** | Sends a Select press to the game and resumes |
| **Frameskip** | Change with **Left/Right**: `auto` (recommended), or fixed `04`. `auto` shows the skip level currently in use |
| **Sound** | Toggle piezo sound on/off (`n/a` if the speaker is in use by another app) |
| **Sound** | Volume: cycle off/25/50/75/100% with **Left/Right**; **OK** toggles mute/full (`n/a` if the speaker is in use by another app) |
| **Save SRAM** | Writes the cartridge battery save (`.sav`) to the SD card immediately |
| **Exit** | Saves SRAM (if the cartridge has a battery) and quits the app |
@@ -95,8 +95,9 @@ piezo:
3. otherwise the noise channel, mapped to a short low buzz (percussion).
The result is a monophonic ringtone-style rendition of the game's music
and sound effects (sweeps like Mario's jump work). It can be toggled in
the emulator menu. Since no waveforms are synthesized, the CPU cost is
and sound effects (sweeps like Mario's jump work). Volume is adjustable in
the emulator menu (off/25/50/75/100%, perceptually spaced for the piezo's
nonlinear loudness). Since no waveforms are synthesized, the CPU cost is
negligible (one counter per emulated instruction) and RAM cost is ~120
bytes.
@@ -108,8 +109,31 @@ bytes.
(70224 M-cycles instead of 17556). Unnoticeable on a desktop, a slideshow
on a 64 MHz Cortex-M4. Fixed — this alone made everything ~4.5x faster.
- While halted (games spend most of each frame in HALT waiting for vblank),
the CPU steps 4 M-cycles at a time instead of 1, making the idle part of
the CPU steps 8 M-cycles at a time instead of 1, making the idle part of
the frame cheap.
- PPU/timer/APU use catch-up batching: instead of stepping their state
machines after every CPU instruction, cycles accumulate and are flushed
every 32 M-cycles - or immediately before any IO register access, so
LY/STAT/DIV/TIMA/IF always read exact. This removes the single biggest
per-instruction cost of the interpreter loop (~2.8x faster emulation).
- Undefined opcodes cost 1 cycle instead of 0: with 0-cycle entries,
executing garbage could advance the PC without advancing the PPU clock
and spin run_to_vblank() forever, freezing the whole device.
- EI enables interrupts only after the following instruction (hardware
behaviour). Without the delay, the classic `EI / HALT` pause idiom
consumed its wake interrupt before HALT and games froze waiting for a
button press that had already been eaten (input dead, music playing).
- STOP freezes the CPU until joypad activity, independent of IE/IF/IME
(hardware behaviour), and skips its padding byte.
- ROM streaming: bank switches to the already-mapped bank skip the cache;
the upper 8 KB half of a bank streams in lazily on first read; the LRU
never evicts the most-recently-used page and protects the single hottest
page (the music-driver bank) from map-streaming evictions; the Gameboy
core block is pre-allocated during ROM load so the page cache can size
itself against real free heap (Pokemon: ~2x more cache slots).
- The auto-frameskip EMA uses signed arithmetic (an unsigned underflow
could pin it at maximum), and the menu shows the measured cost of one
emulated frame in ms next to the free heap (16.7ms = full speed).
- **Frameskip `auto`** (default) measures the real cost of each emulated frame
and skips *rendering* (never emulation) to keep the game running at correct
speed. Games stay full-speed logically; visible FPS drops instead.
@@ -117,8 +141,26 @@ bytes.
- The PPU only renders the 64 scanlines (out of 144) that survive the
downscale to the Flipper LCD — ~55% of the per-frame rendering work is
skipped with zero visual difference.
- Bank-switch heavy games may micro-stutter when a 16 KB bank has to be
streamed from the SD card (only happens when the ROM doesn't fit in RAM).
- ROM streaming works on 8 KB pages (half a MBC bank): twice the cache
slots per KB of heap and half the SD stall per miss compared to whole-bank
caching. Bank-switch heavy games may still micro-stutter on a cache miss
(only happens when the ROM doesn't fit in RAM).
- The emulator core is compiled `-O2` (the rest of the app stays `-Os`) and
the per-instruction hot path (opcode fetch, PPU/timer/APU ticks) is
manually inlined across the core, cutting per-instruction call overhead.
- The CPU interpreter is a unity build: the 500 opcode bodies compile in
the same translation unit as the dispatch switch, so the compiler inlines
them directly into the jump table (two cross-TU calls per instruction
eliminated) - and the deduplication actually made the binary smaller.
- The PPU renders tiles byte-wise, not pixel-wise: tile bitplanes are
decoded 8 pixels at once through a 256-entry spread LUT and written as
whole palette-mapped bytes (4 px per store) - ~7x fewer operations per
scanline. Sprites skip fully-transparent rows and use the same LUTs.
- The 160x144 -> 128x64 downscale/dither also works one packed byte
(4 pixels) per lookup instead of per-pixel shade extraction.
- In `auto` mode the frameskip may go up to 8 (fixed settings stay 0-4):
for heavy streamed games, correct game speed at a lower visible fps beats
slow motion.
- The emulator menu shows the free heap (`NNk free`) so you can see the
memory headroom of the current game at a glance.
@@ -149,8 +191,8 @@ ufbt launch # builds, installs and runs on a connected Flipper
The exact core that ships in the FAP can be compiled and tested on a desktop:
```sh
g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -I gb \
-o hosttest/hosttest hosttest/main.cpp gb/*.cc
g++ -std=c++17 -O2 -fno-exceptions -fno-rtti -I lib/gbcore \
-o hosttest/hosttest hosttest/main.cpp lib/gbcore/*.cc
# Blargg CPU tests (print Passed/Failed via the serial port):
./hosttest/hosttest path/to/01-special.gb 4000
@@ -170,8 +212,8 @@ Current status: **Blargg `cpu_instrs` 11/11 PASS**.
| Upstream (PC) | This port (Flipper) |
|---|---|
| Whole ROM in RAM (with several transient copies) | 16 KB bank streaming from SD with an adaptive LRU cache; bank 0 resident; when every bank fits, the whole ROM is preloaded into individual 16 KB slots (O(1) switching, SD file closed) |
| — | All ROM-dependent allocations are 16 KB or smaller and are checked against the largest free heap block first: heap fragmentation can never crash the firmware, the app degrades to streaming or shows "Not enough RAM" instead |
| Whole ROM in RAM (with several transient copies) | 8 KB page streaming from SD with an adaptive LRU cache; bank 0 resident; when every page fits, the whole ROM is preloaded into individual 8 KB slots (O(1) switching, SD file closed) |
| — | All ROM-dependent allocations are 8 KB or smaller and are checked against the largest free heap block first: heap fragmentation can never crash the firmware, the app degrades to streaming or shows "Not enough RAM" instead |
| Renders all 144 scanlines | Renders only the 64 scanlines that are actually displayed after the 144→64 downscale (row mask, ~2x faster rendering) |
| PPU counts M-cycles against T-cycle constants (4x too much CPU emulation per frame) | Hardware-correct M-cycle constants (114 per scanline): ~4.5x faster overall |
| DIV register incremented every M-cycle (64x too fast) | Correct 16384 Hz rate (games use DIV for delays and randomness) |
@@ -187,14 +229,14 @@ Current status: **Blargg `cpu_instrs` 11/11 PASS**.
| No APU at all | Register-level APU (sweep/envelope/length/NR52, proper read-back masks) driving the piezo with the dominant voice |
RAM budget on device (256 KB total, ~140 KB heap; the app binary itself
loads into ~32 KB of that heap): ~19.5 KB emulation state, 16 KB bank 0,
adaptive bank cache (10 KB heap kept in reserve for the system), 032 KB
cartridge RAM per game, 4 KB stack. The bank cache is allocated greedily in
independent 16 KB blocks until the reserve would be touched, so any `.gb`
loads into ~37 KB of that heap): ~19.5 KB emulation state, 16 KB bank 0,
adaptive page cache (10 KB heap kept in reserve for the system), 032 KB
cartridge RAM per game, 4 KB stack. The page cache is allocated greedily in
independent 8 KB blocks until the reserve would be touched, so any `.gb`
ROM size works: small ROMs end up fully resident, large ones stream through
however many slots fit. Worst case (1 MB ROM + 32 KB battery RAM, e.g.
Pokémon Red/Blue) needs ~78 KB before the first cache slot, which fits the
post-launch heap with room for 12 streaming slots.
post-launch heap with room for a few streaming pages.
## License
+12 -1
View File
@@ -3,10 +3,21 @@ App(
name="FlipGB",
apptype=FlipperAppType.EXTERNAL,
entry_point="flipgb_app",
sources=["*.c*", "!hosttest"],
sources=["*.c*", "!hosttest", "!lib"],
requires=["gui", "dialogs", "storage"],
stack_size=8 * 1024,
cdefines=[("GB_FB_ROWS", "64")],
fap_private_libs=[
Lib(
# The emulator core is built as a private lib so it can use -O2:
# ufbt builds FAP sources with -Os, which costs ~30% emulation
# speed in the per-instruction hot path. Code size difference
# is ~6 KB - well worth it.
name="gbcore",
cflags=["-O2"],
cdefines=[("GB_FB_ROWS", "64")],
),
],
fap_category="Games",
fap_icon="flipgb_icon.png",
fap_author="user",
Binary file not shown.
Binary file not shown.
@@ -1,73 +0,0 @@
#pragma once
#include "input.h"
#include "cpu.h"
#include "video.h"
#include "timer.h"
#include "mmu.h"
#include "cartridge.h"
#include "apu.h"
class Gameboy {
public:
/* bank0: pointer to the first 16 KB of ROM (stays resident).
* provider: returns pointers to 16 KB switchable banks. */
Gameboy(
const u8* bank0,
uint rom_bank_count,
MBCType mbc,
u8* cart_ram,
u32 cart_ram_size,
RomBankProvider provider,
void* provider_ctx);
/* Runs the emulator until the next vblank (one full frame). */
void run_to_vblank();
/* Called on every vblank BEFORE the framebuffer is cleared for the next
* frame: this is where the frontend must convert/copy the image. */
void set_frame_callback(void (*cb)(void*), void* ctx) {
user_frame_cb = cb;
user_frame_ctx = ctx;
}
void button_pressed(GbButton button);
void button_released(GbButton button);
void set_skip_render(bool skip) { video.skip_render = skip; }
/* Optional display-line mask (see Video::row_mask). The array must stay
* valid for the lifetime of the emulator. */
void set_row_mask(const u8* mask) { video.set_row_mask(mask); }
auto get_framebuffer() const -> const FrameBuffer& { return video.get_framebuffer(); }
auto get_cartridge_ram() -> u8* { return cartridge.get_ram(); }
auto get_cartridge_ram_size() const -> u32 { return cartridge.get_ram_size(); }
Cartridge cartridge;
CPU cpu;
friend class CPU;
Video video;
friend class Video;
MMU mmu;
friend class MMU;
Timer timer;
friend class Timer;
Apu apu;
Input input;
private:
void tick();
static void vblank_trampoline(void* ctx);
volatile bool frame_done = false;
void (*user_frame_cb)(void*) = nullptr;
void* user_frame_ctx = nullptr;
};
-77
View File
@@ -1,77 +0,0 @@
#include "timer.h"
#include "definitions.h"
#include "gameboy.h"
#include "cpu.h"
#include "bitwise.h"
const uint CLOCKS_PER_CYCLE = 4;
Timer::Timer(Gameboy& _gb) : gb(_gb) {}
void Timer::tick(uint cycles) {
/* DIV increments at 16384 Hz = every 64 M-cycles (upstream incremented
* it once per M-cycle: 64x too fast, breaking games that use DIV for
* delays or randomness) */
div_clocks += cycles;
if(div_clocks >= 64) {
divider.set(static_cast<u8>(divider.value() + (div_clocks >> 6)));
div_clocks &= 63;
}
clocks += cycles * CLOCKS_PER_CYCLE;
auto timer_is_on = timer_control.check_bit(2);
if (timer_is_on == 0) { return; }
auto clock_limit = clocks_needed_to_increment();
if (clocks >= clock_limit) {
clocks = clocks % clock_limit;
u8 old_timer_counter = timer_counter.value();
timer_counter.increment();
if (timer_counter.value() < old_timer_counter) {
gb.cpu.interrupt_flag.set_bit_to(2, true);
timer_counter.set(timer_modulo.value());
}
}
}
auto Timer::get_divider() const -> u8 { return divider.value(); }
auto Timer::get_timer() const -> u8 { return timer_counter.value(); }
auto Timer::get_timer_modulo() const -> u8 { return timer_modulo.value(); }
// Only the bottom three bits of this register are usable
auto Timer::get_timer_control() const -> u8 { return timer_control.value() & 0x3; }
void Timer::reset_divider() {
divider.set(0x0);
}
void Timer::set_timer(u8 value) {
timer_counter.set(value);
}
void Timer::set_timer_modulo(u8 value) {
timer_modulo.set(value);
}
void Timer::set_timer_control(u8 value) {
timer_control.set(value);
}
uint Timer::clocks_needed_to_increment() {
using bitwise::check_bit;
switch (get_timer_control()) {
case 0: return CLOCK_RATE / 4096;
case 1: return CLOCK_RATE / 262144;
case 2: return CLOCK_RATE / 65536;
case 3: return CLOCK_RATE / 16384;
default: return CLOCK_RATE / 4096; /* unreachable */
}
}
-37
View File
@@ -1,37 +0,0 @@
#pragma once
#include "definitions.h"
#include "register.h"
class Gameboy;
class Timer {
public:
Timer(Gameboy& inGb);
void tick(uint cycles);
auto get_divider() const -> u8;
auto get_timer() const -> u8;
auto get_timer_modulo() const -> u8;
auto get_timer_control() const -> u8;
void reset_divider();
void set_timer(u8 value);
void set_timer_modulo(u8 value);
void set_timer_control(u8 value);
private:
uint clocks_needed_to_increment();
uint clocks = 0;
uint div_clocks = 0;
Gameboy& gb;
ByteRegister divider;
ByteRegister timer_counter;
ByteRegister timer_modulo;
ByteRegister timer_control;
};
-353
View File
@@ -1,353 +0,0 @@
#include "video.h"
#include "gameboy.h"
#include "cpu.h"
#include "bitwise.h"
using bitwise::check_bit;
Video::Video(Gameboy& inGb)
: gb(inGb) {
}
void Video::tick(Cycles cycles) {
cycle_counter += cycles.cycles;
switch(current_mode) {
case VideoMode::ACCESS_OAM:
if(cycle_counter >= CLOCKS_PER_SCANLINE_OAM) {
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_OAM;
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, true);
current_mode = VideoMode::ACCESS_VRAM;
}
break;
case VideoMode::ACCESS_VRAM:
if(cycle_counter >= CLOCKS_PER_SCANLINE_VRAM) {
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE_VRAM;
current_mode = VideoMode::HBLANK;
bool hblank_interrupt = check_bit(lcd_status.value(), 3);
if(hblank_interrupt) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
bool ly_coincidence_interrupt = check_bit(lcd_status.value(), 6);
bool ly_coincidence = ly_compare.value() == line.value();
if(ly_coincidence_interrupt && ly_coincidence) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
lcd_status.set_bit_to(2, ly_coincidence);
lcd_status.set_bit_to(1, false);
lcd_status.set_bit_to(0, false);
}
break;
case VideoMode::HBLANK:
if(cycle_counter >= CLOCKS_PER_HBLANK) {
if(!skip_render) write_scanline(line.value());
line.increment();
cycle_counter = cycle_counter % CLOCKS_PER_HBLANK;
/* Line 145 (index 144) is the first line of VBLANK */
if(line == 144) {
current_mode = VideoMode::VBLANK;
lcd_status.set_bit_to(1, false);
lcd_status.set_bit_to(0, true);
gb.cpu.interrupt_flag.set_bit_to(0, true);
} else {
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, false);
current_mode = VideoMode::ACCESS_OAM;
}
}
break;
case VideoMode::VBLANK:
if(cycle_counter >= CLOCKS_PER_SCANLINE) {
line.increment();
cycle_counter = cycle_counter % CLOCKS_PER_SCANLINE;
/* Line 155 (index 154) is the last line */
if(line == 154) {
if(!skip_render) {
write_sprites();
draw();
buffer.reset();
} else {
draw(); /* still notify the frontend for pacing */
}
line.reset();
current_mode = VideoMode::ACCESS_OAM;
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, false);
};
}
break;
}
}
auto Video::display_enabled() const -> bool {
return check_bit(control_byte, 7);
}
auto Video::window_tile_map() const -> bool {
return check_bit(control_byte, 6);
}
auto Video::window_enabled() const -> bool {
return check_bit(control_byte, 5);
}
auto Video::bg_window_tile_data() const -> bool {
return check_bit(control_byte, 4);
}
auto Video::bg_tile_map_display() const -> bool {
return check_bit(control_byte, 3);
}
auto Video::sprite_size() const -> bool {
return check_bit(control_byte, 2);
}
auto Video::sprites_enabled() const -> bool {
return check_bit(control_byte, 1);
}
auto Video::bg_enabled() const -> bool {
return check_bit(control_byte, 0);
}
void Video::write_scanline(u8 current_line) {
if(!display_enabled()) {
return;
}
/* Lines the frontend never displays are not worth rendering */
if(row_mask && current_line < GAMEBOY_HEIGHT && !row_mask[current_line]) {
return;
}
if(bg_enabled()) {
draw_bg_line(current_line);
}
if(window_enabled()) {
draw_window_line(current_line);
}
}
void Video::write_sprites() {
if(!sprites_enabled()) {
return;
}
for(uint sprite_n = 0; sprite_n < 40; sprite_n++) {
draw_sprite(sprite_n);
}
}
void Video::draw_bg_line(uint current_line) {
/* Note: tileset two uses signed numbering to share half the tiles with
* tileset one */
bool use_tile_set_zero = bg_window_tile_data();
bool use_tile_map_zero = !bg_tile_map_display();
Palette palette = load_palette(bg_palette);
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
uint screen_y = current_line;
uint scrolled_y = (screen_y + scroll_y.value()) % BG_MAP_SIZE;
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
uint tile_data_line_offset = tile_pixel_y * 2;
/* Render tile-by-tile instead of refetching the tile data for every
* pixel like upstream did */
uint scroll_x_val = scroll_x.value();
uint screen_x = 0;
while(screen_x < GAMEBOY_WIDTH) {
uint scrolled_x = (screen_x + scroll_x_val) % BG_MAP_SIZE;
uint tile_x = scrolled_x / TILE_WIDTH_PX;
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
uint tile_data_mem_offset = use_tile_set_zero ?
tile_id * TILE_BYTES :
static_cast<uint>(
(static_cast<s8>(tile_id) + 128)) *
TILE_BYTES;
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
tile_data_line_offset;
u8 pixels_1 = video_ram[line_addr];
u8 pixels_2 = video_ram[line_addr + 1];
/* Draw the remainder of this tile's row */
for(uint px = tile_pixel_x; px < TILE_WIDTH_PX && screen_x < GAMEBOY_WIDTH;
px++, screen_x++) {
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(px));
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
}
}
}
void Video::draw_window_line(uint current_line) {
bool use_tile_set_zero = bg_window_tile_data();
bool use_tile_map_zero = !window_tile_map();
Palette palette = load_palette(bg_palette);
u16 tile_set_address = use_tile_set_zero ? TILE_SET_ZERO_ADDRESS : TILE_SET_ONE_ADDRESS;
u16 tile_map_address = use_tile_map_zero ? TILE_MAP_ZERO_ADDRESS : TILE_MAP_ONE_ADDRESS;
uint screen_y = current_line;
uint scrolled_y = screen_y - window_y.value();
if(scrolled_y >= GAMEBOY_HEIGHT) {
return;
}
uint tile_y = scrolled_y / TILE_HEIGHT_PX;
uint tile_pixel_y = scrolled_y % TILE_HEIGHT_PX;
uint tile_data_line_offset = tile_pixel_y * 2;
for(uint screen_x = 0; screen_x < GAMEBOY_WIDTH; screen_x++) {
uint scrolled_x = screen_x + window_x.value() - 7;
uint tile_x = scrolled_x / TILE_WIDTH_PX;
uint tile_pixel_x = scrolled_x % TILE_WIDTH_PX;
uint tile_index = tile_y * TILES_PER_LINE + tile_x;
if(tile_index >= 32 * 32) continue;
u8 tile_id = video_ram[tile_map_address - 0x8000 + tile_index];
uint tile_data_mem_offset = use_tile_set_zero ?
tile_id * TILE_BYTES :
static_cast<uint>(
(static_cast<s8>(tile_id) + 128)) *
TILE_BYTES;
uint line_addr = (tile_set_address - 0x8000) + tile_data_mem_offset +
tile_data_line_offset;
u8 pixels_1 = video_ram[line_addr];
u8 pixels_2 = video_ram[line_addr + 1];
u8 pixel_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(tile_pixel_x));
buffer.set_pixel(screen_x, screen_y, get_shade_from_palette(pixel_color, palette));
}
}
void Video::draw_sprite(const uint sprite_n) {
/* Each sprite is represented by 4 bytes */
u16 oam_start = static_cast<u16>(sprite_n * SPRITE_BYTES);
u8 sprite_y = gb.mmu.oam_ram[oam_start];
u8 sprite_x = gb.mmu.oam_ram[oam_start + 1];
/* Offscreen sprites are not drawn */
if(sprite_y == 0 || sprite_y >= 160) {
return;
}
if(sprite_x == 0 || sprite_x >= 168) {
return;
}
uint sprite_height = sprite_size() ? 16 : 8;
u8 pattern_n = gb.mmu.oam_ram[oam_start + 2];
u8 sprite_attrs = gb.mmu.oam_ram[oam_start + 3];
/* Bits 0-3 are used only for CGB */
bool use_palette_1 = check_bit(sprite_attrs, 4);
bool flip_x = check_bit(sprite_attrs, 5);
bool flip_y = check_bit(sprite_attrs, 6);
bool obj_behind_bg = check_bit(sprite_attrs, 7);
Palette palette = use_palette_1 ? load_palette(sprite_palette_1) :
load_palette(sprite_palette_0);
uint tile_offset = pattern_n * TILE_BYTES;
int start_y = sprite_y - 16;
int start_x = sprite_x - 8;
for(uint y = 0; y < sprite_height; y++) {
int screen_y = start_y + static_cast<int>(y);
if(screen_y < 0 || screen_y >= static_cast<int>(GAMEBOY_HEIGHT)) continue;
if(row_mask && !row_mask[screen_y]) continue;
uint src_y = !flip_y ? y : sprite_height - y - 1;
uint line_addr = tile_offset + src_y * 2; /* relative to tile set zero */
u8 pixels_1 = video_ram[line_addr];
u8 pixels_2 = video_ram[line_addr + 1];
for(uint x = 0; x < TILE_WIDTH_PX; x++) {
int screen_x = start_x + static_cast<int>(x);
if(screen_x < 0 || screen_x >= static_cast<int>(GAMEBOY_WIDTH)) continue;
uint src_x = !flip_x ? x : TILE_WIDTH_PX - x - 1;
u8 gb_color = get_pixel_from_line(pixels_1, pixels_2, static_cast<u8>(src_x));
/* Color 0 is transparent */
if(gb_color == 0) {
continue;
}
Shade existing_pixel = buffer.get_pixel(
static_cast<uint>(screen_x), static_cast<uint>(screen_y));
/* Note: same behaviour as upstream - compares the final shade
* rather than the logical color 0 */
if(obj_behind_bg && existing_pixel != SHADE_WHITE) {
continue;
}
buffer.set_pixel(
static_cast<uint>(screen_x),
static_cast<uint>(screen_y),
get_shade_from_palette(gb_color, palette));
}
}
}
auto Video::get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8 {
using bitwise::bit_value;
return static_cast<u8>(
(bit_value(byte2, 7 - pixel_index) << 1) | bit_value(byte1, 7 - pixel_index));
}
auto Video::load_palette(const ByteRegister& palette_register) -> Palette {
u8 v = palette_register.value();
Palette palette;
palette.color0 = static_cast<Shade>(v & 0x3);
palette.color1 = static_cast<Shade>((v >> 2) & 0x3);
palette.color2 = static_cast<Shade>((v >> 4) & 0x3);
palette.color3 = static_cast<Shade>((v >> 6) & 0x3);
return palette;
}
auto Video::get_shade_from_palette(u8 color, const Palette& palette) -> Shade {
switch(color) {
case 0:
return palette.color0;
case 1:
return palette.color1;
case 2:
return palette.color2;
default:
return palette.color3;
}
}
void Video::draw() {
if(vblank_callback) vblank_callback(vblank_ctx);
}
Binary file not shown.
@@ -3,7 +3,7 @@
* Usage: hosttest <rom.gb> [max_frames] [--dump-frame N]
*/
#include "../gb/gameboy.h"
#include "../lib/gbcore/gameboy.h"
#include <cstdio>
#include <cstdlib>
@@ -29,9 +29,9 @@ static void serial_hook(u8 byte) {
}
}
static const u8* bank_provider(void* /*ctx*/, uint bank) {
long offset = static_cast<long>(bank) * 0x4000;
if(offset + 0x4000 > g_rom_size) offset = 0;
static const u8* bank_provider(void* /*ctx*/, uint page) {
long offset = static_cast<long>(page) * 0x2000; /* 8 KB ROM pages */
if(offset + 0x2000 > g_rom_size) offset = 0;
return g_rom + offset;
}
@@ -98,10 +98,12 @@ int main(int argc, char** argv) {
bool dump = false;
bool rowmask = false;
bool dump_audio = false;
bool skiprender = false;
for(int i = 1; i < argc; i++) {
if(!strcmp(argv[i], "--dump-frame")) dump = true;
if(!strcmp(argv[i], "--rowmask")) rowmask = true;
if(!strcmp(argv[i], "--dump-audio")) dump_audio = true;
if(!strcmp(argv[i], "--skiprender")) skiprender = true;
}
/* Same 144 -> 64 line subsampling the Flipper frontend uses */
@@ -146,6 +148,7 @@ int main(int argc, char** argv) {
auto* gb = new Gameboy(g_rom, banks, mbc, cart_ram, ram_size, bank_provider, nullptr);
gb->set_frame_callback(frame_hook, gb);
if(rowmask) gb->set_row_mask(mask);
if(skiprender) gb->set_skip_render(true);
u32 last_freq = 0;
int last_ch = -1;
@@ -1,15 +1,9 @@
#include "apu.h"
/* One frame-sequencer step every 2048 M-cycles (8192 T-cycles = 512 Hz),
* in the same M-cycle domain the CPU/PPU/timer now share. */
static const uint FRAME_SEQ_PERIOD = 2048;
void Apu::tick(uint cycles) {
if(!power) return;
seq_counter += cycles;
while(seq_counter >= FRAME_SEQ_PERIOD) {
seq_counter -= FRAME_SEQ_PERIOD;
/* Slow path of Apu::tick (see apu.h): runs the due frame-sequencer steps */
void Apu::seq_run() {
while(seq_counter >= 2048) {
seq_counter -= 2048;
seq_step = (u8)((seq_step + 1) & 7);
if((seq_step & 1) == 0) clock_lengths(); /* 256 Hz */
@@ -28,7 +28,14 @@ struct ApuVoice {
class Apu {
public:
void tick(uint cycles);
/* Called once per emulated instruction; the fast path (powered off, or
* no sequencer step due) must stay inline and branch-cheap. One
* frame-sequencer step every 2048 M-cycles = 8192 T-cycles = 512 Hz. */
void tick(uint cycles) {
if(!power) return;
seq_counter += cycles;
if(seq_counter >= 2048) seq_run();
}
/* 0xFF10 - 0xFF3F (sound registers + wave RAM) */
auto read(u16 addr) const -> u8;
@@ -81,6 +88,7 @@ private:
}
void trigger(uint n);
void seq_run();
void clock_lengths();
void clock_envelopes();
void clock_sweep();
@@ -21,6 +21,7 @@ void Cartridge::init(
bank_low = 1;
bank_high = 0;
ram_bank = 0;
cur_bank = 0xFFFFFFFFu;
update_rom_bank();
}
@@ -51,7 +52,24 @@ void Cartridge::update_rom_bank() {
if(bank_count) bank %= bank_count;
bankN = provider(provider_ctx, bank);
/* Games (Pokemon's Bankswitch routine included) frequently rewrite the
* bank register with the bank that is already mapped: skip the cache
* lookups entirely in that case. */
if(bank == cur_bank) return;
cur_bank = bank;
/* map the lo 8 KB page now; the hi page streams in lazily on first
* read from 0x6000-0x7FFF (Cartridge::read) */
bankN_lo = provider(provider_ctx, bank * 2);
bankN_hi = nullptr;
}
void Cartridge::fetch_hi_page() const {
/* Refresh the lo page's LRU stamp first so the hi fetch can never
* evict it (the provider guarantees the two most recently returned
* pages are safe when the cache has >= 2 slots). */
bankN_lo = provider(provider_ctx, cur_bank * 2);
bankN_hi = provider(provider_ctx, cur_bank * 2 + 1);
}
void Cartridge::write(u16 addr, u8 value) {
@@ -2,19 +2,23 @@
#include "definitions.h"
/* Cartridge with pluggable ROM bank provider.
/* Cartridge with pluggable ROM page provider.
*
* Instead of holding the whole ROM in RAM (impossible on Flipper Zero for
* anything above 32 KB), the cartridge asks the platform for a pointer to a
* 16 KB bank whenever the game switches banks. On the desktop test build the
* provider just returns `rom + bank * 0x4000`; on the Flipper it is backed
* by an LRU cache streaming from the SD card.
* anything above 32 KB), the cartridge asks the platform for pointers to
* 8 KB ROM pages (page n = ROM offset n * 0x2000) whenever the game
* switches banks. 8 KB granularity -- half a MBC bank -- doubles how many
* cache slots fit in the same RAM and halves the SD stall of a cache miss,
* which matters a lot for bank-switch heavy games (Pokemon switches banks
* for music/code/data every frame). On the desktop test build the provider
* just returns `rom + page * 0x2000`; on the Flipper it is backed by an
* LRU cache streaming from the SD card.
*
* Supported mappers: ROM only, MBC1 (incl. upper bits / mode select),
* MBC2 (built-in 512x4 RAM), MBC3 (no RTC), MBC5.
*/
using RomBankProvider = const u8* (*)(void* ctx, uint bank);
using RomBankProvider = const u8* (*)(void* ctx, uint page);
enum class MBCType : u8 {
None,
@@ -39,7 +43,15 @@ public:
auto read(u16 addr) const -> u8 {
if(addr < 0x4000) return bank0[addr];
if(addr < 0x8000) return bankN[addr - 0x4000];
if(addr < 0x6000) return bankN_lo[addr - 0x4000];
if(addr < 0x8000) {
/* lazy: the hi half of a bank is only streamed in when the
* game actually reads 0x6000-0x7FFF from it. Many switches
* exist just to read a table at 0x4xxx; fetching both 8 KB
* pages eagerly doubled the SD misses of streamed games. */
if(!bankN_hi) fetch_hi_page();
return bankN_hi[addr - 0x6000];
}
/* 0xA000 - 0xBFFF: cartridge RAM */
return read_ram(addr);
}
@@ -59,9 +71,12 @@ private:
auto read_ram(u16 addr) const -> u8;
void write_ram(u16 addr, u8 value);
void update_rom_bank();
void fetch_hi_page() const;
const u8* bank0 = nullptr;
const u8* bankN = nullptr;
mutable const u8* bankN_lo = nullptr; /* 0x4000 - 0x5FFF */
mutable const u8* bankN_hi = nullptr; /* 0x6000 - 0x7FFF, lazy (see read) */
uint cur_bank = 0xFFFFFFFFu; /* currently mapped bank (memo) */
u8* ram = nullptr;
u32 ram_size = 0;
@@ -29,18 +29,42 @@ void CPU::init_post_boot() {
}
auto CPU::tick() -> Cycles {
handle_interrupts();
/* STOP: the CPU core is frozen until joypad activity (notify_joypad).
* Interrupt flags raised meanwhile stay pending. */
if (stopped) { return 8; }
/* Halted: batch 4 M-cycles per iteration. Games spend most of every
/* Interrupt fast path: nothing pending for the enabled sources (the
* overwhelmingly common case) costs two loads and a branch. Bits 5-7
* of IF/IE are unwired on hardware and must be masked: without the
* mask, IF's always-set upper bits (post-boot 0xE1) against a game
* writing IE=0xFF would push PC without dispatching any vector. */
u8 fired = (u8)(interrupt_flag.value() & interrupt_enabled.value() & 0x1F);
if(fired) handle_interrupts(fired);
/* Halted: batch 8 M-cycles per iteration. Games spend most of every
* frame in HALT waiting for vblank; stepping 1 cycle at a time made
* the idle part of the frame as expensive to emulate as the busy part.
* Interrupt recognition is delayed by at most 3 M-cycles (12 T-cycles),
* Interrupt recognition is delayed by at most 7 M-cycles (28 T-cycles),
* well within what real hardware tolerates. */
if (halted) { return 4; }
if (halted) { return 8; }
/* EI enables IME only AFTER the instruction that follows it. Without
* this delay, the classic pause idiom `EI / HALT` with an interrupt
* already pending dispatched BETWEEN the two: the ISR consumed the
* wake event, RETI returned onto the HALT, and the game slept with
* its wake condition already spent -- input appeared permanently dead
* while the vblank ISR (music) kept running. */
bool ei_was_pending = ime_pending;
u16 opcode_pc = pc.value();
auto opcode = get_byte_from_pc();
auto cycles = execute_opcode(opcode, opcode_pc);
if(ei_was_pending && ime_pending) {
/* not cancelled by a DI in the delay slot */
interrupts_enabled = true;
ime_pending = false;
}
return cycles;
}
@@ -55,11 +79,8 @@ auto CPU::execute_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
return execute_normal_opcode(opcode, opcode_pc);
}
void CPU::handle_interrupts() {
u8 fired_interrupts = interrupt_flag.value() & interrupt_enabled.value();
if (!fired_interrupts) { return; }
if (halted && fired_interrupts != 0x0) {
void CPU::handle_interrupts(u8 fired_interrupts) {
if (halted) {
// TODO: Handle halt bug
halted = false;
}
@@ -99,24 +120,9 @@ auto CPU::handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_inte
return true;
}
auto CPU::get_byte_from_pc() -> u8 {
u8 byte = gb.mmu.read(Address(pc));
pc.increment();
return byte;
}
auto CPU::get_signed_byte_from_pc() -> s8 {
u8 byte = get_byte_from_pc();
return static_cast<s8>(byte);
}
auto CPU::get_word_from_pc() -> u16 {
u8 low_byte = get_byte_from_pc();
u8 high_byte = get_byte_from_pc();
return compose_bytes(high_byte, low_byte);
}
/* get_byte_from_pc / get_signed_byte_from_pc / get_word_from_pc are the
* hottest functions in the emulator (every instruction fetches through
* them): they are defined inline at the bottom of gameboy.h. */
void CPU::set_flag_zero(bool set) { f.set_flag_zero(set); }
void CPU::set_flag_subtract(bool set) { f.set_flag_subtract(set); }
@@ -223,3 +229,11 @@ auto CPU::execute_cb_opcode(const u8 opcode, u16 opcode_pc) -> Cycles {
return opcode_cycles_cb[opcode];
}
/* Unity build: the opcode implementations are compiled inside this same
* translation unit so the dispatch switches above can call (and inline)
* them directly. As separate TUs, every emulated instruction paid two
* cross-TU calls (dispatch -> opcode_XX -> opcode helper), which the
* compiler could not eliminate without LTO. */
#include "opcode_mapping.inc"
#include "opcodes.inc"
@@ -52,13 +52,27 @@ public:
ByteRegister interrupt_enabled;
private:
void handle_interrupts();
void handle_interrupts(u8 fired_interrupts);
auto handle_interrupt(u8 interrupt_bit, u16 interrupt_vector, u8 fired_interrupts) -> bool;
Gameboy& gb;
bool interrupts_enabled = false;
bool ime_pending = false; /* EI takes effect AFTER the next instruction */
bool halted = false;
bool stopped = false; /* STOP: woken only by joypad activity */
public:
/* Joypad line activity: wakes STOP unconditionally (real DMG behaviour:
* STOP exits on a joypad line change regardless of IE/IF/IME). */
void notify_joypad() {
if(stopped) {
stopped = false;
halted = false;
}
}
private:
bool branch_taken = false;
@@ -61,6 +61,15 @@ public:
* storage slot s starts at bit offset s * GAMEBOY_WIDTH * 2. */
auto raw() const -> const u8* { return buf; }
/* Direct pointer to the packed bytes of screen row y (40 bytes,
* 4 pixels each, LSB-first) or nullptr if the row has no storage.
* Used by the byte-oriented renderer in video.cc. */
auto row_ptr(uint y) -> u8* {
uint slot = row_slot[y];
if(slot == NO_ROW) return nullptr;
return buf + slot * (GAMEBOY_WIDTH / 4);
}
private:
static const u8 NO_ROW = 0xFF;
@@ -44,8 +44,10 @@ void Gameboy::vblank_trampoline(void* ctx) {
void Gameboy::button_pressed(GbButton button) {
input.button_pressed(button);
/* Request the joypad interrupt (missing upstream); mainly wakes
* games waiting in HALT/STOP for input. */
* games waiting in HALT for input. */
cpu.interrupt_flag.set_bit_to(4, true);
/* STOP wakes on joypad activity regardless of IE/IF/IME */
cpu.notify_joypad();
}
void Gameboy::button_released(GbButton button) {
@@ -53,15 +55,11 @@ void Gameboy::button_released(GbButton button) {
}
void Gameboy::run_to_vblank() {
/* Gameboy::tick and the peripheral ticks it calls are defined inline
* in gameboy.h, so this loop compiles into one flattened body with a
* single out-of-line call per instruction (cpu.tick). */
frame_done = false;
while(!frame_done) {
tick();
}
}
void Gameboy::tick() {
auto cycles = cpu.tick();
video.tick(cycles);
timer.tick(cycles.cycles);
apu.tick(cycles.cycles);
}
@@ -0,0 +1,206 @@
#pragma once
#include "input.h"
#include "cpu.h"
#include "video.h"
#include "timer.h"
#include "mmu.h"
#include "cartridge.h"
#include "apu.h"
class Gameboy {
public:
/* bank0: pointer to the first 16 KB of ROM (stays resident).
* provider: returns pointers to 16 KB switchable banks. */
Gameboy(
const u8* bank0,
uint rom_bank_count,
MBCType mbc,
u8* cart_ram,
u32 cart_ram_size,
RomBankProvider provider,
void* provider_ctx);
/* Runs the emulator until the next vblank (one full frame). */
void run_to_vblank();
/* Called on every vblank BEFORE the framebuffer is cleared for the next
* frame: this is where the frontend must convert/copy the image. */
void set_frame_callback(void (*cb)(void*), void* ctx) {
user_frame_cb = cb;
user_frame_ctx = ctx;
}
void button_pressed(GbButton button);
void button_released(GbButton button);
void set_skip_render(bool skip) { video.skip_render = skip; }
/* Flush pending peripheral cycles. Called automatically every
* PERIPH_BATCH M-cycles and before any IO register read/write so
* LY/STAT/DIV/TIMA/IF are always fresh where the game can see them. */
void sync_peripherals() {
uint n = pending_cycles;
if(n) {
pending_cycles = 0;
video.tick(Cycles(n));
timer.tick(n);
apu.tick(n);
}
}
/* Optional display-line mask (see Video::row_mask). The array must stay
* valid for the lifetime of the emulator. */
void set_row_mask(const u8* mask) { video.set_row_mask(mask); }
auto get_framebuffer() const -> const FrameBuffer& { return video.get_framebuffer(); }
auto get_cartridge_ram() -> u8* { return cartridge.get_ram(); }
auto get_cartridge_ram_size() const -> u32 { return cartridge.get_ram_size(); }
Cartridge cartridge;
CPU cpu;
friend class CPU;
Video video;
friend class Video;
MMU mmu;
friend class MMU;
Timer timer;
friend class Timer;
Apu apu;
Input input;
private:
void tick();
/* Peripheral catch-up batching (the single biggest hot-loop win on the
* Cortex-M4): instead of stepping the PPU/timer/APU state machines
* after every CPU instruction, cycles accumulate and are flushed every
* PERIPH_BATCH M-cycles -- or immediately whenever the CPU touches an
* IO register. Interrupt delivery is delayed by at most PERIPH_BATCH
* M-cycles (128 T-cycles), far below what games can observe, while IO
* reads always see exact values thanks to the sync points. */
static const uint PERIPH_BATCH = 32;
uint pending_cycles = 0;
static void vblank_trampoline(void* ctx);
volatile bool frame_done = false;
void (*user_frame_cb)(void*) = nullptr;
void* user_frame_ctx = nullptr;
};
/* ==================== hot-path inline implementations ====================
*
* Everything below runs once (or more) per emulated instruction. These
* bodies need the complete Gameboy definition, and they are defined here
* -- instead of in their .cc files -- so the core can inline them without
* LTO. Before this, each emulated instruction paid 6-8 cross-TU function
* calls (opcode fetch through the MMU, PPU/timer/APU ticks), which
* dominated the frame time on the Cortex-M4.
*
* Deliberately NOT inlined: the full MMU read/write dispatch. Inlining it
* into the ~300 opcode call sites costs ~15 KB of code (= RAM on the
* Flipper, where the binary loads into the same heap as the ROM cache).
* Only the instruction-fetch fast path (PC in ROM, true for virtually
* every instruction) is inlined below. */
inline void Video::tick(Cycles cycles) {
cycle_counter += cycles.cycles;
/* Fast path: no mode boundary crossed. Boundary work (interrupts,
* scanline rendering) is out-of-line in video.cc. The counter wrap
* uses subtraction instead of the old modulo: per-instruction cycle
* increments (<= 6) can never overshoot a whole extra period. */
switch(current_mode) {
case VideoMode::ACCESS_OAM:
if(cycle_counter >= CLOCKS_PER_SCANLINE_OAM) {
cycle_counter -= CLOCKS_PER_SCANLINE_OAM;
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, true);
current_mode = VideoMode::ACCESS_VRAM;
}
break;
case VideoMode::ACCESS_VRAM:
if(cycle_counter >= CLOCKS_PER_SCANLINE_VRAM) {
cycle_counter -= CLOCKS_PER_SCANLINE_VRAM;
mode_transition_vram_end();
}
break;
case VideoMode::HBLANK:
if(cycle_counter >= CLOCKS_PER_HBLANK) {
cycle_counter -= CLOCKS_PER_HBLANK;
mode_transition_hblank_end();
}
break;
case VideoMode::VBLANK:
if(cycle_counter >= CLOCKS_PER_SCANLINE) {
cycle_counter -= CLOCKS_PER_SCANLINE;
mode_transition_vblank_line();
}
break;
}
}
inline void Timer::tick(uint cycles) {
/* DIV increments at 16384 Hz = every 64 M-cycles */
div_clocks += cycles;
if(div_clocks >= 64) {
divider.set(static_cast<u8>(divider.value() + (div_clocks >> 6)));
div_clocks &= 63;
}
/* Accumulate T-cycles only while the timer is enabled: accumulating
* with TAC off built up an unbounded backlog that, when a game later
* enabled the timer, drained as a burst of TIMA overflows + spurious
* timer interrupts (plus a long stall in the loop below). */
if(!timer_control.check_bit(2)) {
clocks = 0;
return;
}
clocks += cycles * 4; /* M-cycles -> T-cycles */
uint clock_limit = clocks_needed_to_increment();
/* a long instruction can cross more than one period of the fastest
* (16 T-cycle) timer rate; the old modulo silently dropped those */
while(clocks >= clock_limit) {
clocks -= clock_limit;
u8 old_timer_counter = timer_counter.value();
timer_counter.increment();
if(timer_counter.value() < old_timer_counter) {
gb.cpu.interrupt_flag.set_bit_to(2, true);
timer_counter.set(timer_modulo.value());
}
}
}
inline auto CPU::get_byte_from_pc() -> u8 {
u16 a = pc.value();
pc.increment();
/* PC sits in cartridge ROM for virtually every instruction: read the
* mapped page directly and skip the full MMU dispatch */
if(a < 0x8000) return gb.cartridge.read(a);
return gb.mmu.read(Address(a));
}
inline auto CPU::get_signed_byte_from_pc() -> s8 {
return static_cast<s8>(get_byte_from_pc());
}
inline auto CPU::get_word_from_pc() -> u16 {
u8 low_byte = get_byte_from_pc();
u8 high_byte = get_byte_from_pc();
return static_cast<u16>((high_byte << 8) | low_byte);
}
inline void Gameboy::tick() {
pending_cycles += cpu.tick().cycles;
if(pending_cycles >= PERIPH_BATCH) sync_peripherals();
}
@@ -50,5 +50,6 @@ auto Input::get_input() const -> u8 {
buttons = set_bit_to(buttons, 4, !direction_switch);
buttons = set_bit_to(buttons, 5, !button_switch);
return buttons;
/* bits 6-7 are unwired and read as 1 on hardware */
return static_cast<u8>(buttons | 0xC0);
}
@@ -94,6 +94,9 @@ void MMU::write(const Address& address, u8 byte) {
}
auto MMU::read_io(const Address& address) const -> u8 {
/* catch-up: make LY/STAT/DIV/TIMA/IF/NRxx exact before the game reads */
gb.sync_peripherals();
u16 a = address.value();
/* Sound registers + wave RAM */
@@ -167,6 +170,9 @@ auto MMU::read_io(const Address& address) const -> u8 {
}
void MMU::write_io(const Address& address, u8 byte) {
/* catch-up: apply pending time before the write changes IO state */
gb.sync_peripherals();
u16 a = address.value();
/* Sound registers + wave RAM */
@@ -3,6 +3,13 @@
#include <array>
/* NOTE: undefined opcodes (0xD3, 0xDB, 0xDD, 0xE3, 0xE4, 0xEB, 0xEC, 0xED,
* 0xF4, 0xFC, 0xFD) and the 0xCB prefix placeholder cost 1 M-cycle here,
* NOT 0. With 0-cycle entries, executing garbage (bad jump, corrupted
* read) advanced the PC without advancing the PPU/timer clocks, so
* run_to_vblank() could spin without ever finishing a frame: on the
* Flipper that froze the whole device until a hard reboot. */
const std::array<u8, 256> opcode_cycles = {
1, 3, 2, 2, 1, 1, 2, 1, 5, 2, 2, 2, 1, 1, 2, 1,
1, 3, 2, 2, 1, 1, 2, 1, 3, 2, 2, 2, 1, 1, 2, 1,
@@ -16,10 +23,10 @@ const std::array<u8, 256> opcode_cycles = {
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 0, 3, 6, 2, 4,
2, 3, 3, 0, 3, 4, 2, 4, 2, 4, 3, 0, 3, 0, 2, 4,
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
2, 3, 3, 4, 3, 4, 2, 4, 2, 4, 3, 1, 3, 6, 2, 4,
2, 3, 3, 1, 3, 4, 2, 4, 2, 4, 3, 1, 3, 1, 2, 4,
3, 3, 2, 1, 1, 4, 2, 4, 4, 1, 4, 1, 1, 1, 2, 4,
3, 3, 2, 1, 1, 4, 2, 4, 3, 2, 4, 1, 1, 1, 2, 4
};
const std::array<u8, 256> opcode_cycles_branched = {
@@ -35,10 +42,10 @@ const std::array<u8, 256> opcode_cycles_branched = {
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 0, 6, 6, 2, 4,
5, 3, 4, 0, 6, 4, 2, 4, 5, 4, 4, 0, 6, 0, 2, 4,
3, 3, 2, 0, 0, 4, 2, 4, 4, 1, 4, 0, 0, 0, 2, 4,
3, 3, 2, 1, 0, 4, 2, 4, 3, 2, 4, 1, 0, 0, 2, 4
5, 3, 4, 4, 6, 4, 2, 4, 5, 4, 4, 1, 6, 6, 2, 4,
5, 3, 4, 1, 6, 4, 2, 4, 5, 4, 4, 1, 6, 1, 2, 4,
3, 3, 2, 1, 1, 4, 2, 4, 4, 1, 4, 1, 1, 1, 2, 4,
3, 3, 2, 1, 1, 4, 2, 4, 3, 2, 4, 1, 1, 1, 2, 4
};
const std::array<u8, 256> opcode_cycles_cb = {
@@ -1,4 +1,5 @@
#include "cpu.h"
#include "gameboy.h" /* inline hot-path definitions (operand fetch) */
/* clang-format off */
/**
@@ -262,12 +262,13 @@ void CPU::opcode_dec(Address&& addr) {
/* DI */
void CPU::opcode_di() {
interrupts_enabled = false;
ime_pending = false; /* DI in the EI delay slot cancels the enable */
}
/* EI */
/* EI: IME is enabled after the NEXT instruction (see CPU::tick) */
void CPU::opcode_ei() {
interrupts_enabled = true;
ime_pending = true;
}
@@ -543,10 +544,10 @@ void CPU::opcode_ret(Condition condition) {
}
/* RETI */
/* RETI: unlike EI, re-enables IME immediately (no delay slot) */
void CPU::opcode_reti() {
opcode_ret();
opcode_ei();
interrupts_enabled = true;
}
@@ -810,9 +811,17 @@ void CPU::opcode_srl(Address&& addr) {
}
/* STOP */
/* STOP: on real DMG the CPU freezes until a joypad LINE CHANGE -- this is
* independent of IE/IF/IME (it does not go through interrupt dispatch).
* Modelling it as plain HALT was wrong twice over: with IE bit4 clear a
* press never satisfied IF&IE so the game froze forever, and with IE
* bit0 set our free-running PPU vblank woke it instantly. The `stopped`
* flag is cleared only by CPU::notify_joypad() (wired to button presses).
* STOP is also a 2-byte instruction: consume the padding byte. */
void CPU::opcode_stop() {
/* halted = true; */
get_byte_from_pc(); /* skip the 0x00 pad */
halted = true;
stopped = true;
}
@@ -0,0 +1,48 @@
#pragma once
#include "definitions.h"
#include "register.h"
class Gameboy;
class Timer {
public:
Timer(Gameboy& inGb)
: gb(inGb) {}
/* Called once per emulated instruction: defined inline at the bottom
* of gameboy.h (needs the complete Gameboy type). */
void tick(uint cycles);
auto get_divider() const -> u8 { return divider.value(); }
auto get_timer() const -> u8 { return timer_counter.value(); }
auto get_timer_modulo() const -> u8 { return timer_modulo.value(); }
/* Only the bottom three bits of this register are usable */
auto get_timer_control() const -> u8 { return timer_control.value() & 0x7; }
void reset_divider() { divider.set(0x0); }
void set_timer(u8 value) { timer_counter.set(value); }
void set_timer_modulo(u8 value) { timer_modulo.set(value); }
void set_timer_control(u8 value) { timer_control.set(value); }
private:
auto clocks_needed_to_increment() const -> uint {
switch(get_timer_control() & 0x3) {
case 0: return CLOCK_RATE / 4096;
case 1: return CLOCK_RATE / 262144;
case 2: return CLOCK_RATE / 65536;
default: return CLOCK_RATE / 16384;
}
}
uint clocks = 0;
uint div_clocks = 0;
Gameboy& gb;
ByteRegister divider;
ByteRegister timer_counter;
ByteRegister timer_modulo;
ByteRegister timer_control;
};
@@ -0,0 +1,398 @@
#include "video.h"
#include "gameboy.h"
#include "cpu.h"
#include "bitwise.h"
using bitwise::check_bit;
/* ---------------------------------------------------------- tile decode */
/* s_tile_lut[b] spreads the 8 bits of a tile-data byte into 8 2-bit pixel
* fields, leftmost pixel (bit 7) in the LOWEST field -- the same LSB-first
* packing the framebuffer uses, so whole bytes can be emitted directly.
* s_tile_lut_rev is the X-flipped variant for sprites. 1 KB total. */
static u16 s_tile_lut[256];
static u16 s_tile_lut_rev[256];
static bool s_luts_ready = false;
Video::Video(Gameboy& inGb)
: gb(inGb) {
if(!s_luts_ready) {
for(uint b = 0; b < 256; b++) {
u16 fwd = 0, rev = 0;
for(uint k = 0; k < 8; k++) {
u16 bit = (u16)((b >> (7 - k)) & 1);
fwd |= (u16)(bit << (2 * k));
rev |= (u16)(bit << (2 * (7 - k)));
}
s_tile_lut[b] = fwd;
s_tile_lut_rev[b] = rev;
}
s_luts_ready = true;
}
}
/* Palette LUT: maps a packed 2bpp byte (4 pixels) through BGP in one
* lookup. Rebuilt only when the game writes a new BGP value. */
auto Video::bg_pal_lut() -> const u8* {
u8 bgp = bg_palette.value();
if(!bgp_lut_valid || bgp != bgp_lut_cached_for) {
u8 shade[4] = {
(u8)(bgp & 0x3),
(u8)((bgp >> 2) & 0x3),
(u8)((bgp >> 4) & 0x3),
(u8)((bgp >> 6) & 0x3),
};
for(uint v = 0; v < 256; v++) {
bgp_lut[v] = (u8)(
shade[v & 3] | (shade[(v >> 2) & 3] << 2) | (shade[(v >> 4) & 3] << 4) |
(shade[(v >> 6) & 3] << 6));
}
bgp_lut_cached_for = bgp;
bgp_lut_valid = true;
}
return bgp_lut;
}
/* Video::tick is called once per emulated instruction; it lives as an
* inline definition at the bottom of gameboy.h. Mode-transition work
* (rendering, interrupts) stays out-of-line in this file. */
void Video::mode_transition_vram_end() {
current_mode = VideoMode::HBLANK;
bool hblank_interrupt = check_bit(lcd_status.value(), 3);
if(hblank_interrupt) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
bool ly_coincidence_interrupt = check_bit(lcd_status.value(), 6);
bool ly_coincidence = ly_compare.value() == line.value();
if(ly_coincidence_interrupt && ly_coincidence) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
lcd_status.set_bit_to(2, ly_coincidence);
lcd_status.set_bit_to(1, false);
lcd_status.set_bit_to(0, false);
}
void Video::mode_transition_hblank_end() {
if(!skip_render) write_scanline(line.value());
line.increment();
/* Line 145 (index 144) is the first line of VBLANK */
if(line == 144) {
current_mode = VideoMode::VBLANK;
lcd_status.set_bit_to(1, false);
lcd_status.set_bit_to(0, true);
gb.cpu.interrupt_flag.set_bit_to(0, true);
} else {
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, false);
current_mode = VideoMode::ACCESS_OAM;
}
}
void Video::mode_transition_vblank_line() {
line.increment();
/* LY=LYC STAT interrupt must also fire for lines 144-153: games that
* wait for a coincidence inside vblank hung without this (it was only
* evaluated at the mode3->hblank transition of visible lines) */
if(check_bit(lcd_status.value(), 6) && ly_compare.value() == line.value()) {
gb.cpu.interrupt_flag.set_bit_to(1, true);
}
lcd_status.set_bit_to(2, ly_compare.value() == line.value());
/* Line 155 (index 154) is the last line */
if(line == 154) {
if(!skip_render) {
write_sprites();
draw();
buffer.reset();
} else {
draw(); /* still notify the frontend for pacing */
}
line.reset();
current_mode = VideoMode::ACCESS_OAM;
lcd_status.set_bit_to(1, true);
lcd_status.set_bit_to(0, false);
}
}
auto Video::display_enabled() const -> bool {
return check_bit(control_byte, 7);
}
auto Video::window_tile_map() const -> bool {
return check_bit(control_byte, 6);
}
auto Video::window_enabled() const -> bool {
return check_bit(control_byte, 5);
}
auto Video::bg_window_tile_data() const -> bool {
return check_bit(control_byte, 4);
}
auto Video::bg_tile_map_display() const -> bool {
return check_bit(control_byte, 3);
}
auto Video::sprite_size() const -> bool {
return check_bit(control_byte, 2);
}
auto Video::sprites_enabled() const -> bool {
return check_bit(control_byte, 1);
}
auto Video::bg_enabled() const -> bool {
return check_bit(control_byte, 0);
}
void Video::write_scanline(u8 current_line) {
if(!display_enabled()) {
return;
}
/* Lines the frontend never displays are not worth rendering */
if(row_mask && current_line < GAMEBOY_HEIGHT && !row_mask[current_line]) {
return;
}
if(bg_enabled()) {
draw_bg_line(current_line);
}
if(window_enabled()) {
draw_window_line(current_line);
}
}
void Video::write_sprites() {
if(!sprites_enabled()) {
return;
}
for(uint sprite_n = 0; sprite_n < 40; sprite_n++) {
draw_sprite(sprite_n);
}
}
/* Byte-oriented tile-strip renderer: emits `count` pixels of one tile-map
* row into the packed framebuffer row `dst_row`, starting at screen pixel
* `dst_x`, sourcing map pixels from `src_px` (wraps at 256). Pixels flow
* through a small bit-queue so the bulk of the line is written as whole
* palette-mapped bytes (4 px per store) instead of per-pixel RMW packing:
* ~7x fewer operations per line than the old per-pixel path. */
void Video::render_strip(
u8* dst_row,
uint dst_x,
uint count,
uint map_row_base, /* VRAM offset of the tile map row (32 entries) */
uint src_px,
uint tile_line_off, /* 2 * (row within the tile) */
bool use_tile_set_zero,
const u8* pal_lut) {
uint tile_x = (src_px >> 3) & 31;
uint fine = src_px & 7;
uint tset_base = use_tile_set_zero ? 0x0000u : 0x0800u;
/* bit-queue of pending 2bpp pixels, LSB = next pixel to emit */
u32 acc = 0;
uint nbits = 0;
#define FETCH_TILE_ROW() \
do { \
u8 tid = video_ram[map_row_base + tile_x]; \
tile_x = (tile_x + 1) & 31; \
uint toff = use_tile_set_zero ? \
(uint)tid * TILE_BYTES : \
(uint)((s8)tid + 128) * TILE_BYTES; \
uint la = tset_base + toff + tile_line_off; \
u16 v = (u16)(s_tile_lut[video_ram[la]] | (s_tile_lut[video_ram[la + 1]] << 1)); \
acc |= (u32)v << nbits; \
nbits += 16; \
} while(0)
/* prime the queue, discarding the fine-scroll pixels */
FETCH_TILE_ROW();
acc >>= fine * 2;
nbits -= fine * 2;
/* head: single pixels until the destination is byte-aligned */
while(count && (dst_x & 3)) {
if(nbits < 2) FETCH_TILE_ROW();
uint sh = (dst_x & 3) * 2;
u8* p = dst_row + (dst_x >> 2);
*p = (u8)((*p & ~(3u << sh)) | ((pal_lut[acc & 3] & 3u) << sh));
acc >>= 2;
nbits -= 2;
dst_x++;
count--;
}
/* body: whole bytes, palette applied 4 pixels at a time */
u8* out = dst_row + (dst_x >> 2);
while(count >= 4) {
if(nbits < 8) FETCH_TILE_ROW();
*out++ = pal_lut[acc & 0xFF];
acc >>= 8;
nbits -= 8;
count -= 4;
dst_x += 4;
}
/* tail */
while(count) {
if(nbits < 2) FETCH_TILE_ROW();
uint sh = (dst_x & 3) * 2;
u8* p = dst_row + (dst_x >> 2);
*p = (u8)((*p & ~(3u << sh)) | ((pal_lut[acc & 3] & 3u) << sh));
acc >>= 2;
nbits -= 2;
dst_x++;
count--;
}
#undef FETCH_TILE_ROW
}
void Video::draw_bg_line(uint current_line) {
/* Note: tileset two uses signed numbering to share half the tiles with
* tileset one */
bool use_tile_set_zero = bg_window_tile_data();
bool use_tile_map_zero = !bg_tile_map_display();
u8* dst = buffer.row_ptr(current_line);
if(!dst) return;
uint scrolled_y = (current_line + scroll_y.value()) % BG_MAP_SIZE;
uint map_row_base = (use_tile_map_zero ? (TILE_MAP_ZERO_ADDRESS - 0x8000) :
(TILE_MAP_ONE_ADDRESS - 0x8000)) +
(scrolled_y / TILE_HEIGHT_PX) * TILES_PER_LINE;
render_strip(
dst,
0,
GAMEBOY_WIDTH,
map_row_base,
scroll_x.value(),
(scrolled_y % TILE_HEIGHT_PX) * 2,
use_tile_set_zero,
bg_pal_lut());
}
void Video::draw_window_line(uint current_line) {
bool use_tile_set_zero = bg_window_tile_data();
bool use_tile_map_zero = !window_tile_map();
uint scrolled_y = current_line - window_y.value();
if(scrolled_y >= GAMEBOY_HEIGHT) {
return;
}
/* the window covers screen pixels from WX-7 onward, sourcing the
* window map from column 0 */
uint wx = window_x.value();
uint start_x = wx >= 7 ? wx - 7 : 0;
if(start_x >= GAMEBOY_WIDTH) return;
u8* dst = buffer.row_ptr(current_line);
if(!dst) return;
uint map_row_base = (use_tile_map_zero ? (TILE_MAP_ZERO_ADDRESS - 0x8000) :
(TILE_MAP_ONE_ADDRESS - 0x8000)) +
(scrolled_y / TILE_HEIGHT_PX) * TILES_PER_LINE;
render_strip(
dst,
start_x,
GAMEBOY_WIDTH - start_x,
map_row_base,
wx >= 7 ? 0 : 7 - wx,
(scrolled_y % TILE_HEIGHT_PX) * 2,
use_tile_set_zero,
bg_pal_lut());
}
void Video::draw_sprite(const uint sprite_n) {
/* Each sprite is represented by 4 bytes */
u16 oam_start = static_cast<u16>(sprite_n * SPRITE_BYTES);
u8 sprite_y = gb.mmu.oam_ram[oam_start];
u8 sprite_x = gb.mmu.oam_ram[oam_start + 1];
/* Offscreen sprites are not drawn */
if(sprite_y == 0 || sprite_y >= 160) {
return;
}
if(sprite_x == 0 || sprite_x >= 168) {
return;
}
uint sprite_height = sprite_size() ? 16 : 8;
u8 pattern_n = gb.mmu.oam_ram[oam_start + 2];
u8 sprite_attrs = gb.mmu.oam_ram[oam_start + 3];
/* Bits 0-3 are used only for CGB */
bool use_palette_1 = check_bit(sprite_attrs, 4);
bool flip_x = check_bit(sprite_attrs, 5);
bool flip_y = check_bit(sprite_attrs, 6);
bool obj_behind_bg = check_bit(sprite_attrs, 7);
/* 4-entry shade table instead of a per-pixel palette switch */
u8 obp = use_palette_1 ? sprite_palette_1.value() : sprite_palette_0.value();
u8 shade[4] = {
(u8)(obp & 0x3),
(u8)((obp >> 2) & 0x3),
(u8)((obp >> 4) & 0x3),
(u8)((obp >> 6) & 0x3),
};
uint tile_offset = pattern_n * TILE_BYTES;
int start_y = sprite_y - 16;
int start_x = sprite_x - 8;
const u16* lut = flip_x ? s_tile_lut_rev : s_tile_lut;
for(uint y = 0; y < sprite_height; y++) {
int screen_y = start_y + static_cast<int>(y);
if(screen_y < 0 || screen_y >= static_cast<int>(GAMEBOY_HEIGHT)) continue;
if(row_mask && !row_mask[screen_y]) continue;
u8* row = buffer.row_ptr(static_cast<uint>(screen_y));
if(!row) continue;
uint src_y = !flip_y ? y : sprite_height - y - 1;
uint line_addr = tile_offset + src_y * 2; /* relative to tile set zero */
/* all 8 pixel colors of this sprite row, LSB-first left-to-right
* (flip handled by the reversed LUT) */
u16 v = (u16)(lut[video_ram[line_addr]] | (lut[video_ram[line_addr + 1]] << 1));
if(v == 0) continue; /* fully transparent row: common, skip */
for(uint x = 0; x < TILE_WIDTH_PX; x++, v >>= 2) {
uint color = v & 3;
if(color == 0) continue; /* color 0 is transparent */
int screen_x = start_x + static_cast<int>(x);
if(screen_x < 0 || screen_x >= static_cast<int>(GAMEBOY_WIDTH)) continue;
uint sh = ((uint)screen_x & 3) * 2;
u8* p = row + ((uint)screen_x >> 2);
/* Note: same behaviour as upstream - the priority bit compares
* the final shade rather than the logical color 0 */
if(obj_behind_bg && ((*p >> sh) & 3) != SHADE_WHITE) {
continue;
}
*p = (u8)((*p & ~(3u << sh)) | (shade[color] << sh));
}
}
}
void Video::draw() {
if(vblank_callback) vblank_callback(vblank_ctx);
}
@@ -72,10 +72,22 @@ private:
void write_scanline(u8 current_line);
void write_sprites();
void draw();
void mode_transition_vram_end();
void mode_transition_hblank_end();
void mode_transition_vblank_line();
void draw_bg_line(uint current_line);
void draw_window_line(uint current_line);
void draw_sprite(uint sprite_n);
static auto get_pixel_from_line(u8 byte1, u8 byte2, u8 pixel_index) -> u8;
void render_strip(
u8* dst_row,
uint dst_x,
uint count,
uint map_row_base,
uint src_px,
uint tile_line_off,
bool use_tile_set_zero,
const u8* pal_lut);
auto bg_pal_lut() -> const u8*;
static auto is_on_screen(int x, int y) -> bool {
return x >= 0 && y >= 0 && x < static_cast<int>(GAMEBOY_WIDTH) &&
@@ -91,13 +103,15 @@ private:
auto sprites_enabled() const -> bool;
auto bg_enabled() const -> bool;
static auto load_palette(const ByteRegister& palette_register) -> Palette;
static auto get_shade_from_palette(u8 color, const Palette& palette) -> Shade;
Gameboy& gb;
FrameBuffer buffer;
/* packed-byte -> palette-mapped-byte LUT for the current BGP value */
u8 bgp_lut[256];
u8 bgp_lut_cached_for = 0;
bool bgp_lut_valid = false;
u8 video_ram[0x2000] = {}; /* DMG: 8 KB (was 16 KB upstream) */
VideoMode current_mode = VideoMode::ACCESS_OAM;
+207 -105
View File
@@ -21,14 +21,15 @@
#include <string.h>
#include <new>
#include "gb/gameboy.h"
#include "lib/gbcore/gameboy.h"
#define SCREEN_W 128
#define SCREEN_H 64
#define BUFFER_SIZE (SCREEN_W * SCREEN_H / 8)
#define GB_FRAME_US 16742 /* 59.73 Hz */
#define BANK_SIZE 0x4000u
#define BANK_SIZE 0x4000u /* MBC bank: 16 KB (bank 0 stays resident whole) */
#define PAGE_SIZE 0x2000u /* cache granularity: 8 KB = half a bank */
/* Heap kept free for the GUI/system while playing. 10 KB is enough for the
* direct-draw takeover + input subscription + background services; the old
* 24 KB reserve was more than one whole ROM bank of wasted headroom and
@@ -58,28 +59,32 @@ enum {
KBIT_B = 1 << 5,
};
/* ROM bank cache.
/* ROM page cache.
*
* Every switchable bank lives in its own 16 KB heap block: there is no
* "whole ROM in one contiguous malloc" fast path any more, because a large
* contiguous allocation is exactly what fails (and used to crash the
* firmware) on a fragmented heap. Instead:
* Every 8 KB ROM page lives in its own heap block: there is no "whole ROM
* in one contiguous malloc" fast path, because a large contiguous
* allocation is exactly what fails (and used to crash the firmware) on a
* fragmented heap. 8 KB granularity (half a MBC bank) doubles the number
* of cache slots per KB of heap and halves the SD stall of a miss, which
* is what makes bank-switch heavy games (Pokemon) playable while
* streaming.
*
* - as many 16 KB slots as the heap safely affords are allocated up front;
* - if every switchable bank got a slot, the ROM is fully resident: bank
* - as many 8 KB slots as the heap safely affords are allocated up front;
* - if every switchable page got a slot, the ROM is fully resident: page
* lookup is a direct O(1) index and the SD file is closed;
* - otherwise the slots form an LRU cache streaming banks from SD.
* - otherwise the slots form an LRU cache streaming pages from SD.
*/
typedef struct {
File* file; /* NULL once the ROM is fully resident */
u8* bank0;
u8** slots; /* num_slots pointers to 16 KB blocks */
u16* slot_bank; /* which bank each slot holds (0 = empty) */
u8** slots; /* num_slots pointers to 8 KB blocks */
u16* slot_page; /* which ROM page each slot holds (0 = empty) */
u32* slot_use; /* LRU stamps (streaming mode only) */
u16* slot_hits; /* hit counters: the hottest slot is eviction-protected */
u32 use_counter;
u16 num_slots;
u16 banks;
bool fully_loaded; /* slots[i] permanently holds bank i+1 */
bool fully_loaded; /* slots[i] permanently holds page i+2 */
} RomCache;
typedef struct {
@@ -90,21 +95,24 @@ typedef struct {
FuriMutex* fb_mutex;
volatile uint8_t keys; /* KBIT_* currently pressed */
uint8_t keys_blocked; /* held keys suppressed until physical release */
volatile bool menu_requested;
volatile bool menu_active;
volatile bool exit_requested;
Gameboy* gb;
void* gb_mem; /* raw block for the Gameboy, reserved during rom_load */
RomCache rom;
u8* cart_ram;
u32 cart_ram_size;
bool has_battery;
/* sound (single-tone piezo fed with the dominant APU voice) */
bool sound_enabled;
uint8_t volume_setting; /* 0 = off, 1..4 = 25/50/75/100% */
bool speaker_acquired;
uint32_t tone_freq; /* currently playing tone, 0 = silent */
uint8_t tone_vol; /* 0..15, scaled by master volume */
uint8_t tone_setting; /* volume_setting the current tone was started with */
/* menu */
int menu_cursor;
@@ -138,39 +146,66 @@ extern "C" [[noreturn]] void gb_fatal(const char* msg) {
/* ------------------------------------------------------- rom bank provider */
static const u8* rom_bank_provider(void* ctx, uint bank) {
static const u8* rom_bank_provider(void* ctx, uint page) {
RomCache* rc = (RomCache*)ctx;
if(bank == 0) return rc->bank0;
/* pages 0 and 1 are the two halves of the resident bank 0 */
if(page < 2) return rc->bank0 + page * PAGE_SIZE;
if(rc->fully_loaded) {
return rc->slots[bank - 1]; /* O(1), the whole ROM is resident */
return rc->slots[page - 2]; /* O(1), the whole ROM is resident */
}
/* cache lookup */
u16 lru = 0;
/* cache lookup. Eviction is LRU with two protections:
* - never evict the MRU slot: the cartridge's lo page is always the
* MRU when its (lazy) hi page faults in, so the lo pointer can
* never be left dangling;
* - keep the single hottest slot (most hits, e.g. the page holding
* the music driver that is switched in every frame): pure LRU on
* Pokemon's cyclic per-frame bank pattern degenerated to a ~0%
* hit rate. Hit counters decay periodically so the protection
* adapts when the hot page changes. */
u16 hottest = 0;
u16 mru = 0;
for(u16 i = 1; i < rc->num_slots; i++) {
if(rc->slot_hits[i] > rc->slot_hits[hottest]) hottest = i;
if(rc->slot_use[i] > rc->slot_use[mru]) mru = i;
}
u16 lru = 0xFFFF;
u32 lru_use = 0xFFFFFFFFu;
for(u16 i = 0; i < rc->num_slots; i++) {
if(rc->slot_bank[i] == bank) {
if(rc->slot_page[i] == page) {
rc->slot_use[i] = ++rc->use_counter;
if(rc->slot_hits[i] < 0xFFFF) rc->slot_hits[i]++;
if((rc->use_counter & 63) == 0) {
for(u16 j = 0; j < rc->num_slots; j++)
rc->slot_hits[j] >>= 1;
}
return rc->slots[i];
}
if(rc->slot_use[i] < lru_use) {
if(i != hottest && i != mru && rc->slot_use[i] < lru_use) {
lru_use = rc->slot_use[i];
lru = i;
}
}
/* miss: stream the bank from SD into the LRU slot */
storage_file_seek(rc->file, (u32)bank * BANK_SIZE, true);
size_t got = storage_file_read(rc->file, rc->slots[lru], BANK_SIZE);
if(got < BANK_SIZE) {
/* short read (SD hiccup / truncated ROM): open-bus instead of
* stale data from whatever bank lived here before */
memset(rc->slots[lru] + got, 0xFF, BANK_SIZE - got);
if(lru == 0xFFFF) {
/* every other slot excluded (2-slot cache): sacrifice the hottest;
* it is never the MRU in this branch, so the lo page stays safe */
lru = (hottest != mru) ? hottest : (u16)(mru == 0 ? 1 : 0);
}
rc->slot_bank[lru] = (u16)bank;
/* miss: stream the page from SD into the LRU slot */
storage_file_seek(rc->file, (u32)page * PAGE_SIZE, true);
size_t got = storage_file_read(rc->file, rc->slots[lru], PAGE_SIZE);
if(got < PAGE_SIZE) {
/* short read (SD hiccup / truncated ROM): open-bus instead of
* stale data from whatever page lived here before */
memset(rc->slots[lru] + got, 0xFF, PAGE_SIZE - got);
}
rc->slot_page[lru] = (u16)page;
rc->slot_use[lru] = ++rc->use_counter;
rc->slot_hits[lru] = 1;
return rc->slots[lru];
}
@@ -185,9 +220,11 @@ static uint8_t s_ymap[SCREEN_H];
* rendering work per frame). */
static u8 s_rowmask[GAMEBOY_HEIGHT];
/* light/dark decision per (y-parity, x-parity, shade): 2x2 ordered dither.
* white -> lit, light gray -> 3/4 lit, dark gray -> 1/4 lit, black -> off */
static uint8_t s_dither[2][2][4];
/* Dither LUT: maps one packed 2bpp framebuffer byte (4 consecutive GB
* pixels) to 4 light/dark bits (bit j = pixel j), per destination row
* parity. 2x2 ordered dither: white -> lit, light gray -> 3/4 lit,
* dark gray -> 1/4 lit, black -> off. */
static uint8_t s_dither4[2][256];
static void init_scale_maps(void) {
for(int x = 0; x < SCREEN_W; x++)
@@ -200,17 +237,32 @@ static void init_scale_maps(void) {
s_rowmask[s_ymap[y]] = 1;
for(int py = 0; py < 2; py++) {
for(int px = 0; px < 2; px++) {
s_dither[py][px][0] = 1;
s_dither[py][px][1] = (px == 1 && py == 1) ? 0 : 1;
s_dither[py][px][2] = (px == 0 && py == 0) ? 1 : 0;
s_dither[py][px][3] = 0;
for(int v = 0; v < 256; v++) {
uint8_t bits = 0;
for(int j = 0; j < 4; j++) {
int s = (v >> (j * 2)) & 3;
int px = j & 1;
bool light;
switch(s) {
case 0: light = true; break;
case 1: light = !(px == 1 && py == 1); break;
case 2: light = (px == 0 && py == 0); break;
default: light = false; break;
}
if(light) bits |= (uint8_t)(1 << j);
}
s_dither4[py][v] = bits;
}
}
}
/* Called by the core on every vblank, before the framebuffer is cleared.
* Converts 4 shades -> 1 bit with a 2x2 ordered dither. */
* Converts 4 shades -> 1 bit with a 2x2 ordered dither.
*
* Byte-oriented: destination pixels 4g..4g+3 map to source pixels
* 5g..5g+3 (the 160->128 downscale drops every 5th column), so each
* destination nibble comes from 8 consecutive source bits, converted with
* a single LUT lookup instead of per-pixel shade extraction. */
static void frame_callback(void* ctx) {
AppState* app = (AppState*)ctx;
const u8* raw = app->gb->get_framebuffer().raw(); /* packed 2bpp */
@@ -221,19 +273,24 @@ static void frame_callback(void* ctx) {
for(int y = 0; y < SCREEN_H; y++) {
/* storage is compacted to the 64 displayed rows in ascending order
* (GB_FB_ROWS=64 + row mask), so displayed row y == storage slot y */
uint base = (uint)y * GAMEBOY_WIDTH;
const u8* src = raw + (uint)y * (GAMEBOY_WIDTH / 4);
uint8_t bit = (uint8_t)(1u << (y & 7));
uint8_t nbit = (uint8_t)~bit;
uint8_t* row = dst + (y >> 3) * SCREEN_W;
const uint8_t(*dither)[4] = s_dither[y & 1];
const uint8_t* dlut = s_dither4[y & 1];
for(int x = 0; x < SCREEN_W; x++) {
uint i = base + s_xmap[x];
uint s = (raw[i >> 2] >> ((i & 3) << 1)) & 0x3;
if(dither[x & 1][s])
row[x] |= bit;
else
row[x] &= nbit;
for(int g = 0; g < SCREEN_W / 4; g++) {
uint spx = (uint)g * 5; /* first source pixel of this group */
uint so = spx >> 2;
uint sh = (spx & 3) * 2;
uint packed = ((uint)src[so] | ((uint)src[so + 1] << 8)) >> sh;
uint8_t four = dlut[packed & 0xFF];
uint8_t* p = row + g * 4;
p[0] = (four & 1) ? (uint8_t)(p[0] | bit) : (uint8_t)(p[0] & nbit);
p[1] = (four & 2) ? (uint8_t)(p[1] | bit) : (uint8_t)(p[1] & nbit);
p[2] = (four & 4) ? (uint8_t)(p[2] | bit) : (uint8_t)(p[2] & nbit);
p[3] = (four & 8) ? (uint8_t)(p[3] | bit) : (uint8_t)(p[3] & nbit);
}
}
@@ -311,8 +368,11 @@ static void input_events_callback(const void* value, void* ctx) {
uint8_t keys =
(uint8_t)__atomic_or_fetch((uint8_t*)&app->keys, bit, __ATOMIC_RELAXED);
/* Up+Down together: physically impossible on a real GB d-pad,
* so it is our reserved menu gesture */
if((keys & (KBIT_UP | KBIT_DOWN)) == (KBIT_UP | KBIT_DOWN)) {
* so it is our reserved menu gesture. Ignored while the menu
* is already open: fast Up->Down rollover while navigating
* used to re-latch the request and instantly reopen the menu
* after closing it (merging consecutive Start injections). */
if(!app->menu_active && (keys & (KBIT_UP | KBIT_DOWN)) == (KBIT_UP | KBIT_DOWN)) {
app->menu_requested = true;
}
} else if(event->type == InputTypeRelease) {
@@ -325,7 +385,11 @@ static void input_events_callback(const void* value, void* ctx) {
/* Apply the current key snapshot to the emulated joypad (edge based) */
static void apply_input(AppState* app, uint8_t* last_applied) {
uint8_t now = app->menu_active ? 0 : app->keys;
/* keys_blocked: the A/B press that operated the menu must not leak
* into the game as a fresh press when the menu closes; blocked bits
* clear automatically on physical release */
app->keys_blocked &= app->keys;
uint8_t now = app->menu_active ? 0 : (uint8_t)(app->keys & ~app->keys_blocked);
uint8_t changed = (uint8_t)(now ^ *last_applied);
if(!changed && !app->inject_start_frames && !app->inject_select_frames) return;
@@ -379,7 +443,7 @@ static void sound_update(AppState* app, bool force_silent) {
ApuVoice best = {false, 0, 0, 0};
bool have = false;
if(!force_silent && app->sound_enabled && !app->menu_active) {
if(!force_silent && app->volume_setting && !app->menu_active) {
ApuVoice v;
/* pulse 1 / pulse 2 */
for(uint n = 0; n < 2; n++) {
@@ -422,10 +486,17 @@ static void sound_update(AppState* app, bool force_silent) {
uint8_t vol = (uint8_t)((best.volume * (master + 1)) >> 3); /* 0..15 */
if(vol == 0) {
have = false;
} else if(f != app->tone_freq || vol != app->tone_vol) {
furi_hal_speaker_start((float)f, (float)vol / 15.0f);
} else if(
f != app->tone_freq || vol != app->tone_vol ||
app->volume_setting != app->tone_setting) {
/* piezo loudness vs PWM value is very nonlinear: spread
* the 4 user levels perceptually, not linearly */
static const float level[5] = {0.0f, 0.10f, 0.25f, 0.50f, 1.0f};
furi_hal_speaker_start(
(float)f, ((float)vol / 15.0f) * level[app->volume_setting]);
app->tone_freq = f;
app->tone_vol = vol;
app->tone_setting = app->volume_setting;
}
}
}
@@ -511,18 +582,25 @@ static void menu_draw(AppState* app) {
canvas_draw_str(c, 72, y, buf);
}
if(i == 4) {
static const char* vols[5] = {"off", "25%", "50%", "75%", "100%"};
canvas_draw_str(
c, 72, y, !app->speaker_acquired ? "n/a" : (app->sound_enabled ? "on" : "off"));
c, 72, y, !app->speaker_acquired ? "n/a" : vols[app->volume_setting]);
}
}
if(app->status_msg[0]) {
canvas_draw_str(c, 70, 10, app->status_msg);
} else {
/* free heap indicator: helps spotting memory pressure on device */
char rambuf[16];
snprintf(rambuf, sizeof(rambuf), "%uk free", (unsigned)(memmgr_get_free_heap() / 1024u));
canvas_draw_str_aligned(c, 126, 10, AlignRight, AlignBottom, rambuf);
/* diagnostics: free heap + real cost of one emulated frame
* (16.7ms = full speed; above that the game runs slow) */
char diagbuf[28];
snprintf(
diagbuf,
sizeof(diagbuf),
"%uk %lums",
(unsigned)(memmgr_get_free_heap() / 1024u),
(unsigned long)(app->emu_ms_ema >> 4));
canvas_draw_str_aligned(c, 126, 10, AlignRight, AlignBottom, diagbuf);
}
canvas_commit(c);
@@ -554,7 +632,11 @@ static bool menu_tick(AppState* app, Storage* storage, FuriString* sav_path) {
app->frameskip_setting = v;
}
if(app->menu_cursor == 4 && app->speaker_acquired) {
app->sound_enabled = !app->sound_enabled;
/* volume: off, 25, 50, 75, 100% */
if(pressed & KBIT_RIGHT)
app->volume_setting = (uint8_t)((app->volume_setting + 1) % 5);
else
app->volume_setting = (uint8_t)((app->volume_setting + 4) % 5);
}
}
@@ -565,17 +647,22 @@ static bool menu_tick(AppState* app, Storage* storage, FuriString* sav_path) {
case 0:
return false;
case 1:
app->gb->button_pressed(GbButton::Start);
/* extend rather than re-press if an injection is still live:
* two overlapping presses would merge into one edge for the
* game (Tetris pause state would get out of sync) */
if(app->inject_start_frames == 0) app->gb->button_pressed(GbButton::Start);
app->inject_start_frames = 8;
return false;
case 2:
app->gb->button_pressed(GbButton::Select);
if(app->inject_select_frames == 0) app->gb->button_pressed(GbButton::Select);
app->inject_select_frames = 8;
return false;
case 3:
break;
case 4:
if(app->speaker_acquired) app->sound_enabled = !app->sound_enabled;
/* OK toggles mute <-> full volume */
if(app->speaker_acquired)
app->volume_setting = app->volume_setting ? 0 : 4;
break;
case 5:
if(app->has_battery) {
@@ -648,52 +735,59 @@ static RomLoadResult
memset(app->cart_ram, 0, app->cart_ram_size);
}
/* Allocate as many 16 KB bank slots as the heap safely affords.
*
* IMPORTANT: the emulator core (Gameboy: 8K VRAM + 8K WRAM + packed
* framebuffer + CPU state, ~22 KB) plus the GUI takeover (mutex, canvas,
* input subscription) are allocated AFTER the ROM cache, so room for
* them is reserved up front. Each slot is its own allocation: no huge
* contiguous block is ever requested, which makes the loader immune to
* heap fragmentation (the old single-malloc full-ROM path could crash
* the firmware even when the total free heap looked sufficient). */
u32 switchable = (u32)(rc->banks - 1);
const size_t reserve = HEAP_RESERVE + sizeof(Gameboy) + 1024 /* alloc slack */;
/* Reserve the emulator core RIGHT NOW as a real allocation instead of
* a pessimistic estimate: every byte of over-reservation here used to
* cost cache slots (Pokemon got 2-3 slots for a 1 MB ROM). The block
* is placement-constructed later. */
app->gb_mem = safe_malloc(sizeof(Gameboy));
if(!app->gb_mem) return RomLoadNoMem;
/* Allocate as many 8 KB page slots as the heap safely affords. Each
* slot is its own allocation: no huge contiguous block is ever
* requested, which makes the loader immune to heap fragmentation.
* HEAP_RESERVE stays free for the GUI takeover + system services. */
u32 switchable = (u32)(rc->banks - 1) * 2; /* 8 KB pages past bank 0 */
const size_t reserve = HEAP_RESERVE;
/* slot bookkeeping arrays (a few bytes per slot) */
size_t free_heap = memmgr_get_free_heap();
u32 max_slots = (u32)(free_heap / BANK_SIZE) + 1;
u32 max_slots = (u32)(free_heap / PAGE_SIZE) + 1;
if(max_slots > switchable) max_slots = switchable;
if(max_slots < 1) max_slots = 1;
if(max_slots < 2) max_slots = 2;
rc->slots = (u8**)safe_malloc(max_slots * sizeof(u8*));
rc->slot_bank = (u16*)safe_malloc(max_slots * sizeof(u16));
rc->slot_page = (u16*)safe_malloc(max_slots * sizeof(u16));
rc->slot_use = (u32*)safe_malloc(max_slots * sizeof(u32));
if(!rc->slots || !rc->slot_bank || !rc->slot_use) return RomLoadNoMem;
rc->slot_hits = (u16*)safe_malloc(max_slots * sizeof(u16));
if(!rc->slots || !rc->slot_page || !rc->slot_use || !rc->slot_hits) return RomLoadNoMem;
rc->num_slots = 0;
while((u32)rc->num_slots < max_slots) {
if(memmgr_get_free_heap() < reserve + BANK_SIZE + ALLOC_MARGIN) break;
u8* slot = (u8*)safe_malloc(BANK_SIZE);
if(memmgr_get_free_heap() < reserve + PAGE_SIZE + ALLOC_MARGIN) break;
u8* slot = (u8*)safe_malloc(PAGE_SIZE);
if(!slot) break;
rc->slots[rc->num_slots] = slot;
rc->slot_bank[rc->num_slots] = 0; /* bank 0 never lives in a slot */
rc->slot_page[rc->num_slots] = 0; /* page 0 never lives in a slot */
rc->slot_use[rc->num_slots] = 0;
rc->slot_hits[rc->num_slots] = 0;
rc->num_slots++;
}
/* not even one 16 KB slot fits: fail gracefully with the
* "Not enough RAM" dialog instead of crashing inside malloc() */
if(rc->num_slots == 0) return RomLoadNoMem;
/* fewer than two 8 KB slots: fail gracefully with the "Not enough
* RAM" dialog instead of crashing inside malloc(). Two is the hard
* minimum: the cartridge maps a lo and a (lazy) hi page and the
* provider guarantees the two most recently returned pages are never
* evicted -- with a single slot the lo pointer would dangle. */
if(rc->num_slots < 2) return RomLoadNoMem;
if((u32)rc->num_slots >= switchable) {
/* every switchable bank fits: preload the whole ROM and close the
/* every switchable page fits: preload the whole ROM and close the
* SD file (O(1) bank switching, zero stutter, frees the handle) */
storage_file_seek(rc->file, BANK_SIZE, true);
for(u32 i = 0; i < switchable; i++) {
size_t got = storage_file_read(rc->file, rc->slots[i], BANK_SIZE);
if(got < BANK_SIZE) memset(rc->slots[i] + got, 0xFF, BANK_SIZE - got);
rc->slot_bank[i] = (u16)(i + 1);
size_t got = storage_file_read(rc->file, rc->slots[i], PAGE_SIZE);
if(got < PAGE_SIZE) memset(rc->slots[i] + got, 0xFF, PAGE_SIZE - got);
rc->slot_page[i] = (u16)(i + 2);
}
rc->fully_loaded = true;
storage_file_close(rc->file);
@@ -709,8 +803,9 @@ static void rom_free(AppState* app) {
for(u16 i = 0; i < rc->num_slots; i++)
if(rc->slots[i]) free(rc->slots[i]);
if(rc->slots) free(rc->slots);
if(rc->slot_bank) free(rc->slot_bank);
if(rc->slot_page) free(rc->slot_page);
if(rc->slot_use) free(rc->slot_use);
if(rc->slot_hits) free(rc->slot_hits);
if(rc->bank0) free(rc->bank0);
if(rc->file) {
storage_file_close(rc->file);
@@ -778,18 +873,10 @@ extern "C" int32_t flipgb_app(void* p) {
app->fb_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
if(!app->fb_mutex) break;
/* rom_load reserved room for this, but double-check anyway: on the
* Flipper an unchecked new/malloc crashes the firmware on OOM */
void* gb_mem = safe_malloc(sizeof(Gameboy));
if(!gb_mem) {
DialogMessage* msg = dialog_message_alloc();
dialog_message_set_text(msg, "Not enough RAM", 64, 30, AlignCenter, AlignCenter);
dialog_message_set_buttons(msg, NULL, "OK", NULL);
dialog_message_show(dialogs, msg);
dialog_message_free(msg);
break;
}
app->gb = new(gb_mem) Gameboy(
/* rom_load pre-allocated this block (so the ROM cache could size
* itself against real free heap, not an estimate) */
furi_check(app->gb_mem != NULL);
app->gb = new(app->gb_mem) Gameboy(
app->rom.bank0,
app->rom.banks,
mbc,
@@ -802,7 +889,7 @@ extern "C" int32_t flipgb_app(void* p) {
/* --- sound: piezo plays the dominant APU voice --- */
app->speaker_acquired = furi_hal_speaker_acquire(50);
app->sound_enabled = app->speaker_acquired;
app->volume_setting = app->speaker_acquired ? 4 : 0;
/* --- take over the display --- */
gui = (Gui*)furi_record_open(RECORD_GUI);
@@ -846,6 +933,11 @@ extern "C" int32_t flipgb_app(void* p) {
}
furi_delay_ms(33);
}
/* the A/B press that operated the menu must not reach the
* game; the request latch is cleared so held Up+Down (or
* menu-navigation rollover) can't instantly reopen it */
app->keys_blocked = (uint8_t)(app->keys & (KBIT_A | KBIT_B));
app->menu_requested = false;
frame_deadline_us = 0;
last_now_ms = furi_get_tick();
continue;
@@ -867,11 +959,18 @@ extern "C" int32_t flipgb_app(void* p) {
/* refresh the piezo with this frame's dominant APU voice */
sound_update(app, false);
/* EMA of the cost of one emulated frame (x16 fixed point) */
app->emu_ms_ema += ((emu_ms << 4) - app->emu_ms_ema) / 8;
/* EMA of the cost of one emulated frame (x16 fixed point).
* MUST be signed: with unsigned arithmetic, any frame faster
* than the average underflowed the difference to ~4 billion,
* blowing the EMA up and pinning auto-frameskip at maximum. */
int32_t ema_diff = (int32_t)(emu_ms << 4) - (int32_t)app->emu_ms_ema;
app->emu_ms_ema = (uint32_t)((int32_t)app->emu_ms_ema + ema_diff / 8);
uint32_t ema_ms = app->emu_ms_ema >> 4;
app->auto_skip = ema_ms <= 17 ? 0 : (int)((ema_ms - 1) / 17);
if(app->auto_skip > 4) app->auto_skip = 4;
/* auto mode may skip harder than the fixed 0-4 settings: for
* heavy streamed games, correct game speed at a lower visible
* fps beats slow motion */
if(app->auto_skip > 8) app->auto_skip = 8;
if(render_this && !app->menu_active) {
canvas_commit(canvas);
@@ -925,6 +1024,9 @@ extern "C" int32_t flipgb_app(void* p) {
if(app->gb) {
app->gb->~Gameboy(); /* placement-new counterpart */
free(app->gb);
app->gb_mem = NULL;
} else if(app->gb_mem) {
free(app->gb_mem); /* rom_load reserved it but construction never ran */
}
rom_free(app);
if(app->cart_ram) free(app->cart_ram);
+25 -12
View File
@@ -13,12 +13,15 @@ def ProtoPirateDefineEnabled(name, app_manifest_path=app_manifest_path):
_ENABLE_TIMING_TUNER = ProtoPirateDefineEnabled("ENABLE_TIMING_TUNER_SCENE")
_ENABLE_EMULATE = ProtoPirateDefineEnabled("ENABLE_EMULATE_FEATURE")
_MAIN_APP_SOURCES = [
"*.c*",
"!protocols/plugins",
"!scenes/plugins",
"!protocols",
"!protopirate_emulate_plugin.c",
"!protopirate_psa_bf_plugin.c",
"protocols/protocol_items.c",
"protocols/protocols_common.c",
"!raw_file_reader.c",
@@ -30,6 +33,11 @@ if not _ENABLE_TIMING_TUNER:
"!protocol_timings.c",
]
if not _ENABLE_EMULATE:
_MAIN_APP_SOURCES += [
"!protopirate_scene_emulate.c",
]
App(
appid="proto_pirate",
name="ProtoPirate",
@@ -39,7 +47,7 @@ App(
requires=["gui"],
stack_size=8 * 1024,
fap_description="Decode car key fob signals from Sub-GHz",
fap_version="3.0",
fap_version="3.2",
fap_icon="images/protopirate_10px.png",
fap_category="Sub-GHz",
fap_icon_assets="images",
@@ -165,7 +173,11 @@ def ProtoPirateTxProtocolPlugin(
App=App,
FlipperAppType=FlipperAppType,
tx_plugin_sources=_TX_PLUGIN_SOURCES,
enable_emulate=_ENABLE_EMULATE,
):
if not enable_emulate:
return
App(
appid=f"protopirate_tx_{key}_plugin",
apptype=FlipperAppType.PLUGIN,
@@ -370,17 +382,18 @@ if _ENABLE_TIMING_TUNER:
fal_embedded=True,
)
App(
appid="protopirate_emulate_plugin",
apptype=FlipperAppType.PLUGIN,
entry_point="protopirate_emulate_plugin_ep",
requires=["proto_pirate"],
sources=[
"scenes/plugins/protopirate_emulate_plugin.c",
"protocols/protocol_items.c",
],
fal_embedded=True,
)
if _ENABLE_EMULATE:
App(
appid="protopirate_emulate_plugin",
apptype=FlipperAppType.PLUGIN,
entry_point="protopirate_emulate_plugin_ep",
requires=["proto_pirate"],
sources=[
"scenes/plugins/protopirate_emulate_plugin.c",
"protocols/protocol_items.c",
],
fal_embedded=True,
)
App(
appid="protopirate_psa_bf_plugin",
@@ -7,7 +7,9 @@
#include <string.h>
#define TAG "ProtoPirateProtocolPlugin"
#ifdef ENABLE_EMULATE_FEATURE
#define PROTOPIRATE_TX_PLUGIN_PATH_MAX 160U
#endif
static const char* protopirate_get_registry_plugin_path(ProtoPirateProtocolRegistryRoute route) {
switch(route) {
@@ -25,6 +27,7 @@ static const char* protopirate_get_registry_plugin_path(ProtoPirateProtocolRegis
}
}
#ifdef ENABLE_EMULATE_FEATURE
static bool protopirate_build_tx_protocol_plugin_path(
const char* tx_key,
char* plugin_path,
@@ -40,6 +43,7 @@ static bool protopirate_build_tx_protocol_plugin_path(
tx_key);
return (written > 0) && ((size_t)written < plugin_path_size);
}
#endif
static const SubGhzProtocolRegistry protopirate_empty_protocol_registry = {
.items = NULL,
@@ -165,6 +169,7 @@ static bool protopirate_ensure_protocol_registry_plugin(
return true;
}
#ifdef ENABLE_EMULATE_FEATURE
static bool protopirate_ensure_tx_protocol_plugin(
ProtoPirateApp* app,
const char* protocol_name,
@@ -264,6 +269,7 @@ static bool protopirate_ensure_tx_protocol_plugin(
*registry = plugin->registry;
return true;
}
#endif
bool protopirate_refresh_protocol_registry(ProtoPirateApp* app, bool ensure_receiver_ready) {
furi_check(app);
@@ -378,6 +384,7 @@ bool protopirate_apply_protocol_registry_for_context(
}
if(protocol_name && protocol_name[0] != '\0') {
#ifdef ENABLE_EMULATE_FEATURE
const char* registry_name = protopirate_protocol_catalog_canonical_name(protocol_name);
if(!registry_name || !protopirate_protocol_catalog_can_tx(protocol_name)) {
FURI_LOG_E(TAG, "No TX protocol plugin for %s", protocol_name);
@@ -410,6 +417,9 @@ bool protopirate_apply_protocol_registry_for_context(
subghz_environment_set_protocol_registry(app->txrx->environment, tx_registry);
app->txrx->protocol_registry = tx_registry;
return true;
#else
return false;
#endif
}
ProtoPirateProtocolRegistryRoute route = protopirate_get_protocol_registry_route(
@@ -103,12 +103,14 @@ void protopirate_settings_load(ProtoPirateSettings* settings) {
}
settings->hopping_enabled = (hopping_temp == 1);
#ifdef ENABLE_EMULATE_FEATURE
uint32_t emulate_temp = 0;
if(!flipper_format_read_uint32(ff, "EmulateFeature", &emulate_temp, 1)) {
FURI_LOG_I(TAG, "EmulateFeature key missing, defaulting to disabled");
emulate_temp = 0;
}
settings->emulate_feature_enabled = (emulate_temp == 1);
#endif
uint32_t check_saved_temp = 0;
if(!flipper_format_read_uint32(ff, "CheckSaved", &check_saved_temp, 1)) {
@@ -185,11 +187,13 @@ void protopirate_settings_save(ProtoPirateSettings* settings) {
break;
}
#ifdef ENABLE_EMULATE_FEATURE
uint32_t emulate_temp = settings->emulate_feature_enabled ? 1 : 0;
if(!flipper_format_write_uint32(ff, "EmulateFeature", &emulate_temp, 1)) {
FURI_LOG_E(TAG, "Failed to write emulate feature flag");
break;
}
#endif
uint32_t check_saved_temp = settings->check_saved ? 1 : 0;
if(!flipper_format_write_uint32(ff, "CheckSaved", &check_saved_temp, 1)) {
@@ -513,6 +513,20 @@ static bool protopirate_storage_copy_key_2(
protopirate_storage_copy_u32_optional(save_file, flipper_format, "Key_2");
}
static bool protopirate_storage_copy_key2(
FlipperFormat* save_file,
FlipperFormat* flipper_format) {
bool copied = false;
if(!protopirate_storage_copy_hex_fixed(save_file, flipper_format, "Key2", 8, &copied)) {
return false;
}
if(copied) {
return true;
}
return protopirate_storage_copy_hex_or_u32(save_file, flipper_format, "Key2", 4);
}
static bool protopirate_storage_write_capture_data(
FlipperFormat* save_file,
FlipperFormat* flipper_format) {
@@ -548,7 +562,7 @@ static bool protopirate_storage_write_capture_data(
protopirate_storage_base_u32_fields,
COUNT_OF(protopirate_storage_base_u32_fields)))
break;
if(!protopirate_storage_copy_hex_or_u32(save_file, flipper_format, "Key2", 4)) break;
if(!protopirate_storage_copy_key2(save_file, flipper_format)) break;
if(!protopirate_storage_copy_u32_optional(save_file, flipper_format, "KeyIdx")) break;
if(!protopirate_storage_copy_u32_optional(save_file, flipper_format, "Seed")) break;
if(!protopirate_storage_copy_hex_or_u32(save_file, flipper_format, "ValidationField", 2))
@@ -3,6 +3,8 @@
#include <furi.h>
#include <string.h>
#include "../defines.h"
#define TAG "ProtoPirateCatalog"
#define PROTOPIRATE_CC1101_REG_MDMCFG2 0x12U
@@ -17,6 +19,12 @@
#define PROTOPIRATE_COUNT_OF(array) (sizeof(array) / sizeof((array)[0]))
#ifdef ENABLE_EMULATE_FEATURE
#define PROTOPIRATE_TX_KEY(key) key
#else
#define PROTOPIRATE_TX_KEY(key) NULL
#endif
typedef enum {
ProtoPirateProtocolCatalogModulationAM = 0,
ProtoPirateProtocolCatalogModulationFM,
@@ -28,33 +36,33 @@ typedef struct {
} ProtoPirateProtocolCatalogAlias;
static const ProtoPirateProtocolCatalogEntry protopirate_protocol_catalog[] = {
{"Chrysler V0", ProtoPirateProtocolCatalogRouteAMDefault, "chrysler_v0"},
{"Fiat V0", ProtoPirateProtocolCatalogRouteAMDefault, "fiat_v0"},
{"Fiat V1", ProtoPirateProtocolCatalogRouteAMDefault, "fiat_v1"},
{"Chrysler V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("chrysler_v0")},
{"Fiat V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("fiat_v0")},
{"Fiat V1", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("fiat_v1")},
{"Fiat V2", ProtoPirateProtocolCatalogRouteAMDefault, NULL},
{"Ford V0", ProtoPirateProtocolCatalogRouteAMDefault, "ford_v0"},
{"Ford V1", ProtoPirateProtocolCatalogRouteFMF4, "ford_v1"},
{"Ford V2", ProtoPirateProtocolCatalogRouteFMF4, "ford_v2"},
{"Ford V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("ford_v0")},
{"Ford V1", ProtoPirateProtocolCatalogRouteFMF4, PROTOPIRATE_TX_KEY("ford_v1")},
{"Ford V2", ProtoPirateProtocolCatalogRouteFMF4, PROTOPIRATE_TX_KEY("ford_v2")},
{"Ford V3", ProtoPirateProtocolCatalogRouteFMF4, NULL},
{"Honda Static", ProtoPirateProtocolCatalogRouteFMHonda1, "honda_static"},
{"Honda V1", ProtoPirateProtocolCatalogRouteAMDefault, "honda_v1"},
{"Kia V0", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v0"},
{"Kia V1", ProtoPirateProtocolCatalogRouteAMDefault, "kia_v1"},
{"Kia V2", ProtoPirateProtocolCatalogRouteAMDefault, "kia_v2"},
{"Kia V3/V4", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v3_v4"},
{"Kia V5", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v5"},
{"Kia V6", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v6"},
{"Kia V7", ProtoPirateProtocolCatalogRouteFMDefault, "kia_v7"},
{"Honda V2", ProtoPirateProtocolCatalogRouteFMF4, "honda_v2"},
{"Mazda V0", ProtoPirateProtocolCatalogRouteByModulation, "mazda_v0"},
{"Honda Static", ProtoPirateProtocolCatalogRouteFMHonda1, PROTOPIRATE_TX_KEY("honda_static")},
{"Honda V1", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("honda_v1")},
{"Kia V0", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v0")},
{"Kia V1", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("kia_v1")},
{"Kia V2", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("kia_v2")},
{"Kia V3/V4", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v3_v4")},
{"Kia V5", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v5")},
{"Kia V6", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v6")},
{"Kia V7", ProtoPirateProtocolCatalogRouteFMDefault, PROTOPIRATE_TX_KEY("kia_v7")},
{"Honda V2", ProtoPirateProtocolCatalogRouteFMF4, PROTOPIRATE_TX_KEY("honda_v2")},
{"Mazda V0", ProtoPirateProtocolCatalogRouteByModulation, PROTOPIRATE_TX_KEY("mazda_v0")},
{"Mitsubishi V0", ProtoPirateProtocolCatalogRouteFMDefault, NULL},
{"Porsche Touareg", ProtoPirateProtocolCatalogRouteAMDefault, NULL},
{"PSA", ProtoPirateProtocolCatalogRouteByModulation, "psa"},
{"Renault V0", ProtoPirateProtocolCatalogRouteAMDefault, "renault_v0"},
{"PSA", ProtoPirateProtocolCatalogRouteByModulation, PROTOPIRATE_TX_KEY("psa")},
{"Renault V0", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("renault_v0")},
{"Scher-Khan", ProtoPirateProtocolCatalogRouteFMDefault, NULL},
{"Star Line", ProtoPirateProtocolCatalogRouteAMDefault, "star_line"},
{"Subaru", ProtoPirateProtocolCatalogRouteAMDefault, "subaru"},
{"VAG", ProtoPirateProtocolCatalogRouteAMVag, "vag"},
{"Star Line", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("star_line")},
{"Subaru", ProtoPirateProtocolCatalogRouteAMDefault, PROTOPIRATE_TX_KEY("subaru")},
{"VAG", ProtoPirateProtocolCatalogRouteAMVag, PROTOPIRATE_TX_KEY("vag")},
};
static const ProtoPirateProtocolCatalogAlias protopirate_protocol_catalog_aliases[] = {
@@ -90,7 +90,11 @@ ProtoPirateApp* protopirate_app_alloc() {
app->auto_save = settings.auto_save;
app->check_saved = settings.check_saved;
app->tx_power = settings.tx_power;
#ifdef ENABLE_EMULATE_FEATURE
app->emulate_feature_enabled = settings.emulate_feature_enabled;
#else
app->emulate_feature_enabled = false;
#endif
// Init setting - KEEP THIS, it's small
app->setting = subghz_setting_alloc();
@@ -175,7 +179,11 @@ void protopirate_app_free(ProtoPirateApp* app) {
settings.check_saved = app->check_saved;
settings.tx_power = app->tx_power;
settings.hopping_enabled = (app->txrx->hopper_state != ProtoPirateHopperStateOFF);
#ifdef ENABLE_EMULATE_FEATURE
settings.emulate_feature_enabled = app->emulate_feature_enabled;
#else
settings.emulate_feature_enabled = false;
#endif
// Find current preset index
settings.preset_index = 0;
@@ -273,14 +281,18 @@ int32_t protopirate_app(char* p) {
//We now jump straight to emulate scene from Browser. If the user wanted the key to look at, just click back.
if(load_saved) {
#ifdef ENABLE_EMULATE_FEATURE
if(protopirate_app->emulate_feature_enabled) {
view_dispatcher_send_custom_event(
protopirate_app->view_dispatcher, ProtoPirateCustomEventSavedInfoEmulate);
notification_message(protopirate_app->notifications, &sequence_success);
} else {
#endif
view_dispatcher_send_custom_event(
protopirate_app->view_dispatcher, ProtoPirateCustomEventReceiverInfoSave);
#ifdef ENABLE_EMULATE_FEATURE
}
#endif
}
view_dispatcher_run(protopirate_app->view_dispatcher);
@@ -12,6 +12,7 @@
#define CREDIT_LINE_HEIGHT 10
#define SCROLL_SPEED 1
#ifdef ENABLE_EMULATE_FEATURE
static const InputKey EMULATE_TOGGLE_COMBO[] = {
InputKeyUp,
InputKeyUp,
@@ -23,6 +24,7 @@ static const InputKey EMULATE_TOGGLE_COMBO[] = {
InputKeyRight,
};
#define EMULATE_TOGGLE_COMBO_LEN (sizeof(EMULATE_TOGGLE_COMBO) / sizeof(EMULATE_TOGGLE_COMBO[0]))
#endif
static const char* credits[] = {
"",
@@ -62,7 +64,9 @@ typedef struct {
uint8_t frame;
uint8_t seed;
int16_t scroll_offset;
#ifdef ENABLE_EMULATE_FEATURE
uint8_t combo_progress;
#endif
} GlitchState;
static GlitchState g_state = {0};
@@ -159,6 +163,7 @@ static void about_draw_callback(Canvas* canvas, void* context) {
static bool about_input_callback(InputEvent* event, void* context) {
furi_check(context);
#ifdef ENABLE_EMULATE_FEATURE
ProtoPirateApp* app = context;
if(event->type != InputTypePress) {
@@ -184,8 +189,13 @@ static bool about_input_callback(InputEvent* event, void* context) {
}
return true;
#else
UNUSED(event);
return false;
#endif
}
#ifdef ENABLE_EMULATE_FEATURE
static void about_show_emulate_toggle_popup(ProtoPirateApp* app) {
const bool now_enabled = app->emulate_feature_enabled;
@@ -204,6 +214,7 @@ static void about_show_emulate_toggle_popup(ProtoPirateApp* app) {
dialog_message_show(app->dialogs, message);
dialog_message_free(message);
}
#endif
void protopirate_scene_about_on_enter(void* context) {
furi_check(context);
@@ -218,7 +229,9 @@ void protopirate_scene_about_on_enter(void* context) {
g_state.frame = 0;
g_state.seed = furi_get_tick() & 0xFF;
g_state.scroll_offset = 0;
#ifdef ENABLE_EMULATE_FEATURE
g_state.combo_progress = 0;
#endif
view_set_draw_callback(app->view_about, about_draw_callback);
view_set_input_callback(app->view_about, about_input_callback);
@@ -245,7 +258,9 @@ bool protopirate_scene_about_on_event(void* context, SceneManagerEvent event) {
view_commit_model(app->view_about, true);
consumed = true;
} else if(event.type == SceneManagerEventTypeCustom) {
}
#ifdef ENABLE_EMULATE_FEATURE
else if(event.type == SceneManagerEventTypeCustom) {
if(event.event == ProtoPirateCustomEventAboutToggleEmulate) {
app->emulate_feature_enabled = !app->emulate_feature_enabled;
@@ -262,6 +277,7 @@ bool protopirate_scene_about_on_event(void* context, SceneManagerEvent event) {
consumed = true;
}
}
#endif
return consumed;
}
@@ -1205,7 +1205,9 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
app);
ctx->signal_info_left_is_emulate = false;
#ifdef ENABLE_EMULATE_FEATURE
bool left_button_used = false;
#endif
app->emulate_disabled_for_loaded = true;
// Store reference to history item's flipper format for saving
@@ -1239,7 +1241,9 @@ bool protopirate_scene_sub_decode_on_event(void* context, SceneManagerEvent even
"Brute force",
protopirate_scene_sub_decode_widget_callback,
app);
#ifdef ENABLE_EMULATE_FEATURE
left_button_used = true;
#endif
}
}
}
+4
View File
@@ -24,6 +24,10 @@ uint8_t subghz_custom_btn_get(void) {
return custom_btn_id;
}
uint8_t subghz_custom_btn_get_max(void) {
return custom_btn_max_btns;
}
void subghz_custom_btn_set_original(uint8_t btn_code) {
custom_btn_original = btn_code;
}
+2
View File
@@ -13,6 +13,8 @@ void subghz_custom_btn_set_original(uint8_t btn_code);
void subghz_custom_btn_set_max(uint8_t b);
uint8_t subghz_custom_btn_get_max(void);
void subghz_custom_btn_set_prog_mode(ProgMode prog_mode);
ProgMode subghz_custom_btn_get_prog_mode(void);
+1 -1
View File
@@ -407,7 +407,7 @@ LevelDuration subghz_protocol_encoder_fiat_spa_yield(void* context) {
}
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
return ret;
+10 -2
View File
@@ -364,7 +364,7 @@ LevelDuration subghz_protocol_encoder_ford_v0_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
@@ -438,12 +438,18 @@ SubGhzProtocolStatus subghz_protocol_encoder_ford_v0_deserialize(void* context,
}
subghz_custom_btn_set_max(3);
instance->count = (instance->count + furi_hal_subghz_get_rolling_counter_mult()) & 0xFFFFF;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->count = override_cnt & 0xFFFFF;
} else {
instance->count = (instance->count + furi_hal_subghz_get_rolling_counter_mult()) & 0xFFFFF;
}
uint8_t btn = subghz_custom_btn_get() == SUBGHZ_CUSTOM_BTN_OK ?
subghz_custom_btn_get_original() :
subghz_custom_btn_get();
instance->button = ford_v0_get_button_code(btn);
subghz_block_generic_global_button_override_get(&instance->button);
uint8_t new_chk = 0;
encode_ford_v0(instance->generic.data, instance->serial, instance->button, instance->count, &new_chk, &instance->key1);
@@ -459,6 +465,8 @@ SubGhzProtocolStatus subghz_protocol_encoder_ford_v0_deserialize(void* context,
flipper_format_update_hex(flipper_format, "Key", key_data, 8);
flipper_format_update_uint32(flipper_format, "CheckSum", (uint32_t[]){new_chk}, 1);
flipper_format_update_uint32(flipper_format, "CRC", (uint32_t[]){new_crc}, 1);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", (uint32_t[]){instance->button}, 1);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->count, 1);
instance->encoder.is_running = true;
return SubGhzProtocolStatusOk;
+3 -2
View File
@@ -1278,8 +1278,9 @@ LevelDuration subghz_protocol_encoder_ford_v1_yield(void* context) {
"Encoder yield: finished one full %lu-word frame (all %u bursts); repeats_left=%u",
(unsigned long)instance->encoder.size_upload,
(unsigned)FORD_V1_ENC_BURST_COUNT,
(unsigned)instance->encoder.repeat - 1U);
instance->encoder.repeat--;
subghz_block_generic_global.endless_tx ? (unsigned)instance->encoder.repeat :
(unsigned)instance->encoder.repeat - 1U);
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
} else if(instance->encoder.front <= 4U) {
uint32_t raw_word;
+72 -22
View File
@@ -399,6 +399,62 @@ static void ford_v2_encoder_refresh_data_from_raw(SubGhzProtocolEncoderFordV2* i
}
}
static void ford_v2_encoder_set_button_code(
SubGhzProtocolEncoderFordV2* instance,
uint8_t new_code) {
if(!ford_v2_button_is_valid(new_code)) return;
instance->raw_bytes[6] = new_code;
instance->raw_bytes[7] =
(instance->raw_bytes[7] & 0x7FU) | (uint8_t)(ford_v2_uint8_parity(new_code) << 7);
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.btn = new_code;
}
static void ford_v2_encoder_set_counter(
SubGhzProtocolEncoderFordV2* instance,
uint16_t cnt) {
cnt &= 0x7FFFU;
instance->raw_bytes[7] =
(instance->raw_bytes[7] & 0x80U) | (uint8_t)((cnt >> 9) & 0x7FU);
instance->raw_bytes[8] = (uint8_t)((cnt >> 1) & 0xFFU);
instance->raw_bytes[9] =
(instance->raw_bytes[9] & 0x7FU) | (uint8_t)((cnt & 1U) << 7);
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.cnt = cnt;
}
static void ford_v2_encoder_update_flipper_format(
SubGhzProtocolEncoderFordV2* instance,
FlipperFormat* flipper_format) {
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(
flipper_format, "Key", instance->raw_bytes, FORD_V2_KEY_BYTE_COUNT);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(
flipper_format, "Serial", &instance->generic.serial, 1);
uint32_t btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &btn, 1);
uint32_t cnt = instance->generic.cnt;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &cnt, 1);
uint32_t tail31 = (((uint32_t)(instance->raw_bytes[9] & 0x7FU)) << 24) |
((uint32_t)instance->raw_bytes[10] << 16) |
((uint32_t)instance->raw_bytes[11] << 8) |
(uint32_t)instance->raw_bytes[12];
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Tail31", &tail31, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(
flipper_format, "TailRaw", &instance->raw_bytes[8], FORD_V2_TAIL_RAW_BYTE_COUNT);
}
static inline void ford_v2_encoder_emit_manchester_bit(
SubGhzProtocolEncoderFordV2* instance,
bool bit) {
@@ -490,17 +546,14 @@ static SubGhzProtocolStatus ford_v2_encoder_deserialize_validate_and_pack(
(((uint16_t)instance->raw_bytes[8]) << 1) |
((uint16_t)(instance->raw_bytes[9] >> 7)));
cnt = (cnt + 1U) & 0x7FFFU;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
cnt = override_cnt & 0x7FFFU;
} else {
cnt = (cnt + furi_hal_subghz_get_rolling_counter_mult()) & 0x7FFFU;
}
// raw_bytes[7] bits [6:0] = cnt[14:8], bit[7] = parity(btn)
instance->raw_bytes[7] = (instance->raw_bytes[7] & 0x80U) |
(uint8_t)((cnt >> 9) & 0x7FU);
instance->raw_bytes[8] = (uint8_t)((cnt >> 1) & 0xFFU);
// raw_bytes[9] bit[7] = cnt[0], bits[6:0] = tail
instance->raw_bytes[9] = (instance->raw_bytes[9] & 0x7FU) |
(uint8_t)((cnt & 1U) << 7);
ford_v2_encoder_refresh_data_from_raw(instance);
ford_v2_encoder_set_counter(instance, cnt);
instance->generic.btn = instance->raw_bytes[6];
instance->generic.serial =
@@ -566,17 +619,13 @@ SubGhzProtocolStatus
if(ret == SubGhzProtocolStatusOk) {
ford_v2_custom_btn_init(instance->raw_bytes[6]);
uint8_t btn_sel = subghz_custom_btn_get();
if(btn_sel != SUBGHZ_CUSTOM_BTN_OK) {
uint8_t new_code = ford_v2_custom_btn_to_code(btn_sel);
if(ford_v2_button_is_valid(new_code)) {
instance->raw_bytes[6] = new_code;
const uint8_t k7_msb =
(uint8_t)(ford_v2_uint8_parity(new_code) << 7);
instance->raw_bytes[7] =
(instance->raw_bytes[7] & 0x7FU) | k7_msb;
ford_v2_encoder_refresh_data_from_raw(instance);
instance->generic.btn = new_code;
uint8_t new_code = 0;
if(subghz_block_generic_global_button_override_get(&new_code)) {
ford_v2_encoder_set_button_code(instance, new_code);
} else {
uint8_t btn_sel = subghz_custom_btn_get();
if(btn_sel != SUBGHZ_CUSTOM_BTN_OK) {
ford_v2_encoder_set_button_code(instance, ford_v2_custom_btn_to_code(btn_sel));
}
}
@@ -586,6 +635,7 @@ SubGhzProtocolStatus
(instance->extra_data << 8) | (uint64_t)instance->raw_bytes[8U + i];
}
ford_v2_encoder_update_flipper_format(instance, flipper_format);
ford_v2_encoder_deserialize_apply_repeat(instance, flipper_format);
ford_v2_encoder_build_upload(instance);
instance->encoder.is_running = true;
@@ -615,7 +665,7 @@ LevelDuration subghz_protocol_encoder_ford_v2_yield(void* context) {
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.front = 0U;
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
}
return ret;
+13 -4
View File
@@ -154,7 +154,7 @@ LevelDuration subghz_protocol_encoder_kia_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
@@ -193,8 +193,10 @@ static bool subghz_protocol_encoder_kia_get_upload(SubGhzProtocolEncoderKIA* ins
instance->encoder.size_upload = size_upload;
}
// Counter increment logic
if(instance->generic.cnt < 0xFFFF) {
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
} else if(instance->generic.cnt < 0xFFFF) {
if((instance->generic.cnt + furi_hal_subghz_get_rolling_counter_mult()) > 0xFFFF) {
instance->generic.cnt = 0;
} else {
@@ -209,6 +211,7 @@ static bool subghz_protocol_encoder_kia_get_upload(SubGhzProtocolEncoderKIA* ins
uint8_t btn = subghz_custom_btn_get() == SUBGHZ_CUSTOM_BTN_OK ?
subghz_custom_btn_get_original() :
subghz_custom_btn_get();
subghz_block_generic_global_button_override_get(&btn);
// Update the generic button value for potential button changes
instance->generic.btn = btn;
@@ -325,6 +328,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_deserialize(void* context, Flip
break;
}
uint32_t temp_btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &temp_btn, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
} while(false);
@@ -594,4 +604,3 @@ void subghz_protocol_decoder_kia_get_string(void* context, FuriString* output) {
received_crc,
crc_valid ? "(OK)" : "(FAIL)");
}
+15 -4
View File
@@ -174,7 +174,7 @@ LevelDuration subghz_protocol_encoder_kia_v1_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
@@ -289,8 +289,10 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v1_deserialize(void* context, F
}
subghz_custom_btn_set_max(4);
// Incrementa counter
if(instance->generic.cnt < 0xFFF) {
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFF;
} else if(instance->generic.cnt < 0xFFF) {
instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult();
if(instance->generic.cnt > 0xFFF) {
instance->generic.cnt = 0;
@@ -301,7 +303,9 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v1_deserialize(void* context, F
// Gestione bottoni custom
uint8_t btn = subghz_custom_btn_get();
if(btn != SUBGHZ_CUSTOM_BTN_OK) {
if(subghz_block_generic_global_button_override_get(&btn)) {
instance->generic.btn = btn;
} else if(btn != SUBGHZ_CUSTOM_BTN_OK) {
instance->generic.btn = btn;
}
@@ -324,6 +328,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v1_deserialize(void* context, F
ret = SubGhzProtocolStatusErrorParserKey;
break;
}
uint32_t temp_btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &temp_btn, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
+15 -3
View File
@@ -183,7 +183,7 @@ LevelDuration subghz_protocol_encoder_kia_v2_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
@@ -214,7 +214,10 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v2_deserialize(void* context, F
}
subghz_custom_btn_set_max(4);
if(instance->generic.cnt < 0xFFF) {
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFF;
} else if(instance->generic.cnt < 0xFFF) {
instance->generic.cnt += furi_hal_subghz_get_rolling_counter_mult();
if(instance->generic.cnt > 0xFFF) {
instance->generic.cnt = 0;
@@ -224,7 +227,9 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v2_deserialize(void* context, F
}
uint8_t btn = subghz_custom_btn_get();
if(btn != SUBGHZ_CUSTOM_BTN_OK) {
if(subghz_block_generic_global_button_override_get(&btn)) {
instance->generic.btn = btn;
} else if(btn != SUBGHZ_CUSTOM_BTN_OK) {
instance->generic.btn = btn;
}
@@ -257,6 +262,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v2_deserialize(void* context, F
ret = SubGhzProtocolStatusErrorParserKey;
break;
}
uint32_t temp_btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &temp_btn, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
+34 -7
View File
@@ -124,6 +124,8 @@ static uint8_t kia_v3_v4_calculate_crc(uint8_t* bytes) {
return crc & 0x0F;
}
static uint8_t kia_v3_v4_btn_to_custom(uint8_t btn);
static bool kia_v3_v4_process_buffer(SubGhzProtocolDecoderKiaV3V4* instance) {
if(instance->raw_bit_count < 68) {
return false;
@@ -175,7 +177,7 @@ static bool kia_v3_v4_process_buffer(SubGhzProtocolDecoderKiaV3V4* instance) {
instance->decoder.decode_count_bit = 68;
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(kia_v3_v4_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(5);
@@ -222,6 +224,17 @@ static const char* subghz_protocol_kia_v3_v4_get_name_button(uint8_t btn) {
}
}
static uint8_t kia_v3_v4_btn_to_custom(uint8_t btn) {
switch(btn) {
case 0x1: return 1;
case 0x2: return 2;
case 0x3: return 3;
case 0x4: return 4;
case 0x8: return 5;
default: return 1;
}
}
void* subghz_protocol_encoder_kia_v3_v4_alloc(SubGhzEnvironment* environment) {
UNUSED(environment);
SubGhzProtocolEncoderKiaV3V4* instance = malloc(sizeof(SubGhzProtocolEncoderKiaV3V4));
@@ -520,7 +533,7 @@ SubGhzProtocolStatus
}
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->btn);
subghz_custom_btn_set_original(kia_v3_v4_btn_to_custom(instance->btn));
}
subghz_custom_btn_set_max(5);
@@ -531,14 +544,21 @@ SubGhzProtocolStatus
selected_btn = subghz_custom_btn_get();
}
if(selected_btn == 5) {
if(subghz_block_generic_global_button_override_get(&selected_btn)) {
instance->btn = selected_btn;
} else if(selected_btn == 5) {
instance->btn = 0x8;
} else if(selected_btn >= 1 && selected_btn <= 4) {
instance->btn = selected_btn;
}
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->cnt = (instance->cnt + mult) & 0xFFFF;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->cnt = override_cnt & 0xFFFF;
} else {
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->cnt = (instance->cnt + mult) & 0xFFFF;
}
instance->generic.btn = instance->btn;
instance->generic.cnt = instance->cnt;
@@ -583,6 +603,13 @@ SubGhzProtocolStatus
uint32_t crc_to_write = instance->crc;
flipper_format_update_uint32(flipper_format, "CRC", &crc_to_write, 1);
uint32_t btn_to_write = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &btn_to_write, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
instance->encoder.front = 0;
@@ -648,7 +675,7 @@ LevelDuration subghz_protocol_encoder_kia_v3_v4_yield(void* context) {
instance->crc_iter = (uint8_t)((instance->crc_iter + 1U) & 0x0FU);
subghz_protocol_encoder_kia_v3_v4_patch_crc(instance);
instance->encoder.front = 0;
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
if(instance->bursts_sent < 16U) {
instance->bursts_sent++;
}
@@ -931,7 +958,7 @@ SubGhzProtocolStatus
}
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(kia_v3_v4_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(5);
+68 -19
View File
@@ -95,8 +95,7 @@ static uint16_t mixer_decode(uint32_t encrypted) {
return (s0 + (s1 << 8)) & 0xFFFF;
}
static __attribute__((unused)) uint32_t mixer_encode(
uint32_t serial, uint16_t counter, uint8_t button) {
static uint32_t mixer_encode(uint32_t serial, uint16_t counter, uint8_t button) {
uint8_t s0 = (uint8_t)(((serial >> 8) & 0x0FU) | ((button & 0x0FU) << 4));
uint8_t s1 = (uint8_t)((counter >> 8) & 0xFFU);
uint8_t s2 = (uint8_t)(serial & 0xFFU);
@@ -156,6 +155,37 @@ static __attribute__((unused)) uint32_t mixer_encode(
return ((uint32_t)s0 << 24) | ((uint32_t)s2 << 16) | ((uint32_t)s1 << 8) | (uint32_t)s3;
}
static uint64_t kia_v5_encode_data(uint32_t serial, uint16_t counter, uint8_t button) {
serial &= 0x0FFFFFFFU;
button &= 0x0FU;
const uint32_t encrypted = mixer_encode(serial, counter, button);
const uint64_t yek =
((uint64_t)button << 60) | ((uint64_t)serial << 32) | (uint64_t)encrypted;
return bit_reverse_64(yek);
}
static uint8_t kia_v5_custom_to_btn(uint8_t custom_btn) {
switch(custom_btn) {
case 1: return 0x01; // Unlock
case 2: return 0x02; // Lock
case 3: return 0x04; // Trunk
case 4: return 0x08; // Horn
default: return 0x01;
}
}
static uint8_t kia_v5_btn_to_custom(uint8_t btn) {
switch(btn) {
case 0x01: return 1; // Unlock
case 0x02: return 2; // Lock
case 0x04: return 3; // Trunk
case 0x08: return 4; // Horn
default: return 1;
}
}
struct SubGhzProtocolDecoderKiaV5 {
SubGhzProtocolDecoderBase base;
SubGhzBlockDecoder decoder;
@@ -317,15 +347,31 @@ SubGhzProtocolStatus
uint32_t encrypted = (uint32_t)(yek & 0xFFFFFFFF);
instance->generic.cnt = mixer_decode(encrypted);
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->generic.cnt = (instance->generic.cnt + mult) & 0xFFFF;
FURI_LOG_I(TAG, "deserialize #%lu, cnt after=%04lX", call_count, (uint32_t)instance->generic.cnt);
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(kia_v5_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(4);
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
} else {
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->generic.cnt = (instance->generic.cnt + mult) & 0xFFFF;
}
FURI_LOG_I(
TAG, "deserialize #%lu, cnt after=%04lX", call_count, (uint32_t)instance->generic.cnt);
uint8_t btn = kia_v5_custom_to_btn(
subghz_custom_btn_get() == SUBGHZ_CUSTOM_BTN_OK ? subghz_custom_btn_get_original() :
subghz_custom_btn_get());
if(subghz_block_generic_global_button_override_get(&btn)) {
FURI_LOG_D(TAG, "Button changed to 0x%X", btn);
}
instance->generic.btn = btn & 0x0F;
instance->generic.data = kia_v5_encode_data(
instance->generic.serial, instance->generic.cnt, instance->generic.btn);
instance->replay_data = instance->generic.data;
instance->replay_crc = kia_v5_calculate_crc(instance->replay_data);
@@ -349,12 +395,12 @@ SubGhzProtocolStatus
}
uint32_t temp_btn = instance->generic.btn;
if(!flipper_format_update_uint32(flipper_format, "Btn", &temp_btn, 1)) {
if(!flipper_format_insert_or_update_uint32(flipper_format, "Btn", &temp_btn, 1)) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
if(!flipper_format_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1)) {
if(!flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1)) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
@@ -362,18 +408,18 @@ SubGhzProtocolStatus
yek = bit_reverse_64(instance->generic.data);
yek_high = (uint32_t)(yek >> 32);
yek_low = (uint32_t)(yek & 0xFFFFFFFF);
if(!flipper_format_update_uint32(flipper_format, "YekHi", &yek_high, 1)) {
if(!flipper_format_insert_or_update_uint32(flipper_format, "YekHi", &yek_high, 1)) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
if(!flipper_format_update_uint32(flipper_format, "YekLo", &yek_low, 1)) {
if(!flipper_format_insert_or_update_uint32(flipper_format, "YekLo", &yek_low, 1)) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
uint8_t crc = kia_v5_calculate_crc(yek);
uint32_t crc_temp = crc;
if(!flipper_format_update_uint32(flipper_format, "CRC", &crc_temp, 1)) {
if(!flipper_format_insert_or_update_uint32(flipper_format, "CRC", &crc_temp, 1)) {
ret = SubGhzProtocolStatusErrorParserOthers;
break;
}
@@ -444,7 +490,7 @@ LevelDuration subghz_protocol_encoder_kia_v5_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
@@ -584,7 +630,7 @@ void subghz_protocol_decoder_kia_v5_feed(void* context, bool level, uint32_t dur
instance->decoder.decode_count_bit = instance->generic.data_count_bit;
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(kia_v5_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(4);
@@ -675,10 +721,13 @@ SubGhzProtocolStatus
furi_assert(context);
SubGhzProtocolDecoderKiaV5* instance = context;
SubGhzProtocolStatus ret = subghz_block_generic_deserialize_check_count_bit(
&instance->generic,
flipper_format,
subghz_protocol_kia_v5_const.min_count_bit_for_found);
SubGhzProtocolStatus ret = subghz_block_generic_deserialize(&instance->generic, flipper_format);
if((ret == SubGhzProtocolStatusOk) &&
(instance->generic.data_count_bit <
subghz_protocol_kia_v5_const.min_count_bit_for_found)) {
ret = SubGhzProtocolStatusErrorParserBitCount;
}
if(ret == SubGhzProtocolStatusOk) {
flipper_format_rewind(flipper_format);
@@ -715,7 +764,7 @@ SubGhzProtocolStatus
instance->generic.cnt = mixer_decode(encrypted);
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(kia_v5_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(4);
}
+21 -8
View File
@@ -717,8 +717,10 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v6_deserialize(void* context, F
}
subghz_custom_btn_set_max(4);
// Incrementa cnt
if(instance->generic.cnt < 0xFFFFFFFF) {
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt;
} else if(instance->generic.cnt < 0xFFFFFFFF) {
if((instance->generic.cnt + furi_hal_subghz_get_rolling_counter_mult()) > 0xFFFFFFFF) {
instance->generic.cnt = 0;
} else {
@@ -728,11 +730,15 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v6_deserialize(void* context, F
instance->generic.cnt = 0;
}
// Risolvi btn dal tasto premuto (identico a Suzuki)
instance->generic.btn = kia_v6_custom_to_btn(
subghz_custom_btn_get() == SUBGHZ_CUSTOM_BTN_OK
? subghz_custom_btn_get_original()
: subghz_custom_btn_get());
uint8_t override_btn = 0;
if(subghz_block_generic_global_button_override_get(&override_btn)) {
instance->generic.btn = override_btn & 0x0F;
} else {
instance->generic.btn = kia_v6_custom_to_btn(
subghz_custom_btn_get() == SUBGHZ_CUSTOM_BTN_OK
? subghz_custom_btn_get_original()
: subghz_custom_btn_get());
}
kia_v6_encoder_build_upload(instance);
@@ -766,6 +772,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_kia_v6_deserialize(void* context, F
uint32_t file_key3 = file_part3;
flipper_format_update_uint32(flipper_format, "Key_4", &file_key3, 1);
uint32_t btn_to_write = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &btn_to_write, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
} while(false);
@@ -791,7 +804,7 @@ LevelDuration subghz_protocol_encoder_kia_v6_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
instance->encoder.front++;
if(instance->encoder.front >= instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
return ret;
+1 -1
View File
@@ -431,7 +431,7 @@ LevelDuration kia_protocol_encoder_v7_yield(void* context) {
LevelDuration duration = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+1 -1
View File
@@ -969,7 +969,7 @@ LevelDuration subghz_protocol_encoder_land_rover_v0_yield(void* context) {
if(instance->encoder.front >= instance->encoder.size_upload) {
/* One full repetition done; count it down */
if(instance->encoder.repeat > 0) {
if(instance->encoder.repeat > 0 && !subghz_block_generic_global.endless_tx) {
instance->encoder.repeat--;
}
instance->encoder.front = 0;
+1 -1
View File
@@ -499,7 +499,7 @@ LevelDuration subghz_protocol_encoder_mazda_v0_yield(void* context) {
LevelDuration out = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+1 -1
View File
@@ -193,7 +193,7 @@ LevelDuration subghz_protocol_encoder_mitsubishi_v0_yield(void* context) {
if(!instance->encoder.is_running || instance->encoder.repeat == 0) return level_duration_reset();
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
return ret;
+49 -12
View File
@@ -422,12 +422,14 @@ SubGhzProtocolStatus subghz_protocol_decoder_porsche_cayenne_deserialize(
return ret;
}
static uint8_t porsche_cayenne_btn_to_custom(uint8_t btn);
void subghz_protocol_decoder_porsche_cayenne_get_string(void* context, FuriString* output) {
furi_check(context);
SubGhzProtocolDecoderPorscheCayenne* instance = context;
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(porsche_cayenne_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(4);
@@ -456,16 +458,37 @@ void subghz_protocol_decoder_porsche_cayenne_get_string(void* context, FuriStrin
// ENCODER
// =============================================================================
static uint8_t porsche_cayenne_custom_to_btn(uint8_t custom_btn, uint8_t fallback_btn) {
switch(custom_btn) {
case SUBGHZ_CUSTOM_BTN_UP: return 0x01; // Lock
case SUBGHZ_CUSTOM_BTN_DOWN: return 0x02; // Unlock
case SUBGHZ_CUSTOM_BTN_LEFT: return 0x04; // Trunk
case SUBGHZ_CUSTOM_BTN_RIGHT: return 0x08; // Open
default: return fallback_btn;
}
}
static uint8_t porsche_cayenne_btn_to_custom(uint8_t btn) {
switch(btn) {
case 0x01: return SUBGHZ_CUSTOM_BTN_UP;
case 0x02: return SUBGHZ_CUSTOM_BTN_DOWN;
case 0x04: return SUBGHZ_CUSTOM_BTN_LEFT;
case 0x08: return SUBGHZ_CUSTOM_BTN_RIGHT;
default: return SUBGHZ_CUSTOM_BTN_OK;
}
}
static uint8_t porsche_cayenne_get_btn_code(void) {
uint8_t override_btn = 0;
if(subghz_block_generic_global_button_override_get(&override_btn)) {
return override_btn;
}
uint8_t custom_btn = subghz_custom_btn_get();
uint8_t original_btn = subghz_custom_btn_get_original();
uint8_t original_btn = porsche_cayenne_custom_to_btn(
subghz_custom_btn_get_original(), 0x01);
if(custom_btn == SUBGHZ_CUSTOM_BTN_OK) return original_btn;
// Map d-pad buttons to common VAG key codes
if(custom_btn == SUBGHZ_CUSTOM_BTN_UP) return 0x01; // Lock
if(custom_btn == SUBGHZ_CUSTOM_BTN_DOWN) return 0x02; // Unlock
if(custom_btn == SUBGHZ_CUSTOM_BTN_LEFT) return 0x04; // Trunk
if(custom_btn == SUBGHZ_CUSTOM_BTN_RIGHT) return 0x08; // Open
return original_btn;
return porsche_cayenne_custom_to_btn(custom_btn, original_btn);
}
static void porsche_cayenne_build_upload(SubGhzProtocolEncoderPorscheCayenne* instance) {
@@ -483,6 +506,7 @@ static void porsche_cayenne_build_upload(SubGhzProtocolEncoderPorscheCayenne* in
uint32_t serial = instance->generic.serial & 0xFFFFFF;
uint8_t btn = porsche_cayenne_get_btn_code();
uint16_t cnt = (uint16_t)instance->generic.cnt;
instance->generic.btn = btn;
size_t idx = 0;
LevelDuration* up = instance->encoder.upload;
@@ -522,8 +546,9 @@ static void porsche_cayenne_build_upload(SubGhzProtocolEncoderPorscheCayenne* in
instance->encoder.size_upload = idx;
instance->encoder.front = 0;
// Advance stored counter by 4 (one per frame)
instance->generic.cnt = (uint16_t)(cnt + 4);
if(furi_hal_subghz_get_rolling_counter_mult() != 0) {
instance->generic.cnt = (uint16_t)(cnt + 4);
}
}
void* subghz_protocol_encoder_porsche_cayenne_alloc(SubGhzEnvironment* environment) {
@@ -582,9 +607,14 @@ SubGhzProtocolStatus subghz_protocol_encoder_porsche_cayenne_deserialize(
break;
}
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
}
// Apply custom button if set
if(subghz_custom_btn_get_original() == 0) {
subghz_custom_btn_set_original(instance->generic.btn);
subghz_custom_btn_set_original(porsche_cayenne_btn_to_custom(instance->generic.btn));
}
subghz_custom_btn_set_max(4);
@@ -596,6 +626,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_porsche_cayenne_deserialize(
uint32_t new_cnt = instance->generic.cnt;
flipper_format_insert_or_update_uint32(flipper_format, "Counter", &new_cnt, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &new_cnt, 1);
flipper_format_rewind(flipper_format);
uint32_t new_btn = instance->generic.btn;
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &new_btn, 1);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
} while(false);
@@ -621,7 +658,7 @@ LevelDuration subghz_protocol_encoder_porsche_cayenne_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+62 -19
View File
@@ -70,17 +70,38 @@ static const char* psa_button_name(uint8_t btn) {
}
}
static uint8_t psa_btn_to_custom(uint8_t btn) {
switch(btn) {
case 0x0:
return SUBGHZ_CUSTOM_BTN_UP;
case 0x1:
return SUBGHZ_CUSTOM_BTN_DOWN;
case 0x2:
return SUBGHZ_CUSTOM_BTN_LEFT;
default:
return SUBGHZ_CUSTOM_BTN_OK;
}
}
static uint8_t psa_custom_to_btn(uint8_t custom_btn, uint8_t fallback_btn) {
switch(custom_btn) {
case SUBGHZ_CUSTOM_BTN_UP:
return 0x0; // Lock
case SUBGHZ_CUSTOM_BTN_DOWN:
return 0x1; // Unlock
case SUBGHZ_CUSTOM_BTN_LEFT:
case SUBGHZ_CUSTOM_BTN_RIGHT:
return 0x2; // Trunk
default:
return fallback_btn;
}
}
static uint8_t psa_get_btn_code(void) {
uint8_t custom_btn = subghz_custom_btn_get();
uint8_t original_raw = subghz_custom_btn_get_original();
// 0xFF is sentinel for PSA btn 0x0 (Lock)
uint8_t original_btn = (original_raw == 0xFF) ? 0x0 : original_raw;
if(custom_btn == SUBGHZ_CUSTOM_BTN_OK) return original_btn;
if(custom_btn == SUBGHZ_CUSTOM_BTN_UP) return 0x0; // Lock
if(custom_btn == SUBGHZ_CUSTOM_BTN_DOWN) return 0x1; // Unlock
if(custom_btn == SUBGHZ_CUSTOM_BTN_LEFT) return 0x2; // Trunk
if(custom_btn == SUBGHZ_CUSTOM_BTN_RIGHT) return 0x2; // Trunk
return original_btn;
uint8_t original_btn = psa_custom_to_btn(subghz_custom_btn_get_original(), 0x0);
if(custom_btn == SUBGHZ_CUSTOM_BTN_OK) return original_btn;
return psa_custom_to_btn(custom_btn, original_btn);
}
typedef enum {
@@ -1390,7 +1411,7 @@ void subghz_protocol_decoder_psa_get_string(void* context, FuriString* output) {
if(instance->decrypted == 0x50 && instance->decrypted_type != 0) {
// Always update original button when loading a new file
subghz_custom_btn_set_original(instance->generic.btn == 0 ? 0xFF : instance->generic.btn);
subghz_custom_btn_set_original(psa_btn_to_custom(instance->generic.btn));
subghz_custom_btn_set_max(4);
uint8_t display_btn = psa_get_btn_code();
if(instance->decrypted_type == 0x23) {
@@ -1762,7 +1783,10 @@ SubGhzProtocolStatus subghz_protocol_encoder_psa_deserialize(void* context, Flip
counter = (counter << 4) | nibble;
}
} else {
has_decrypted_data = false;
flipper_format_rewind(flipper_format);
if(!flipper_format_read_uint32(flipper_format, "Cnt", &counter, 1)) {
has_decrypted_data = false;
}
}
flipper_format_rewind(flipper_format);
@@ -1779,7 +1803,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_psa_deserialize(void* context, Flip
}
button = (uint8_t)(btn_val & 0xFF);
} else {
has_decrypted_data = false;
uint32_t btn_val = 0;
flipper_format_rewind(flipper_format);
if(flipper_format_read_uint32(flipper_format, "Btn", &btn_val, 1)) {
button = (uint8_t)(btn_val & 0xFF);
} else {
has_decrypted_data = false;
}
}
flipper_format_rewind(flipper_format);
@@ -1849,14 +1879,20 @@ SubGhzProtocolStatus subghz_protocol_encoder_psa_deserialize(void* context, Flip
// Setup custom button system
// Save original button from FILE before any d-pad remapping
uint8_t file_btn = button; // 'button' was read directly from file above
subghz_custom_btn_set_original(file_btn == 0 ? 0xFF : file_btn);
subghz_custom_btn_set_original(psa_btn_to_custom(file_btn));
subghz_custom_btn_set_max(4);
instance->button = psa_get_btn_code();
if(!subghz_block_generic_global_button_override_get(&instance->button)) {
instance->button = psa_get_btn_code();
}
FURI_LOG_I("PSA_ENC", "file_btn=%02X custom=%02X result=%02X orig=%02X", file_btn, subghz_custom_btn_get(), instance->button, subghz_custom_btn_get_original());
// Increment counter
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->counter = (instance->counter + mult) & 0xFFFFFFFF;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->counter = override_cnt;
} else {
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->counter = (instance->counter + mult) & 0xFFFFFFFF;
}
psa_encoder_build_upload(instance);
@@ -1879,7 +1915,12 @@ SubGhzProtocolStatus subghz_protocol_encoder_psa_deserialize(void* context, Flip
(unsigned int)((instance->counter >> 8) & 0xFF),
(unsigned int)(instance->counter & 0xFF));
}
flipper_format_update_string_cstr(flipper_format, "Cnt", cnt_str);
flipper_format_insert_or_update_string_cstr(flipper_format, "Cnt", cnt_str);
char btn_str[8];
snprintf(btn_str, sizeof(btn_str), "%02X", (unsigned int)instance->button);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_string_cstr(flipper_format, "Btn", btn_str);
char key_str[32];
uint64_t key1 = ((uint64_t)instance->key1_high << 32) | instance->key1_low;
@@ -1892,6 +1933,7 @@ SubGhzProtocolStatus subghz_protocol_encoder_psa_deserialize(void* context, Flip
(unsigned int)((key1 >> 16) & 0xFF),
(unsigned int)((key1 >> 8) & 0xFF),
(unsigned int)(key1 & 0xFF));
flipper_format_rewind(flipper_format);
flipper_format_update_string_cstr(flipper_format, "Key", key_str);
char key2_str[32];
@@ -1899,6 +1941,7 @@ SubGhzProtocolStatus subghz_protocol_encoder_psa_deserialize(void* context, Flip
0, 0, 0, 0, 0, 0,
(unsigned int)((instance->key2_low >> 8) & 0xFF),
(unsigned int)(instance->key2_low & 0xFF));
flipper_format_rewind(flipper_format);
flipper_format_update_string_cstr(flipper_format, "Key_2", key2_str);
@@ -1932,7 +1975,7 @@ LevelDuration subghz_protocol_encoder_psa_yield(void* context) {
instance->encoder.front++;
if(instance->encoder.front >= instance->encoder.size_upload) {
instance->encoder.front = 0;
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
if(instance->encoder.repeat <= 0) instance->is_running = false;
}
return ret;
+20 -7
View File
@@ -307,10 +307,16 @@ static bool subghz_protocol_encoder_scher_khan_get_upload(
// For 51-bit dynamic: rebuild data with new button and incremented counter
if(instance->generic.data_count_bit == 51) {
if((instance->generic.cnt + 1) > 0xFFFF) {
instance->generic.cnt = 0;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
} else {
instance->generic.cnt += 1;
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
if((instance->generic.cnt + mult) > 0xFFFF) {
instance->generic.cnt = 0;
} else {
instance->generic.cnt += mult;
}
}
uint64_t upper = instance->generic.data & 0x7FFFFFFF0000ULL;
@@ -320,10 +326,16 @@ static bool subghz_protocol_encoder_scher_khan_get_upload(
}
if(instance->generic.data_count_bit == 57) {
if((instance->generic.cnt + 1) > 0xFFFF) {
instance->generic.cnt = 0;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
} else {
instance->generic.cnt += 1;
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
if((instance->generic.cnt + mult) > 0xFFFF) {
instance->generic.cnt = 0;
} else {
instance->generic.cnt += mult;
}
}
uint8_t kb[7];
@@ -581,6 +593,7 @@ SubGhzProtocolStatus
instance->protocol_name = pname;
uint8_t selected_btn = scher_khan_get_btn_code(instance->generic.btn);
subghz_block_generic_global_button_override_get(&selected_btn);
FURI_LOG_I(TAG,
"Building upload: original_btn=0x%02X, selected_btn=0x%02X, bits=%d",
@@ -622,7 +635,7 @@ LevelDuration subghz_protocol_encoder_scher_khan_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+29 -4
View File
@@ -282,10 +282,16 @@ static bool subghz_protocol_encoder_sheriff_cfm_get_upload(
uint8_t btn) {
furi_check(instance);
if((instance->generic.cnt + 1) > 0xFFFF) {
instance->generic.cnt = 0;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
} else {
instance->generic.cnt += 1;
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
if((instance->generic.cnt + mult) > 0xFFFF) {
instance->generic.cnt = 0;
} else {
instance->generic.cnt += mult;
}
}
uint32_t fix = (uint32_t)(instance->generic.data >> 32);
@@ -388,11 +394,30 @@ SubGhzProtocolStatus
subghz_custom_btn_set_max(4);
uint8_t selected_btn = cfm_get_btn_code(instance->generic.btn);
subghz_block_generic_global_button_override_get(&selected_btn);
if(!subghz_protocol_encoder_sheriff_cfm_get_upload(instance, selected_btn)) {
break;
}
uint8_t key_data[sizeof(uint64_t)] = {0};
for(size_t i = 0; i < sizeof(uint64_t); i++) {
key_data[sizeof(uint64_t) - i - 1] = (instance->generic.data >> (i * 8)) & 0xFF;
}
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_hex(flipper_format, "Key", key_data, sizeof(uint64_t));
uint32_t model = (uint32_t)instance->model;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Model", &model, 1);
uint32_t btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &btn, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
instance->encoder.front = 0;
ret = SubGhzProtocolStatusOk;
@@ -418,7 +443,7 @@ LevelDuration subghz_protocol_encoder_sheriff_cfm_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+12 -4
View File
@@ -206,11 +206,18 @@ void subghz_protocol_encoder_star_line_free(void* context) {
static bool
subghz_protocol_star_line_gen_data(SubGhzProtocolEncoderStarLine* instance, uint8_t btn) {
if((instance->generic.cnt + 1) > 0xFFFF) {
instance->generic.cnt = 0;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFF;
} else {
instance->generic.cnt += 1;
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
if((instance->generic.cnt + mult) > 0xFFFF) {
instance->generic.cnt = 0;
} else {
instance->generic.cnt += mult;
}
}
instance->generic.btn = btn;
uint32_t fix = btn << 24 | instance->generic.serial;
uint32_t decrypt = btn << 24 | (instance->generic.serial & 0xFF) << 16 | instance->generic.cnt;
@@ -520,6 +527,7 @@ SubGhzProtocolStatus
&instance->generic, instance->keystore, &instance->manufacture_name);
uint8_t selected_btn = star_line_get_btn_code(instance->generic.btn);
subghz_block_generic_global_button_override_get(&selected_btn);
subghz_protocol_encoder_star_line_get_upload(instance, selected_btn);
subghz_protocol_encoder_star_line_serialize(instance, flipper_format);
@@ -556,7 +564,7 @@ LevelDuration subghz_protocol_encoder_star_line_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+16 -3
View File
@@ -527,7 +527,7 @@ LevelDuration subghz_protocol_encoder_subaru_yield(void* context) {
LevelDuration ret = instance->encoder.upload[instance->encoder.front];
if(++instance->encoder.front == instance->encoder.size_upload) {
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
@@ -609,10 +609,16 @@ SubGhzProtocolStatus subghz_protocol_encoder_subaru_deserialize(void* context, F
}
uint8_t new_button = subaru_get_button_code(selected_custom_btn);
subghz_block_generic_global_button_override_get(&new_button);
instance->button = new_button;
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->count = (instance->count + mult) & 0xFFFF;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->count = override_cnt & 0xFFFF;
} else {
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->count = (instance->count + mult) & 0xFFFF;
}
b[0] = (b[0] & 0xF0) | (instance->button & 0x0F);
@@ -643,6 +649,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_subaru_deserialize(void* context, F
ret = SubGhzProtocolStatusErrorParserKey;
break;
}
uint32_t temp_btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &temp_btn, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
instance->encoder.is_running = true;
ret = SubGhzProtocolStatusOk;
+16 -3
View File
@@ -400,13 +400,19 @@ SubGhzProtocolStatus subghz_protocol_encoder_suzuki_deserialize(void *context, F
}
subghz_custom_btn_set_max(4);
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->generic.cnt = (instance->generic.cnt + mult) & 0xFFFFF;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->generic.cnt = override_cnt & 0xFFFFF;
} else {
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->generic.cnt = (instance->generic.cnt + mult) & 0xFFFFF;
}
uint8_t selected = subghz_custom_btn_get() == SUBGHZ_CUSTOM_BTN_OK ?
subghz_custom_btn_get_original() :
subghz_custom_btn_get();
uint8_t btn = suzuki_custom_to_btn(selected);
subghz_block_generic_global_button_override_get(&btn);
instance->generic.btn = btn;
uint64_t new_data = 0;
@@ -434,6 +440,13 @@ SubGhzProtocolStatus subghz_protocol_encoder_suzuki_deserialize(void *context, F
break;
}
uint32_t temp_btn = instance->generic.btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &temp_btn, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->generic.cnt, 1);
size_t preamble_count = SUZUKI_ENCODER_PREAMBLE_COUNT;
size_t bit_count = 64;
instance->encoder.size_upload = (preamble_count * 2) + (bit_count * 2) + 1;
@@ -494,7 +507,7 @@ LevelDuration subghz_protocol_encoder_suzuki_yield(void *context)
if (++instance->encoder.front == instance->encoder.size_upload)
{
instance->encoder.repeat--;
if(!subghz_block_generic_global.endless_tx) instance->encoder.repeat--;
instance->encoder.front = 0;
}
+15 -3
View File
@@ -1358,7 +1358,7 @@ LevelDuration subghz_protocol_encoder_vag_yield(void* context) {
if(instance->front >= instance->size_upload) {
instance->front = 0;
instance->repeat--;
if(!subghz_block_generic_global.endless_tx) instance->repeat--;
}
return ret;
@@ -1443,12 +1443,18 @@ SubGhzProtocolStatus
}
uint8_t new_btn = vag_custom_to_btn(selected_custom, instance->btn);
subghz_block_generic_global_button_override_get(&new_btn);
instance->btn = new_btn;
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->cnt = (instance->cnt + mult) & 0xFFFFFF;
uint32_t override_cnt = 0;
if(subghz_block_generic_global_counter_override_get(&override_cnt)) {
instance->cnt = override_cnt & 0xFFFFFF;
} else {
uint32_t mult = furi_hal_subghz_get_rolling_counter_mult();
instance->cnt = (instance->cnt + mult) & 0xFFFFFF;
}
uint8_t type_byte = (uint8_t)(instance->key1_high >> 24);
if(instance->vag_type == 1 && type_byte == 0x00) {
@@ -1518,6 +1524,12 @@ SubGhzProtocolStatus
flipper_format_insert_or_update_uint32(flipper_format, "Type", &type32, 1);
}
uint32_t btn32 = instance->btn;
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Btn", &btn32, 1);
flipper_format_rewind(flipper_format);
flipper_format_insert_or_update_uint32(flipper_format, "Cnt", &instance->cnt, 1);
instance->repeat = 1;
+32 -4
View File
@@ -3,7 +3,9 @@ import hashlib
import os
import struct
import subprocess
import shutil
import tempfile
import time
from collections import defaultdict
from dataclasses import dataclass
@@ -16,6 +18,21 @@ from flipper.app import App
VERSION = 1
def replace_file_with_retry(source: str, target: str, attempts: int = 50) -> None:
for attempt in range(attempts):
try:
os.replace(source, target)
return
except PermissionError:
if attempt == attempts - 1:
raise
time.sleep(0.1)
except OSError:
shutil.copy2(source, target)
os.remove(source)
return
@dataclass
class RelData:
section: int
@@ -135,11 +152,16 @@ class Main(App):
RelSection(section_name, section.name, unique_relocations)
)
with tempfile.TemporaryDirectory() as temp_dir:
for section in sections:
with tempfile.TemporaryDirectory(dir=os.path.dirname(os.path.abspath(fap_path))) as temp_dir:
current_fap_path = fap_path
for section_index, section in enumerate(sections):
data = serialize_relsection_data(section.data)
hash_name = hashlib.md5(section.name.encode()).hexdigest()
filename = f"{temp_dir}/{hash_name}.bin"
filename = os.path.join(temp_dir, f"{section_index}_{hash_name}.bin")
patched_fap_path = os.path.join(
temp_dir, f"{section_index}_{hash_name}.fap"
)
if os.path.isfile(filename):
self.logger.error(f"File {filename} already exists")
@@ -153,7 +175,8 @@ class Main(App):
objcopy_path,
"--add-section",
f"{section.name}={filename}",
fap_path,
current_fap_path,
patched_fap_path,
],
check=True,
)
@@ -162,6 +185,11 @@ class Main(App):
self.logger.error("objcopy failed")
return 1
current_fap_path = patched_fap_path
if current_fap_path != fap_path:
replace_file_with_retry(current_fap_path, fap_path)
return 0