mirror of
https://github.com/D4C1-Labs/Flipper-ARF.git
synced 2026-07-16 19:42:06 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4fe2d48c1 | ||
|
|
bc59edff17 | ||
|
|
63d951dc38 | ||
|
|
f1422cb701 | ||
|
|
f7b04f1382 | ||
|
|
7286b5c529 | ||
|
|
6a49fd89a1 | ||
|
|
c4378c2e20 | ||
|
|
6e5ba9ec22 | ||
|
|
502570a18b | ||
|
|
e606c5b24f | ||
|
|
b34a73b08a | ||
|
|
069a42d697 | ||
|
|
2a182a2f5c | ||
|
|
b13f42f8bc |
@@ -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 {
|
||||
|
||||
@@ -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, ¤t, ¤t_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);
|
||||
|
||||
@@ -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 `0–4`. `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), 0–32 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), 0–32 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 1–2 streaming slots.
|
||||
post-launch heap with room for a few streaming pages.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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.
BIN
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;
|
||||
};
|
||||
@@ -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 */
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
+4
-10
@@ -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 */
|
||||
+9
-1
@@ -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();
|
||||
+19
-1
@@ -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) {
|
||||
+23
-8
@@ -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;
|
||||
+41
-27
@@ -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"
|
||||
+15
-1
@@ -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;
|
||||
|
||||
+9
@@ -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;
|
||||
|
||||
+6
-8
@@ -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();
|
||||
}
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
+6
@@ -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 */
|
||||
+15
-8
@@ -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
@@ -1,4 +1,5 @@
|
||||
#include "cpu.h"
|
||||
#include "gameboy.h" /* inline hot-path definitions (operand fetch) */
|
||||
/* clang-format off */
|
||||
|
||||
/**
|
||||
+15
-6
@@ -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);
|
||||
}
|
||||
+18
-4
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user