|
|
@@ -1,81 +1,262 @@
|
|
|
#include "virtual_portal.h"
|
|
|
|
|
|
+#include <furi_hal.h>
|
|
|
+#include <stm32wbxx_ll_dma.h>
|
|
|
+
|
|
|
+#include "audio/wav_player_hal.h"
|
|
|
+#include "string.h"
|
|
|
+
|
|
|
#define TAG "VirtualPortal"
|
|
|
|
|
|
#define BLOCK_SIZE 16
|
|
|
|
|
|
-static const NotificationSequence pof_sequence_cyan = {
|
|
|
- &message_blink_start_10,
|
|
|
- &message_blink_set_color_cyan,
|
|
|
+#define PORTAL_SIDE_RING 0
|
|
|
+#define PORTAL_SIDE_RIGHT 0
|
|
|
+#define PORTAL_SIDE_TRAP 1
|
|
|
+#define PORTAL_SIDE_LEFT 2
|
|
|
+
|
|
|
+const NotificationSequence sequence_set_backlight = {
|
|
|
+ &message_display_backlight_on,
|
|
|
+ &message_do_not_reset,
|
|
|
+ NULL,
|
|
|
+};
|
|
|
+const NotificationSequence sequence_set_leds = {
|
|
|
+ &message_red_0,
|
|
|
+ &message_blue_0,
|
|
|
+ &message_green_0,
|
|
|
+ &message_do_not_reset,
|
|
|
NULL,
|
|
|
};
|
|
|
|
|
|
+static float lerp(float start, float end, float t) {
|
|
|
+ return start + (end - start) * t;
|
|
|
+}
|
|
|
+
|
|
|
+static void wav_player_dma_isr(void* ctx) {
|
|
|
+ VirtualPortal* virtual_portal = (VirtualPortal*)ctx;
|
|
|
+ // half of transfer
|
|
|
+ if (LL_DMA_IsActiveFlag_HT1(DMA1)) {
|
|
|
+ LL_DMA_ClearFlag_HT1(DMA1);
|
|
|
+ // fill first half of buffer
|
|
|
+ for (int i = 0; i < SAMPLES_COUNT / 2; i++) {
|
|
|
+ if (!virtual_portal->count) {
|
|
|
+ virtual_portal->audio_buffer[i] = 0;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ virtual_portal->audio_buffer[i] = *virtual_portal->tail;
|
|
|
+ if (++virtual_portal->tail == virtual_portal->end) {
|
|
|
+ virtual_portal->tail = virtual_portal->current_audio_buffer;
|
|
|
+ }
|
|
|
+ virtual_portal->count--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // transfer complete
|
|
|
+ if (LL_DMA_IsActiveFlag_TC1(DMA1)) {
|
|
|
+ LL_DMA_ClearFlag_TC1(DMA1);
|
|
|
+ // fill second half of buffer
|
|
|
+ for (int i = SAMPLES_COUNT / 2; i < SAMPLES_COUNT; i++) {
|
|
|
+ if (!virtual_portal->count) {
|
|
|
+ virtual_portal->audio_buffer[i] = 0;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ virtual_portal->audio_buffer[i] = *virtual_portal->tail;
|
|
|
+ if (++virtual_portal->tail == virtual_portal->end) {
|
|
|
+ virtual_portal->tail = virtual_portal->current_audio_buffer;
|
|
|
+ }
|
|
|
+ virtual_portal->count--;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void virtual_portal_tick(void* ctx) {
|
|
|
+ VirtualPortal* virtual_portal = (VirtualPortal*)ctx;
|
|
|
+ (void)virtual_portal;
|
|
|
+ VirtualPortalLed* led = &virtual_portal->right;
|
|
|
+ if (!led->running) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ uint32_t elapsed = furi_get_tick() - led->start_time;
|
|
|
+ if (elapsed < led->delay) {
|
|
|
+ float t_phase = fminf((float)elapsed / (float)led->delay, 1);
|
|
|
+
|
|
|
+ if (led->two_phase) {
|
|
|
+ if (led->current_phase == 0) {
|
|
|
+ // Phase 1: Increase channels that need to go up, hold others constant
|
|
|
+ if (led->target_r > led->last_r) {
|
|
|
+ led->r = lerp(led->last_r, led->target_r, t_phase);
|
|
|
+ }
|
|
|
+ if (led->target_g > led->last_g) {
|
|
|
+ led->g = lerp(led->last_g, led->target_g, t_phase);
|
|
|
+ }
|
|
|
+ if (led->target_b > led->last_b) {
|
|
|
+ led->b = lerp(led->last_b, led->target_b, t_phase);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Phase 2: Decrease channels that need to go down
|
|
|
+ if (led->target_r < led->last_r) {
|
|
|
+ led->r = lerp(led->last_r, led->target_r, t_phase);
|
|
|
+ }
|
|
|
+ if (led->target_g < led->last_g) {
|
|
|
+ led->g = lerp(led->last_g, led->target_g, t_phase);
|
|
|
+ }
|
|
|
+ if (led->target_b < led->last_b) {
|
|
|
+ led->b = lerp(led->last_b, led->target_b, t_phase);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Simple one-phase transition: all channels change together
|
|
|
+ led->r = lerp(led->last_r, led->target_r, t_phase);
|
|
|
+ led->g = lerp(led->last_g, led->target_g, t_phase);
|
|
|
+ led->b = lerp(led->last_b, led->target_b, t_phase);
|
|
|
+ }
|
|
|
+
|
|
|
+ furi_hal_light_set(LightRed, led->r);
|
|
|
+ furi_hal_light_set(LightGreen, led->g);
|
|
|
+ furi_hal_light_set(LightBlue, led->b);
|
|
|
+ } else if (led->two_phase && led->current_phase == 0) {
|
|
|
+ // Move to phase 2 - save the current state as our "last" values for phase 2
|
|
|
+ led->last_r = led->r;
|
|
|
+ led->last_g = led->g;
|
|
|
+ led->last_b = led->b;
|
|
|
+ led->start_time = furi_get_tick();
|
|
|
+ led->current_phase++;
|
|
|
+ } else {
|
|
|
+ // Transition complete - set final values
|
|
|
+ led->r = led->target_r;
|
|
|
+ led->g = led->target_g;
|
|
|
+ led->b = led->target_b;
|
|
|
+ furi_hal_light_set(LightRed, led->r);
|
|
|
+ furi_hal_light_set(LightGreen, led->g);
|
|
|
+ furi_hal_light_set(LightBlue, led->b);
|
|
|
+ led->running = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void queue_led_command(VirtualPortal* virtual_portal, int side, uint8_t r, uint8_t g, uint8_t b, uint16_t duration) {
|
|
|
+ VirtualPortalLed* led = &virtual_portal->left;
|
|
|
+ switch (side) {
|
|
|
+ case PORTAL_SIDE_RIGHT:
|
|
|
+ led = &virtual_portal->right;
|
|
|
+ break;
|
|
|
+ case PORTAL_SIDE_TRAP:
|
|
|
+ led = &virtual_portal->trap;
|
|
|
+ break;
|
|
|
+ case PORTAL_SIDE_LEFT:
|
|
|
+ led = &virtual_portal->left;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Store current values as last values
|
|
|
+ led->last_r = led->r;
|
|
|
+ led->last_g = led->g;
|
|
|
+ led->last_b = led->b;
|
|
|
+
|
|
|
+ // Set target values
|
|
|
+ led->target_r = r;
|
|
|
+ led->target_g = g;
|
|
|
+ led->target_b = b;
|
|
|
+
|
|
|
+ if (duration) {
|
|
|
+ // Determine if we need a two-phase transition
|
|
|
+ bool increasing = (r > led->last_r) || (g > led->last_g) || (b > led->last_b);
|
|
|
+ bool decreasing = (r < led->last_r) || (g < led->last_g) || (b < led->last_b);
|
|
|
+ led->two_phase = increasing && decreasing;
|
|
|
+
|
|
|
+ // Set up transition parameters
|
|
|
+ led->start_time = furi_get_tick();
|
|
|
+ if (led->two_phase) {
|
|
|
+ // If two-phase, each phase gets half the duration
|
|
|
+ led->delay = duration / 2;
|
|
|
+ } else {
|
|
|
+ led->delay = duration;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Start in phase 0
|
|
|
+ led->current_phase = 0;
|
|
|
+ led->running = true;
|
|
|
+ } else {
|
|
|
+ // Immediate change, no transition
|
|
|
+ if (side == PORTAL_SIDE_RIGHT) {
|
|
|
+ led->r = r;
|
|
|
+ led->g = g;
|
|
|
+ led->b = b;
|
|
|
+ furi_hal_light_set(LightRed, r);
|
|
|
+ furi_hal_light_set(LightGreen, g);
|
|
|
+ furi_hal_light_set(LightBlue, b);
|
|
|
+ }
|
|
|
+ led->running = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
VirtualPortal* virtual_portal_alloc(NotificationApp* notifications) {
|
|
|
VirtualPortal* virtual_portal = malloc(sizeof(VirtualPortal));
|
|
|
virtual_portal->notifications = notifications;
|
|
|
|
|
|
- for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ notification_message(virtual_portal->notifications, &sequence_set_backlight);
|
|
|
+ notification_message(virtual_portal->notifications, &sequence_set_leds);
|
|
|
+
|
|
|
+ for (int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
virtual_portal->tokens[i] = pof_token_alloc();
|
|
|
}
|
|
|
virtual_portal->sequence_number = 0;
|
|
|
virtual_portal->active = false;
|
|
|
+ virtual_portal->volume = 20.0f;
|
|
|
+
|
|
|
+ virtual_portal->led_timer = furi_timer_alloc(virtual_portal_tick,
|
|
|
+ FuriTimerTypePeriodic, virtual_portal);
|
|
|
+ virtual_portal->head = virtual_portal->current_audio_buffer;
|
|
|
+ virtual_portal->tail = virtual_portal->current_audio_buffer;
|
|
|
+ virtual_portal->end = &virtual_portal->current_audio_buffer[SAMPLES_COUNT_BUFFERED];
|
|
|
+
|
|
|
+ furi_timer_start(virtual_portal->led_timer, 10);
|
|
|
+
|
|
|
+ if (furi_hal_speaker_acquire(1000)) {
|
|
|
+ wav_player_speaker_init(8000);
|
|
|
+ wav_player_dma_init((uint32_t)virtual_portal->audio_buffer, SAMPLES_COUNT);
|
|
|
+
|
|
|
+ furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, wav_player_dma_isr, virtual_portal);
|
|
|
+
|
|
|
+ wav_player_dma_start();
|
|
|
+ }
|
|
|
|
|
|
return virtual_portal;
|
|
|
}
|
|
|
|
|
|
+void virtual_portal_set_type(VirtualPortal* virtual_portal, PoFType type) {
|
|
|
+ virtual_portal->type = type;
|
|
|
+}
|
|
|
+
|
|
|
void virtual_portal_cleanup(VirtualPortal* virtual_portal) {
|
|
|
notification_message(virtual_portal->notifications, &sequence_reset_rgb);
|
|
|
notification_message(virtual_portal->notifications, &sequence_display_backlight_on);
|
|
|
}
|
|
|
|
|
|
void virtual_portal_free(VirtualPortal* virtual_portal) {
|
|
|
- for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ for (int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
pof_token_free(virtual_portal->tokens[i]);
|
|
|
virtual_portal->tokens[i] = NULL;
|
|
|
}
|
|
|
+ furi_timer_stop(virtual_portal->led_timer);
|
|
|
+ furi_timer_free(virtual_portal->led_timer);
|
|
|
+ if (furi_hal_speaker_is_mine()) {
|
|
|
+ furi_hal_speaker_release();
|
|
|
+ wav_player_speaker_stop();
|
|
|
+ wav_player_dma_stop();
|
|
|
+ }
|
|
|
+ wav_player_hal_deinit();
|
|
|
+ furi_hal_interrupt_set_isr(FuriHalInterruptIdDma1Ch1, NULL, NULL);
|
|
|
|
|
|
free(virtual_portal);
|
|
|
}
|
|
|
|
|
|
-NotificationMessage message_red = {
|
|
|
- .type = NotificationMessageTypeLedRed,
|
|
|
- .data.led.value = 0xFF,
|
|
|
-};
|
|
|
-NotificationMessage message_green = {
|
|
|
- .type = NotificationMessageTypeLedGreen,
|
|
|
- .data.led.value = 0xFF,
|
|
|
-};
|
|
|
-NotificationMessage message_blue = {
|
|
|
- .type = NotificationMessageTypeLedBlue,
|
|
|
- .data.led.value = 0xFF,
|
|
|
-};
|
|
|
-NotificationMessage message_display_backlight = {
|
|
|
- .type = NotificationMessageTypeLedDisplayBacklight,
|
|
|
- .data.led.value = 0xFF,
|
|
|
-};
|
|
|
-const NotificationSequence sequence_set_backlight = {
|
|
|
- &message_display_backlight,
|
|
|
- &message_do_not_reset,
|
|
|
- NULL,
|
|
|
-};
|
|
|
-const NotificationSequence sequence_set_leds = {
|
|
|
- &message_red,
|
|
|
- &message_green,
|
|
|
- &message_blue,
|
|
|
- &message_do_not_reset,
|
|
|
- NULL,
|
|
|
-};
|
|
|
-
|
|
|
-
|
|
|
-void virtaul_portal_set_leds(VirtualPortal* virtual_portal, uint8_t r, uint8_t g, uint8_t b) {
|
|
|
- message_red.data.led.value = r;
|
|
|
- message_green.data.led.value = g;
|
|
|
- message_blue.data.led.value = b;
|
|
|
- notification_message(virtual_portal->notifications, &sequence_set_leds);
|
|
|
+void virtual_portal_set_leds(uint8_t r, uint8_t g, uint8_t b) {
|
|
|
+ furi_hal_light_set(LightRed, r);
|
|
|
+ furi_hal_light_set(LightGreen, g);
|
|
|
+ furi_hal_light_set(LightBlue, b);
|
|
|
}
|
|
|
-void virtaul_portal_set_backlight(VirtualPortal* virtual_portal, uint8_t brightness) {
|
|
|
- message_display_backlight.data.led.value = brightness;
|
|
|
- notification_message(virtual_portal->notifications, &sequence_set_backlight);
|
|
|
+void virtual_portal_set_backlight(uint8_t brightness) {
|
|
|
+ furi_hal_light_set(LightBacklight, brightness);
|
|
|
}
|
|
|
|
|
|
void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_token) {
|
|
|
@@ -85,10 +266,10 @@ void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_toke
|
|
|
uint8_t empty[4] = {0, 0, 0, 0};
|
|
|
|
|
|
// first try to "reload" to the same slot it used before based on UID
|
|
|
- for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
- if(memcmp(virtual_portal->tokens[i]->UID, pof_token->UID, sizeof(pof_token->UID)) == 0) {
|
|
|
+ for (int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ if (memcmp(virtual_portal->tokens[i]->UID, pof_token->UID, sizeof(pof_token->UID)) == 0) {
|
|
|
// Found match
|
|
|
- if(virtual_portal->tokens[i]->loaded) {
|
|
|
+ if (virtual_portal->tokens[i]->loaded) {
|
|
|
// already loaded, no-op
|
|
|
return;
|
|
|
} else {
|
|
|
@@ -100,9 +281,9 @@ void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_toke
|
|
|
}
|
|
|
|
|
|
// otherwise load into first slot with no set UID
|
|
|
- if(target == NULL) {
|
|
|
- for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
- if(memcmp(virtual_portal->tokens[i]->UID, empty, sizeof(empty)) == 0) {
|
|
|
+ if (target == NULL) {
|
|
|
+ for (int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ if (memcmp(virtual_portal->tokens[i]->UID, empty, sizeof(empty)) == 0) {
|
|
|
FURI_LOG_D(TAG, "Found empty UID at index %d", i);
|
|
|
// By definition an empty UID slot would not be loaded, so I'm not checking. Fight me.
|
|
|
target = virtual_portal->tokens[i];
|
|
|
@@ -112,9 +293,9 @@ void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_toke
|
|
|
}
|
|
|
|
|
|
// Re-use first unloaded slot
|
|
|
- if(target == NULL) {
|
|
|
- for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
- if(virtual_portal->tokens[i]->loaded == false) {
|
|
|
+ if (target == NULL) {
|
|
|
+ for (int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ if (virtual_portal->tokens[i]->loaded == false) {
|
|
|
FURI_LOG_D(TAG, "Re-using previously used slot %d", i);
|
|
|
target = virtual_portal->tokens[i];
|
|
|
break;
|
|
|
@@ -122,7 +303,7 @@ void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_toke
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if(target == NULL) {
|
|
|
+ if (target == NULL) {
|
|
|
FURI_LOG_W(TAG, "Failed to find slot to token into");
|
|
|
return;
|
|
|
}
|
|
|
@@ -134,12 +315,14 @@ void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_toke
|
|
|
memcpy(target->dev_name, pof_token->dev_name, sizeof(pof_token->dev_name));
|
|
|
memcpy(target->UID, pof_token->UID, sizeof(pof_token->UID));
|
|
|
|
|
|
+ furi_string_set(target->load_path, pof_token->load_path);
|
|
|
+
|
|
|
const NfcDeviceData* data = nfc_device_get_data(pof_token->nfc_device, NfcProtocolMfClassic);
|
|
|
nfc_device_set_data(target->nfc_device, NfcProtocolMfClassic, data);
|
|
|
}
|
|
|
|
|
|
uint8_t virtual_portal_next_sequence(VirtualPortal* virtual_portal) {
|
|
|
- if(virtual_portal->sequence_number == 0xff) {
|
|
|
+ if (virtual_portal->sequence_number == 0xff) {
|
|
|
virtual_portal->sequence_number = 0;
|
|
|
}
|
|
|
return virtual_portal->sequence_number++;
|
|
|
@@ -147,7 +330,7 @@ uint8_t virtual_portal_next_sequence(VirtualPortal* virtual_portal) {
|
|
|
|
|
|
int virtual_portal_activate(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
|
|
|
FURI_LOG_D(TAG, "process %c", message[0]);
|
|
|
- virtual_portal->active = (message[1] == 1);
|
|
|
+ virtual_portal->active = message[1] != 0;
|
|
|
|
|
|
response[0] = message[0];
|
|
|
response[1] = message[1];
|
|
|
@@ -160,24 +343,20 @@ int virtual_portal_reset(VirtualPortal* virtual_portal, uint8_t* message, uint8_
|
|
|
FURI_LOG_D(TAG, "process %c", message[0]);
|
|
|
|
|
|
virtual_portal->active = false;
|
|
|
- //virtual_portal->sequence_number = 0;
|
|
|
- for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
- if(virtual_portal->tokens[i]->loaded) {
|
|
|
+ // virtual_portal->sequence_number = 0;
|
|
|
+ for (int i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ if (virtual_portal->tokens[i]->loaded) {
|
|
|
virtual_portal->tokens[i]->change = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
uint8_t index = 0;
|
|
|
response[index++] = 'R';
|
|
|
- response[index++] = 0x02;
|
|
|
- response[index++] = 0x19;
|
|
|
- //response[index++] = 0x0a;
|
|
|
- //response[index++] = 0x03;
|
|
|
- //response[index++] = 0x02;
|
|
|
- // https://github.com/tresni/PoweredPortals/wiki/USB-Protocols
|
|
|
- // Wii Wireless: 01 29 00 00
|
|
|
- // Wii Wired: 02 0a 03 02 (Giants: works)
|
|
|
- // Arduboy: 02 19 (Trap team: works)
|
|
|
+ response[index++] = 0x02; // Trap Team Xbox One
|
|
|
+ response[index++] = 0x27; // Trap Team Xbox One
|
|
|
+
|
|
|
+ // response[index++] = 0x02; // Swap Force 3DS
|
|
|
+ // response[index++] = 0x02; // Swap Force 3DS
|
|
|
return index;
|
|
|
}
|
|
|
|
|
|
@@ -185,12 +364,12 @@ int virtual_portal_status(VirtualPortal* virtual_portal, uint8_t* response) {
|
|
|
response[0] = 'S';
|
|
|
|
|
|
bool update = false;
|
|
|
- for(size_t i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
+ for (size_t i = 0; i < POF_TOKEN_LIMIT; i++) {
|
|
|
// Can't use bit_lib since it uses the opposite endian
|
|
|
- if(virtual_portal->tokens[i]->loaded) {
|
|
|
+ if (virtual_portal->tokens[i]->loaded) {
|
|
|
response[1 + i / 4] |= 1 << ((i % 4) * 2 + 0);
|
|
|
}
|
|
|
- if(virtual_portal->tokens[i]->change) {
|
|
|
+ if (virtual_portal->tokens[i]->change) {
|
|
|
update = true;
|
|
|
response[1 + i / 4] |= 1 << ((i % 4) * 2 + 1);
|
|
|
}
|
|
|
@@ -201,10 +380,10 @@ int virtual_portal_status(VirtualPortal* virtual_portal, uint8_t* response) {
|
|
|
response[6] = 1;
|
|
|
|
|
|
// Let me know when a status that actually has a change is sent
|
|
|
- if(update) {
|
|
|
+ if (update) {
|
|
|
char display[33] = {0};
|
|
|
memset(display, 0, sizeof(display));
|
|
|
- for(size_t i = 0; i < BLOCK_SIZE; i++) {
|
|
|
+ for (size_t i = 0; i < BLOCK_SIZE; i++) {
|
|
|
snprintf(display + (i * 2), sizeof(display), "%02x", response[i]);
|
|
|
}
|
|
|
FURI_LOG_I(TAG, "> S %s", display);
|
|
|
@@ -214,10 +393,7 @@ int virtual_portal_status(VirtualPortal* virtual_portal, uint8_t* response) {
|
|
|
}
|
|
|
|
|
|
int virtual_portal_send_status(VirtualPortal* virtual_portal, uint8_t* response) {
|
|
|
- if(virtual_portal->active) {
|
|
|
- // Disable while I work on RGB
|
|
|
- // notification_message(virtual_portal->notifications, &pof_sequence_cyan);
|
|
|
- UNUSED(pof_sequence_cyan);
|
|
|
+ if (virtual_portal->active) {
|
|
|
return virtual_portal_status(virtual_portal, response);
|
|
|
}
|
|
|
return 0;
|
|
|
@@ -225,8 +401,16 @@ int virtual_portal_send_status(VirtualPortal* virtual_portal, uint8_t* response)
|
|
|
|
|
|
// 4d01ff0000d0077d6c2a77a400000000
|
|
|
int virtual_portal_m(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
|
|
|
- virtual_portal->speaker = (message[1] == 1);
|
|
|
-
|
|
|
+ // Activate speaker for any non-zero value in the range 01-FF
|
|
|
+ virtual_portal->speaker = (message[1] != 0);
|
|
|
+ if (virtual_portal->speaker) {
|
|
|
+ if (!virtual_portal->playing_audio) {
|
|
|
+ wav_player_speaker_start();
|
|
|
+ }
|
|
|
+ virtual_portal->count = 0;
|
|
|
+ virtual_portal->head = virtual_portal->tail = virtual_portal->current_audio_buffer;
|
|
|
+ virtual_portal->playing_audio = true;
|
|
|
+ }
|
|
|
/*
|
|
|
char display[33] = {0};
|
|
|
for(size_t i = 0; i < BLOCK_SIZE; i++) {
|
|
|
@@ -237,13 +421,15 @@ int virtual_portal_m(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* r
|
|
|
|
|
|
size_t index = 0;
|
|
|
response[index++] = 'M';
|
|
|
- response[index++] = message[1];
|
|
|
+ // Always respond with 01 if active, 00 if not
|
|
|
+ response[index++] = virtual_portal->speaker ? 0x01 : 0x00;
|
|
|
response[index++] = 0x00;
|
|
|
- response[index++] = 0x19;
|
|
|
+ response[index++] = virtual_portal->m;
|
|
|
+ g72x_init_state(&virtual_portal->state);
|
|
|
return index;
|
|
|
}
|
|
|
|
|
|
-int virtual_portal_l(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
|
|
|
+int virtual_portal_l(VirtualPortal* virtual_portal, uint8_t* message) {
|
|
|
UNUSED(virtual_portal);
|
|
|
|
|
|
/*
|
|
|
@@ -255,32 +441,26 @@ int virtual_portal_l(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* r
|
|
|
FURI_LOG_I(TAG, "L %s", display);
|
|
|
*/
|
|
|
|
|
|
- uint8_t side = message[1]; // 0: left, 2: right
|
|
|
+ uint8_t side = message[1]; // 0: left, 2: right
|
|
|
uint8_t brightness = 0;
|
|
|
- switch(side) {
|
|
|
- case 0:
|
|
|
- case 2:
|
|
|
- virtaul_portal_set_leds(virtual_portal, message[2], message[3], message[4]);
|
|
|
- break;
|
|
|
- case 1:
|
|
|
- brightness = message[2];
|
|
|
- virtaul_portal_set_backlight(virtual_portal, brightness);
|
|
|
- break;
|
|
|
- case 3:
|
|
|
- brightness = 0xff;
|
|
|
- virtaul_portal_set_backlight(virtual_portal, brightness);
|
|
|
- break;
|
|
|
+ switch (side) {
|
|
|
+ case 0:
|
|
|
+ case 2:
|
|
|
+ queue_led_command(virtual_portal, side, message[2], message[3], message[4], 0);
|
|
|
+ break;
|
|
|
+ case 1:
|
|
|
+ brightness = message[2];
|
|
|
+ virtual_portal_set_backlight(brightness);
|
|
|
+ break;
|
|
|
+ case 3:
|
|
|
+ brightness = 0xff;
|
|
|
+ virtual_portal_set_backlight(brightness);
|
|
|
+ break;
|
|
|
}
|
|
|
-
|
|
|
- // https://marijnkneppers.dev/posts/reverse-engineering-skylanders-toys-to-life-mechanics/
|
|
|
- size_t index = 0;
|
|
|
- response[index++] = 'J';
|
|
|
- return index;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
int virtual_portal_j(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
|
|
|
- UNUSED(virtual_portal);
|
|
|
-
|
|
|
/*
|
|
|
char display[33] = {0};
|
|
|
memset(display, 0, sizeof(display));
|
|
|
@@ -290,21 +470,14 @@ int virtual_portal_j(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* r
|
|
|
FURI_LOG_I(TAG, "J %s", display);
|
|
|
*/
|
|
|
|
|
|
- uint8_t side = message[1]; // 0: left, 2: right
|
|
|
- uint8_t r = message[2]; // 0: left, 2: right
|
|
|
- uint8_t g = message[3]; // 0: left, 2: right
|
|
|
- uint8_t b = message[4]; // 0: left, 2: right
|
|
|
+ uint8_t side = message[1];
|
|
|
uint16_t delay = message[6] << 8 | message[5];
|
|
|
- switch(side) {
|
|
|
- case 0:
|
|
|
- case 2:
|
|
|
- virtaul_portal_set_leds(virtual_portal, r, g, b);
|
|
|
- break;
|
|
|
- }
|
|
|
+
|
|
|
+ queue_led_command(virtual_portal, side, message[2], message[3], message[4], delay);
|
|
|
|
|
|
// Delay response
|
|
|
// furi_delay_ms(delay); // causes issues
|
|
|
- UNUSED(delay);
|
|
|
+ // UNUSED(delay);
|
|
|
|
|
|
// https://marijnkneppers.dev/posts/reverse-engineering-skylanders-toys-to-life-mechanics/
|
|
|
size_t index = 0;
|
|
|
@@ -319,7 +492,7 @@ int virtual_portal_query(VirtualPortal* virtual_portal, uint8_t* message, uint8_
|
|
|
FURI_LOG_I(TAG, "Query %d %d", arrayIndex, blockNum);
|
|
|
|
|
|
PoFToken* pof_token = virtual_portal->tokens[arrayIndex];
|
|
|
- if(!pof_token->loaded) {
|
|
|
+ if (!pof_token->loaded) {
|
|
|
response[0] = 'Q';
|
|
|
response[1] = 0x00 | arrayIndex;
|
|
|
response[2] = blockNum;
|
|
|
@@ -342,13 +515,13 @@ int virtual_portal_write(VirtualPortal* virtual_portal, uint8_t* message, uint8_
|
|
|
int arrayIndex = index & 0x0f;
|
|
|
|
|
|
char display[33] = {0};
|
|
|
- for(size_t i = 0; i < BLOCK_SIZE; i++) {
|
|
|
+ for (size_t i = 0; i < BLOCK_SIZE; i++) {
|
|
|
snprintf(display + (i * 2), sizeof(display), "%02x", message[3 + i]);
|
|
|
}
|
|
|
FURI_LOG_I(TAG, "Write %d %d %s", arrayIndex, blockNum, display);
|
|
|
|
|
|
PoFToken* pof_token = virtual_portal->tokens[arrayIndex];
|
|
|
- if(!pof_token->loaded) {
|
|
|
+ if (!pof_token->loaded) {
|
|
|
response[0] = 'W';
|
|
|
response[1] = 0x00 | arrayIndex;
|
|
|
response[2] = blockNum;
|
|
|
@@ -366,47 +539,149 @@ int virtual_portal_write(VirtualPortal* virtual_portal, uint8_t* message, uint8_
|
|
|
|
|
|
mf_classic_free(data);
|
|
|
|
|
|
+ nfc_device_save(nfc_device, furi_string_get_cstr(pof_token->load_path));
|
|
|
+
|
|
|
response[0] = 'W';
|
|
|
response[1] = 0x10 | arrayIndex;
|
|
|
response[2] = blockNum;
|
|
|
return 3;
|
|
|
}
|
|
|
|
|
|
+// HID portals use send 8000hz 16 bit signed PCM samples
|
|
|
+void virtual_portal_process_audio(
|
|
|
+ VirtualPortal* virtual_portal,
|
|
|
+ uint8_t* message,
|
|
|
+ uint8_t len) {
|
|
|
+ for (size_t i = 0; i < len; i += 2) {
|
|
|
+ int16_t int_16 =
|
|
|
+ (((int16_t)message[i + 1] << 8) + ((int16_t)message[i]));
|
|
|
+
|
|
|
+ float data = ((float)int_16 / 256.0);
|
|
|
+ data /= UINT8_MAX / 2; // scale -1..1
|
|
|
+
|
|
|
+ data *= virtual_portal->volume; // volume
|
|
|
+ data = tanhf(data); // hyperbolic tangent limiter
|
|
|
+
|
|
|
+ data *= UINT8_MAX / 2; // scale -128..127
|
|
|
+ data += UINT8_MAX / 2; // to unsigned
|
|
|
+
|
|
|
+ if (data < 0) {
|
|
|
+ data = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data > 255) {
|
|
|
+ data = 255;
|
|
|
+ }
|
|
|
+ *virtual_portal->head = data;
|
|
|
+ virtual_portal->count++;
|
|
|
+ if (++virtual_portal->head == virtual_portal->end) {
|
|
|
+ virtual_portal->head = virtual_portal->current_audio_buffer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 360 portals didn't have the bandwith, so they use CCITT G.721 ADPCM coding
|
|
|
+// to encode the audio so it uses less bandwith.
|
|
|
+void virtual_portal_process_audio_360(
|
|
|
+ VirtualPortal* virtual_portal,
|
|
|
+ uint8_t* message,
|
|
|
+ uint8_t len) {
|
|
|
+ for (size_t i = 0; i < len; i++) {
|
|
|
+ int16_t int_16 = (int16_t)g721_decoder(message[i], &virtual_portal->state);
|
|
|
+
|
|
|
+ float data = ((float)int_16 / 256.0);
|
|
|
+ data /= UINT8_MAX / 2; // scale -1..1
|
|
|
+
|
|
|
+ data *= virtual_portal->volume; // volume
|
|
|
+ data = tanhf(data); // hyperbolic tangent limiter
|
|
|
+
|
|
|
+ data *= UINT8_MAX / 2; // scale -128..127
|
|
|
+ data += UINT8_MAX / 2; // to unsigned
|
|
|
+
|
|
|
+ if (data < 0) {
|
|
|
+ data = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data > 255) {
|
|
|
+ data = 255;
|
|
|
+ }
|
|
|
+ *virtual_portal->head = data;
|
|
|
+ virtual_portal->count++;
|
|
|
+ if (++virtual_portal->head == virtual_portal->end) {
|
|
|
+ virtual_portal->head = virtual_portal->current_audio_buffer;
|
|
|
+ }
|
|
|
+
|
|
|
+ int_16 = (int16_t)g721_decoder(message[i] >> 4, &virtual_portal->state);
|
|
|
+
|
|
|
+ data = ((float)int_16 / 256.0);
|
|
|
+ data /= UINT8_MAX / 2; // scale -1..1
|
|
|
+
|
|
|
+ data *= virtual_portal->volume; // volume
|
|
|
+ data = tanhf(data); // hyperbolic tangent limiter
|
|
|
+
|
|
|
+ data *= UINT8_MAX / 2; // scale -128..127
|
|
|
+ data += UINT8_MAX / 2; // to unsigned
|
|
|
+
|
|
|
+ if (data < 0) {
|
|
|
+ data = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (data > 255) {
|
|
|
+ data = 255;
|
|
|
+ }
|
|
|
+ *virtual_portal->head = data;
|
|
|
+ virtual_portal->count++;
|
|
|
+ if (++virtual_portal->head == virtual_portal->end) {
|
|
|
+ virtual_portal->head = virtual_portal->current_audio_buffer;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 32 byte message, 32 byte response;
|
|
|
int virtual_portal_process_message(
|
|
|
VirtualPortal* virtual_portal,
|
|
|
uint8_t* message,
|
|
|
uint8_t* response) {
|
|
|
memset(response, 0, 32);
|
|
|
- switch(message[0]) {
|
|
|
- case 'A':
|
|
|
- return virtual_portal_activate(virtual_portal, message, response);
|
|
|
- case 'C': //Ring color R G B
|
|
|
- virtaul_portal_set_leds(virtual_portal, message[1], message[2], message[3]);
|
|
|
- return 0;
|
|
|
- case 'J':
|
|
|
- // https://github.com/flyandi/flipper_zero_rgb_led
|
|
|
- return virtual_portal_j(virtual_portal, message, response);
|
|
|
- case 'L':
|
|
|
- return virtual_portal_l(virtual_portal, message, response);
|
|
|
- case 'M':
|
|
|
- return virtual_portal_m(virtual_portal, message, response);
|
|
|
- case 'Q': //Query
|
|
|
- return virtual_portal_query(virtual_portal, message, response);
|
|
|
- case 'R':
|
|
|
- return virtual_portal_reset(virtual_portal, message, response);
|
|
|
- case 'S': //Status
|
|
|
- return virtual_portal_status(virtual_portal, response);
|
|
|
- case 'V':
|
|
|
- return 0;
|
|
|
- case 'W': //Write
|
|
|
- return virtual_portal_write(virtual_portal, message, response);
|
|
|
- case 'Z':
|
|
|
- return 0;
|
|
|
- default:
|
|
|
- FURI_LOG_W(TAG, "Unhandled command %c", message[0]);
|
|
|
- return 0; //No response
|
|
|
+ switch (message[0]) {
|
|
|
+ case 'A':
|
|
|
+ return virtual_portal_activate(virtual_portal, message, response);
|
|
|
+ case 'C': // Ring color R G B
|
|
|
+ queue_led_command(virtual_portal, PORTAL_SIDE_RING, message[1], message[2], message[3], 0);
|
|
|
+ return 0;
|
|
|
+ case 'J':
|
|
|
+ // https://github.com/flyandi/flipper_zero_rgb_led
|
|
|
+ return virtual_portal_j(virtual_portal, message, response);
|
|
|
+ case 'L':
|
|
|
+ return virtual_portal_l(virtual_portal, message);
|
|
|
+ case 'M':
|
|
|
+ return virtual_portal_m(virtual_portal, message, response);
|
|
|
+ case 'Q': // Query
|
|
|
+ if (!virtual_portal->active) {
|
|
|
+ return 0; // No response if portal is not active
|
|
|
+ }
|
|
|
+ return virtual_portal_query(virtual_portal, message, response);
|
|
|
+ case 'R':
|
|
|
+ return virtual_portal_reset(virtual_portal, message, response);
|
|
|
+ case 'S': // Status
|
|
|
+ if (!virtual_portal->active) {
|
|
|
+ return 0; // No response if portal is not active
|
|
|
+ }
|
|
|
+ return virtual_portal_status(virtual_portal, response);
|
|
|
+ case 'V':
|
|
|
+ virtual_portal->m = message[3];
|
|
|
+ return 0;
|
|
|
+ case 'W': // Write
|
|
|
+ if (!virtual_portal->active) {
|
|
|
+ return 0; // No response if portal is not active
|
|
|
+ }
|
|
|
+ return virtual_portal_write(virtual_portal, message, response);
|
|
|
+ case 'Z':
|
|
|
+ return 0;
|
|
|
+ default:
|
|
|
+ FURI_LOG_W(TAG, "Unhandled command %c", message[0]);
|
|
|
+ return 0; // No response
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
-}
|
|
|
+}
|