Explorar o código

public release

Eric Betts hai 11 meses
achega
cc0bf2e7a7

+ 41 - 0
.github/workflows/build.yml

@@ -0,0 +1,41 @@
+name: "FAP: Build for multiple SDK sources"
+# This will build your app for dev and release channels on GitHub. 
+# It will also build your app every day to make sure it's up to date with the latest SDK changes.
+# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
+
+on:
+  push:
+    ## put your main branch name under "branches"
+    #branches: 
+    #  - master 
+  pull_request:
+  schedule: 
+    # do a build every day
+    - cron: "1 1 * * *"
+
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+          # You can add unofficial channels here. See ufbt action docs for more info.
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          # See ufbt action docs for other output variables
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 7 - 0
.gitignore

@@ -0,0 +1,7 @@
+dist/*
+.vscode
+.clang-format
+.clangd
+.editorconfig
+.env
+.ufbt

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+# Portal of Flipper
+
+USB Emulator
+
+
+## TODO:
+ * Play audio: (should be possible: https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/wav_player/wav_player_hal.c)
+ * Hardware add-on with RGB LEDs to emulate portal and 'jail' lights: https://github.com/flyandi/flipper_zero_rgb_led/blob/master/led_ll.c

+ 17 - 0
application.fam

@@ -0,0 +1,17 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="portal_of_flipper",  # Must be unique
+    name="Portal Of Flipper",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="portal_of_flipper_app",
+    stack_size=5 * 1024,
+    fap_category="USB",
+    # Optional values
+    # fap_version="0.1",
+    fap_icon="portal_of_flipper.png",  # 10x10 1-bit PNG
+    fap_description="USB emulator",
+    fap_author="bettse",
+    fap_weburl="https://gitlab.com/bettse/portal_of_flipper",
+    fap_icon_assets="images",  # Image assets to compile for this application
+)

+ 432 - 0
helpers/pof_usb.c

@@ -0,0 +1,432 @@
+#include "pof_usb.h"
+
+#define TAG "POF USB"
+
+#define HID_INTERVAL 1
+
+#define USB_EP0_SIZE 8
+
+#define POF_USB_VID (0x1430)
+#define POF_USB_PID (0x0150)
+
+#define POF_USB_EP_IN  (0x81)
+#define POF_USB_EP_OUT (0x02)
+
+#define POF_USB_EP_IN_SIZE  (64UL)
+#define POF_USB_EP_OUT_SIZE (64UL)
+
+#define POF_USB_RX_MAX_SIZE (POF_USB_EP_OUT_SIZE)
+#define POF_USB_TX_MAX_SIZE (POF_USB_EP_IN_SIZE)
+
+#define POF_USB_ACTUAL_OUTPUT_SIZE 0x20
+
+static const struct usb_string_descriptor dev_manuf_desc =
+    USB_ARRAY_DESC(0x41, 0x63, 0x74, 0x69, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x00);
+static const struct usb_string_descriptor dev_product_desc =
+    USB_ARRAY_DESC(0x53, 0x70, 0x79, 0x72, 0x6f, 0x20, 0x50, 0x6f, 0x72, 0x74, 0x61, 0x00);
+
+static const uint8_t hid_report_desc[] = {0x06, 0x00, 0xFF, 0x09, 0x01, 0xA1, 0x01, 0x19,
+                                          0x01, 0x29, 0x40, 0x15, 0x00, 0x26, 0xFF, 0x00,
+                                          0x75, 0x08, 0x95, 0x20, 0x81, 0x00, 0x19, 0x01,
+                                          0x29, 0x40, 0x91, 0x00, 0xC0};
+
+static usbd_respond pof_usb_ep_config(usbd_device* dev, uint8_t cfg);
+static usbd_respond
+    pof_hid_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback);
+static void pof_usb_send(usbd_device* dev, uint8_t* buf, uint16_t len);
+static int32_t pof_usb_receive(usbd_device* dev, uint8_t* buf, uint16_t max_len);
+
+typedef enum {
+    EventExit = (1 << 0),
+    EventReset = (1 << 1),
+    EventRx = (1 << 2),
+    EventTx = (1 << 3),
+    EventTxComplete = (1 << 4),
+    EventResetSio = (1 << 5),
+    EventTxImmediate = (1 << 6),
+
+    EventAll = EventExit | EventReset | EventRx | EventTx | EventTxComplete | EventResetSio |
+               EventTxImmediate,
+} PoFEvent;
+
+struct PoFUsb {
+    FuriHalUsbInterface usb;
+    FuriHalUsbInterface* usb_prev;
+
+    FuriThread* thread;
+    usbd_device* dev;
+    VirtualPortal* virtual_portal;
+    uint8_t data_recvest[8];
+    uint16_t data_recvest_len;
+
+    bool tx_complete;
+    bool tx_immediate;
+
+    uint8_t dataAvailable;
+    uint8_t data[POF_USB_RX_MAX_SIZE];
+
+    uint8_t tx_data[POF_USB_TX_MAX_SIZE];
+};
+
+static PoFUsb* pof_cur = NULL;
+
+static int32_t pof_thread_worker(void* context) {
+    PoFUsb* pof_usb = context;
+    usbd_device* dev = pof_usb->dev;
+    VirtualPortal* virtual_portal = pof_usb->virtual_portal;
+    UNUSED(dev);
+
+    uint32_t len_data = 0;
+    uint8_t tx_data[POF_USB_TX_MAX_SIZE] = {0};
+    uint32_t timeout = 100; // FuriWaitForever; //ms
+
+    while(true) {
+        uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, timeout);
+        if(flags & EventRx) { //fast flag
+            UNUSED(pof_usb_receive);
+
+            if(virtual_portal->speaker) {
+                uint8_t buf[POF_USB_RX_MAX_SIZE];
+                len_data = pof_usb_receive(dev, buf, POF_USB_RX_MAX_SIZE);
+                // https://github.com/xMasterX/all-the-plugins/blob/dev/base_pack/wav_player/wav_player_hal.c
+                if(len_data > 0) {
+                    /*
+                    FURI_LOG_RAW_I("pof_usb_receive: ");
+                    for(uint32_t i = 0; i < len_data; i++) {
+                        FURI_LOG_RAW_I("%02x", buf[i]);
+                    }
+                    FURI_LOG_RAW_I("\r\n");
+                    */
+                }
+            }
+
+            if(pof_usb->dataAvailable > 0) {
+                memset(tx_data, 0, sizeof(tx_data));
+                int send_len =
+                    virtual_portal_process_message(virtual_portal, pof_usb->data, tx_data);
+                if(send_len > 0) {
+                    pof_usb_send(dev, tx_data, POF_USB_ACTUAL_OUTPUT_SIZE);
+                }
+                pof_usb->dataAvailable = 0;
+            }
+
+            flags &= ~EventRx; // clear flag
+        }
+
+        if(flags) {
+            if(flags & EventResetSio) {
+            }
+            if(flags & EventTxComplete) {
+                pof_usb->tx_complete = true;
+            }
+
+            if(flags & EventTxImmediate) {
+                pof_usb->tx_immediate = true;
+                if(pof_usb->tx_complete) {
+                    flags |= EventTx;
+                }
+            }
+
+            if(flags & EventTx) {
+                pof_usb->tx_complete = false;
+                pof_usb->tx_immediate = false;
+            }
+
+            if(flags & EventExit) {
+                FURI_LOG_I(TAG, "exit");
+                break;
+            }
+        }
+
+        if(flags == (uint32_t)FuriFlagErrorISR) { // timeout
+            memset(tx_data, 0, sizeof(tx_data));
+            len_data = virtual_portal_send_status(virtual_portal, tx_data);
+            if(len_data > 0) {
+                pof_usb_send(dev, tx_data, POF_USB_ACTUAL_OUTPUT_SIZE);
+            }
+        }
+    }
+
+    return 0;
+}
+
+static void pof_usb_init(usbd_device* dev, FuriHalUsbInterface* intf, void* ctx) {
+    UNUSED(intf);
+    PoFUsb* pof_usb = ctx;
+    pof_cur = pof_usb;
+    pof_usb->dev = dev;
+
+    usbd_reg_config(dev, pof_usb_ep_config);
+    usbd_reg_control(dev, pof_hid_control);
+    UNUSED(pof_hid_control);
+    usbd_connect(dev, true);
+
+    pof_usb->thread = furi_thread_alloc();
+    furi_thread_set_name(pof_usb->thread, "PoFUsb");
+    furi_thread_set_stack_size(pof_usb->thread, 2 * 1024);
+    furi_thread_set_context(pof_usb->thread, ctx);
+    furi_thread_set_callback(pof_usb->thread, pof_thread_worker);
+
+    furi_thread_start(pof_usb->thread);
+}
+
+static void pof_usb_deinit(usbd_device* dev) {
+    usbd_reg_config(dev, NULL);
+    usbd_reg_control(dev, NULL);
+
+    PoFUsb* pof_usb = pof_cur;
+    if(!pof_usb || pof_usb->dev != dev) {
+        return;
+    }
+    pof_cur = NULL;
+
+    furi_assert(pof_usb->thread);
+    furi_thread_flags_set(furi_thread_get_id(pof_usb->thread), EventExit);
+    furi_thread_join(pof_usb->thread);
+    furi_thread_free(pof_usb->thread);
+    pof_usb->thread = NULL;
+
+    free(pof_usb->usb.str_prod_descr);
+    pof_usb->usb.str_prod_descr = NULL;
+    free(pof_usb->usb.str_serial_descr);
+    pof_usb->usb.str_serial_descr = NULL;
+    free(pof_usb);
+}
+
+static void pof_usb_send(usbd_device* dev, uint8_t* buf, uint16_t len) {
+    // Hide frequent responses
+    if(buf[0] != 'S' && buf[0] != 'J') {
+        FURI_LOG_RAW_D("> ");
+        for(size_t i = 0; i < len; i++) {
+            FURI_LOG_RAW_D("%02x", buf[i]);
+        }
+        FURI_LOG_RAW_D("\r\n");
+    }
+    usbd_ep_write(dev, POF_USB_EP_IN, buf, len);
+}
+
+static int32_t pof_usb_receive(usbd_device* dev, uint8_t* buf, uint16_t max_len) {
+    int32_t len = usbd_ep_read(dev, POF_USB_EP_OUT, buf, max_len);
+    return ((len < 0) ? 0 : len);
+}
+
+static void pof_usb_wakeup(usbd_device* dev) {
+    UNUSED(dev);
+}
+
+static void pof_usb_suspend(usbd_device* dev) {
+    PoFUsb* pof_usb = pof_cur;
+    if(!pof_usb || pof_usb->dev != dev) return;
+}
+
+static void pof_usb_rx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) {
+    UNUSED(dev);
+    UNUSED(event);
+    UNUSED(ep);
+    PoFUsb* pof_usb = pof_cur;
+    furi_thread_flags_set(furi_thread_get_id(pof_usb->thread), EventRx);
+}
+
+static void pof_usb_tx_ep_callback(usbd_device* dev, uint8_t event, uint8_t ep) {
+    UNUSED(dev);
+    UNUSED(event);
+    UNUSED(ep);
+    PoFUsb* pof_usb = pof_cur;
+    furi_thread_flags_set(furi_thread_get_id(pof_usb->thread), EventTxComplete);
+}
+
+static usbd_respond pof_usb_ep_config(usbd_device* dev, uint8_t cfg) {
+    switch(cfg) {
+    case 0: // deconfig
+        usbd_ep_deconfig(dev, POF_USB_EP_OUT);
+        usbd_ep_deconfig(dev, POF_USB_EP_IN);
+        usbd_reg_endpoint(dev, POF_USB_EP_OUT, NULL);
+        usbd_reg_endpoint(dev, POF_USB_EP_IN, NULL);
+        return usbd_ack;
+    case 1: // config
+        usbd_ep_config(dev, POF_USB_EP_IN, USB_EPTYPE_INTERRUPT, POF_USB_EP_IN_SIZE);
+        usbd_ep_config(dev, POF_USB_EP_OUT, USB_EPTYPE_INTERRUPT, POF_USB_EP_OUT_SIZE);
+        usbd_reg_endpoint(dev, POF_USB_EP_IN, pof_usb_tx_ep_callback);
+        usbd_reg_endpoint(dev, POF_USB_EP_OUT, pof_usb_rx_ep_callback);
+        return usbd_ack;
+    }
+    return usbd_fail;
+}
+
+struct PoFUsbDescriptor {
+    struct usb_config_descriptor config;
+    struct usb_interface_descriptor intf;
+    struct usb_hid_descriptor hid_desc;
+    struct usb_endpoint_descriptor ep_in;
+    struct usb_endpoint_descriptor ep_out;
+} __attribute__((packed));
+
+static const struct usb_device_descriptor usb_pof_dev_descr = {
+    .bLength = sizeof(struct usb_device_descriptor),
+    .bDescriptorType = USB_DTYPE_DEVICE,
+    .bcdUSB = VERSION_BCD(2, 0, 0),
+    .bDeviceClass = USB_CLASS_PER_INTERFACE,
+    .bDeviceSubClass = USB_SUBCLASS_NONE,
+    .bDeviceProtocol = USB_PROTO_NONE,
+    .bMaxPacketSize0 = USB_EP0_SIZE,
+    .idVendor = POF_USB_VID,
+    .idProduct = POF_USB_PID,
+    .bcdDevice = VERSION_BCD(1, 0, 0),
+    .iManufacturer = 1, // UsbDevManuf
+    .iProduct = 2, // UsbDevProduct
+    .iSerialNumber = 0,
+    .bNumConfigurations = 1,
+};
+
+static const struct PoFUsbDescriptor usb_pof_cfg_descr = {
+    .config =
+        {
+            .bLength = sizeof(struct usb_config_descriptor),
+            .bDescriptorType = USB_DTYPE_CONFIGURATION,
+            .wTotalLength = sizeof(struct PoFUsbDescriptor),
+            .bNumInterfaces = 1,
+            .bConfigurationValue = 1,
+            .iConfiguration = NO_DESCRIPTOR,
+            .bmAttributes = USB_CFG_ATTR_RESERVED,
+            .bMaxPower = USB_CFG_POWER_MA(500),
+        },
+    .intf =
+        {
+            .bLength = sizeof(struct usb_interface_descriptor),
+            .bDescriptorType = USB_DTYPE_INTERFACE,
+            .bInterfaceNumber = 0,
+            .bAlternateSetting = 0,
+            .bNumEndpoints = 2,
+            .bInterfaceClass = USB_CLASS_HID,
+            .bInterfaceSubClass = USB_HID_SUBCLASS_NONBOOT,
+            .bInterfaceProtocol = USB_HID_PROTO_NONBOOT,
+            .iInterface = NO_DESCRIPTOR,
+        },
+    .hid_desc =
+        {
+            .bLength = sizeof(struct usb_hid_descriptor),
+            .bDescriptorType = USB_DTYPE_HID,
+            .bcdHID = VERSION_BCD(1, 1, 1),
+            .bCountryCode = USB_HID_COUNTRY_NONE,
+            .bNumDescriptors = 1,
+            .bDescriptorType0 = USB_DTYPE_HID_REPORT,
+            .wDescriptorLength0 = sizeof(hid_report_desc),
+        },
+    .ep_in =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_EP_IN,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x40,
+            .bInterval = HID_INTERVAL,
+        },
+    .ep_out =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_EP_OUT,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x40,
+            .bInterval = HID_INTERVAL,
+        },
+};
+
+/* Control requests handler */
+static usbd_respond
+    pof_hid_control(usbd_device* dev, usbd_ctlreq* req, usbd_rqc_callback* callback) {
+    UNUSED(callback);
+    uint8_t wValueH = req->wValue >> 8;
+    uint16_t length = req->wLength;
+
+    PoFUsb* pof_usb = pof_cur;
+
+    /* HID control requests */
+    if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) ==
+           (USB_REQ_INTERFACE | USB_REQ_CLASS) &&
+       req->wIndex == 0) {
+        switch(req->bRequest) {
+        case USB_HID_SETIDLE:
+            return usbd_ack;
+        case USB_HID_SETPROTOCOL:
+            return usbd_ack;
+        case USB_HID_GETREPORT:
+            dev->status.data_ptr = pof_usb->tx_data;
+            dev->status.data_count = sizeof(pof_usb->tx_data);
+            return usbd_ack;
+        case USB_HID_SETREPORT:
+            if(wValueH == HID_REPORT_TYPE_INPUT) {
+                if(length == POF_USB_RX_MAX_SIZE) {
+                    return usbd_ack;
+                }
+            } else if(wValueH == HID_REPORT_TYPE_OUTPUT) {
+                memcpy(pof_usb->data, req->data, req->wLength);
+                pof_usb->dataAvailable += req->wLength;
+                furi_thread_flags_set(furi_thread_get_id(pof_usb->thread), EventRx);
+
+                return usbd_ack;
+            } else if(wValueH == HID_REPORT_TYPE_FEATURE) {
+                return usbd_ack;
+            }
+            return usbd_fail;
+        default:
+            return usbd_fail;
+        }
+    }
+
+    if(((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) ==
+           (USB_REQ_INTERFACE | USB_REQ_STANDARD) &&
+       req->wIndex == 0 && req->bRequest == USB_STD_GET_DESCRIPTOR) {
+        switch(wValueH) {
+        case USB_DTYPE_HID:
+            dev->status.data_ptr = (uint8_t*)&(usb_pof_cfg_descr.hid_desc);
+            dev->status.data_count = sizeof(usb_pof_cfg_descr.hid_desc);
+            return usbd_ack;
+        case USB_DTYPE_HID_REPORT:
+            dev->status.data_ptr = (uint8_t*)hid_report_desc;
+            dev->status.data_count = sizeof(hid_report_desc);
+            return usbd_ack;
+        default:
+            return usbd_fail;
+        }
+    }
+    return usbd_fail;
+}
+
+PoFUsb* pof_usb_start(VirtualPortal* virtual_portal) {
+    PoFUsb* pof_usb = malloc(sizeof(PoFUsb));
+    pof_usb->virtual_portal = virtual_portal;
+    pof_usb->dataAvailable = 0;
+
+    pof_usb->usb_prev = furi_hal_usb_get_config();
+    pof_usb->usb.init = pof_usb_init;
+    pof_usb->usb.deinit = pof_usb_deinit;
+    pof_usb->usb.wakeup = pof_usb_wakeup;
+    pof_usb->usb.suspend = pof_usb_suspend;
+    pof_usb->usb.dev_descr = (struct usb_device_descriptor*)&usb_pof_dev_descr;
+    pof_usb->usb.str_manuf_descr = (void*)&dev_manuf_desc;
+    pof_usb->usb.str_prod_descr = (void*)&dev_product_desc;
+    pof_usb->usb.str_serial_descr = NULL;
+    pof_usb->usb.cfg_descr = (void*)&usb_pof_cfg_descr;
+
+    if(!furi_hal_usb_set_config(&pof_usb->usb, pof_usb)) {
+        FURI_LOG_E(TAG, "USB locked, can not start");
+        if(pof_usb->usb.str_manuf_descr) {
+            free(pof_usb->usb.str_manuf_descr);
+        }
+        if(pof_usb->usb.str_prod_descr) {
+            free(pof_usb->usb.str_prod_descr);
+        }
+        if(pof_usb->usb.str_serial_descr) {
+            free(pof_usb->usb.str_serial_descr);
+        }
+
+        free(pof_usb);
+        return NULL;
+    }
+    return pof_usb;
+}
+
+void pof_usb_stop(PoFUsb* pof_usb) {
+    furi_hal_usb_set_config(pof_usb->usb_prev, NULL);
+}

+ 78 - 0
helpers/pof_usb.h

@@ -0,0 +1,78 @@
+#pragma once
+
+#include <furi_hal.h>
+#include <furi_hal_version.h>
+#include <furi_hal_usb.h>
+#include <furi_hal_usb_hid.h>
+
+#include "usb.h"
+#include "usb_hid.h"
+#include "virtual_portal.h"
+
+#define HID_REPORT_TYPE_INPUT   1
+#define HID_REPORT_TYPE_OUTPUT  2
+#define HID_REPORT_TYPE_FEATURE 3
+
+typedef struct PoFUsb PoFUsb;
+
+PoFUsb* pof_usb_start(VirtualPortal* virtual_portal);
+void pof_usb_stop(PoFUsb* pof);
+
+/*descriptor type*/
+typedef enum {
+    PoFDescriptorTypeDevice = 0x01,
+    PoFDescriptorTypeConfig = 0x02,
+    PoFDescriptorTypeString = 0x03,
+    PoFDescriptorTypeInterface = 0x04,
+    PoFDescriptorTypeEndpoint = 0x05,
+} PoFDescriptorType;
+
+/*endpoint direction*/
+typedef enum {
+    PoFEndpointIn = 0x80,
+    PoFEndpointOut = 0x00,
+} PoFEndpointDirection;
+
+/*endpoint type*/
+typedef enum {
+    PoFEndpointTypeCtrl = 0x00,
+    PoFEndpointTypeIso = 0x01,
+    PoFEndpointTypeBulk = 0x02,
+    PoFEndpointTypeIntr = 0x03,
+} PoFEndpointType;
+
+/*control request type*/
+typedef enum {
+    PoFControlTypeStandard = (0 << 5),
+    PoFControlTypeClass = (1 << 5),
+    PoFControlTypeVendor = (2 << 5),
+    PoFControlTypeReserved = (3 << 5),
+} PoFControlType;
+
+/*control request recipient*/
+typedef enum {
+    PoFControlRecipientDevice = 0,
+    PoFControlRecipientInterface = 1,
+    PoFControlRecipientEndpoint = 2,
+    PoFControlRecipientOther = 3,
+} PoFControlRecipient;
+
+/*control request direction*/
+typedef enum {
+    PoFControlOut = 0x00,
+    PoFControlIn = 0x80,
+} PoFControlDirection;
+
+/*endpoint address mask*/
+typedef enum {
+    PoFEndpointAddrMask = 0x0f,
+    PoFEndpointDirMask = 0x80,
+    PoFEndpointTransferTypeMask = 0x03,
+    PoFCtrlDirMask = 0x80,
+} PoFEndpointMask;
+
+/* USB control requests */
+typedef enum {
+    PoFControlRequestsOut = (PoFControlTypeVendor | PoFControlRecipientDevice | PoFControlOut),
+    PoFControlRequestsIn = (PoFControlTypeVendor | PoFControlRecipientDevice | PoFControlIn),
+} PoFControlRequests;

+ 0 - 0
images/.gitkeep


BIN=BIN
images/Nfc_10px.png


+ 135 - 0
pof_token.c

@@ -0,0 +1,135 @@
+#include <toolbox/path.h>
+#include <flipper_format/flipper_format.h>
+
+#include <portal_of_flipper_icons.h>
+#include "pof_token.h"
+
+#define TAG "PoFToken"
+
+static uint8_t pof_token_sector_0_key[] = {0x4b, 0x0b, 0x20, 0x10, 0x7c, 0xcb};
+
+PoFToken* pof_token_alloc() {
+    PoFToken* pof_token = malloc(sizeof(PoFToken));
+    memset(pof_token, 0, sizeof(PoFToken));
+    pof_token->storage = furi_record_open(RECORD_STORAGE);
+    pof_token->dialogs = furi_record_open(RECORD_DIALOGS);
+    pof_token->load_path = furi_string_alloc();
+    pof_token->loaded = false;
+    pof_token->change = false;
+    pof_token->nfc_device = nfc_device_alloc();
+    return pof_token;
+}
+
+void pof_token_set_name(PoFToken* pof_token, const char* name) {
+    furi_assert(pof_token);
+
+    strlcpy(pof_token->dev_name, name, sizeof(pof_token->dev_name));
+}
+
+static bool pof_token_load_data(PoFToken* pof_token, FuriString* path, bool show_dialog) {
+    FuriString* reason = furi_string_alloc_set("Couldn't load file");
+
+    if(pof_token->loading_cb) {
+        pof_token->loading_cb(pof_token->loading_cb_ctx, true);
+    }
+
+    do {
+        NfcDevice* nfc_device = pof_token->nfc_device;
+        if(!nfc_device_load(nfc_device, furi_string_get_cstr(path))) break;
+
+        NfcProtocol protocol = nfc_device_get_protocol(nfc_device);
+        if(protocol != NfcProtocolMfClassic) {
+            furi_string_printf(reason, "Not Mifare Classic");
+            break;
+        }
+
+        const MfClassicData* data = nfc_device_get_data(nfc_device, NfcProtocolMfClassic);
+        if(!mf_classic_is_card_read(data)) {
+            furi_string_printf(reason, "Incomplete data");
+            break;
+        }
+
+        MfClassicKey key = mf_classic_get_key(data, 0, MfClassicKeyTypeA);
+        if(memcmp(key.data, pof_token_sector_0_key, MF_CLASSIC_KEY_SIZE) != 0) {
+            furi_string_printf(reason, "Wrong key");
+            break;
+        }
+
+        pof_token->loaded = true;
+        pof_token->change = true;
+    } while(false);
+
+    if(pof_token->loading_cb) {
+        pof_token->loading_cb(pof_token->loading_cb_ctx, false);
+    }
+
+    if((!pof_token->loaded) && (show_dialog)) {
+        dialog_message_show_storage_error(pof_token->dialogs, furi_string_get_cstr(reason));
+    }
+
+    furi_string_free(reason);
+
+    return pof_token->loaded;
+}
+
+void pof_token_clear(PoFToken* pof_token, bool save) {
+    furi_assert(pof_token);
+    if(save) {
+        // Saving during app clean up causes a crash
+        nfc_device_save(pof_token->nfc_device, furi_string_get_cstr(pof_token->load_path));
+    }
+    nfc_device_clear(pof_token->nfc_device);
+    furi_string_reset(pof_token->load_path);
+    memset(pof_token->dev_name, 0, sizeof(pof_token->dev_name));
+    pof_token->loaded = false;
+    pof_token->change = true;
+}
+
+void pof_token_free(PoFToken* pof_token) {
+    furi_assert(pof_token);
+    pof_token_clear(pof_token, false);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(pof_token->load_path);
+    nfc_device_free(pof_token->nfc_device);
+    free(pof_token);
+}
+
+bool pof_file_select(PoFToken* pof_token) {
+    furi_assert(pof_token);
+
+    FuriString* pof_app_folder;
+    pof_app_folder = furi_string_alloc_set("/ext/nfc");
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, ".nfc", &I_Nfc_10px);
+    browser_options.base_path = "/ext/nfc";
+
+    bool res = dialog_file_browser_show(
+        pof_token->dialogs, pof_token->load_path, pof_app_folder, &browser_options);
+
+    furi_string_free(pof_app_folder);
+    if(res) {
+        FuriString* filename;
+        filename = furi_string_alloc();
+        path_extract_filename(pof_token->load_path, filename, true);
+        strlcpy(pof_token->dev_name, furi_string_get_cstr(filename), sizeof(pof_token->dev_name));
+        res = pof_token_load_data(pof_token, pof_token->load_path, true);
+        if(res) {
+            pof_token_set_name(pof_token, pof_token->dev_name);
+        }
+        furi_string_free(filename);
+    }
+
+    return res;
+}
+
+void pof_token_set_loading_callback(
+    PoFToken* pof_token,
+    PoFLoadingCallback callback,
+    void* context) {
+    furi_assert(pof_token);
+
+    pof_token->loading_cb = callback;
+    pof_token->loading_cb_ctx = context;
+}

+ 36 - 0
pof_token.h

@@ -0,0 +1,36 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <lib/nfc/nfc_device.h>
+#include <lib/nfc/protocols/mf_classic/mf_classic.h>
+
+#define POF_TOKEN_NAME_MAX_LEN 129
+
+typedef void (*PoFLoadingCallback)(void* context, bool state);
+
+typedef struct {
+    Storage* storage;
+    DialogsApp* dialogs;
+    FuriString* load_path;
+    PoFLoadingCallback loading_cb;
+    void* loading_cb_ctx;
+    char dev_name[POF_TOKEN_NAME_MAX_LEN];
+    bool change;
+    bool loaded;
+    NfcDevice* nfc_device;
+} PoFToken;
+
+PoFToken* pof_token_alloc();
+
+void pof_token_free(PoFToken* pof_token);
+
+void pof_token_set_name(PoFToken* pof_token, const char* name);
+
+bool pof_file_select(PoFToken* pof_token);
+
+void pof_token_clear(PoFToken* pof_token, bool save);
+
+void pof_token_set_loading_callback(PoFToken* dev, PoFLoadingCallback callback, void* context);

+ 129 - 0
portal_of_flipper.c

@@ -0,0 +1,129 @@
+#include <furi.h>
+#include "portal_of_flipper_i.h"
+
+static bool pof_app_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    PoFApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, event);
+}
+
+static bool pof_app_back_event_callback(void* context) {
+    furi_assert(context);
+    PoFApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+static void pof_app_tick_event_callback(void* context) {
+    furi_assert(context);
+    PoFApp* app = context;
+    scene_manager_handle_tick_event(app->scene_manager);
+}
+
+void pof_show_loading_popup(void* context, bool show) {
+    PoFApp* app = context;
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(app->view_dispatcher, PoFViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+PoFApp* pof_app_alloc() {
+    PoFApp* app = malloc(sizeof(PoFApp));
+
+    // GUI
+    app->gui = furi_record_open(RECORD_GUI);
+
+    // View Dispatcher
+    app->view_dispatcher = view_dispatcher_alloc();
+    app->scene_manager = scene_manager_alloc(&pof_scene_handlers, app);
+
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, pof_app_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        app->view_dispatcher, pof_app_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        app->view_dispatcher, pof_app_tick_event_callback, 100);
+
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // SubMenu
+    app->submenu = submenu_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, PoFViewSubmenu, submenu_get_view(app->submenu));
+
+    // Popup
+    app->popup = popup_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, PoFViewPopup, popup_get_view(app->popup));
+
+    // Loading
+    app->loading = loading_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, PoFViewLoading, loading_get_view(app->loading));
+
+    // Widget
+    app->widget = widget_alloc();
+    view_dispatcher_add_view(app->view_dispatcher, PoFViewWidget, widget_get_view(app->widget));
+
+    app->virtual_portal = virtual_portal_alloc(app->notifications);
+    // PoF emulation Start
+    pof_start(app);
+
+    scene_manager_next_scene(app->scene_manager, PoFSceneMain);
+    return app;
+}
+
+void pof_app_free(PoFApp* app) {
+    furi_assert(app);
+
+    // PoF emulation Stop
+    pof_stop(app);
+
+    // Submenu
+    view_dispatcher_remove_view(app->view_dispatcher, PoFViewSubmenu);
+    submenu_free(app->submenu);
+
+    // Popup
+    view_dispatcher_remove_view(app->view_dispatcher, PoFViewPopup);
+    popup_free(app->popup);
+
+    // Loading
+    view_dispatcher_remove_view(app->view_dispatcher, PoFViewLoading);
+    loading_free(app->loading);
+
+    //  Widget
+    view_dispatcher_remove_view(app->view_dispatcher, PoFViewWidget);
+    widget_free(app->widget);
+
+    // View dispatcher
+    view_dispatcher_free(app->view_dispatcher);
+    scene_manager_free(app->scene_manager);
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    app->notifications = NULL;
+
+    // Close records
+    furi_record_close(RECORD_GUI);
+
+    virtual_portal_free(app->virtual_portal);
+    app->virtual_portal = NULL;
+
+    free(app);
+}
+
+int32_t portal_of_flipper_app(void* p) {
+    UNUSED(p);
+
+    PoFApp* pof_app = pof_app_alloc();
+
+    view_dispatcher_run(pof_app->view_dispatcher);
+
+    pof_app_free(pof_app);
+
+    return 0;
+}

BIN=BIN
portal_of_flipper.png


+ 17 - 0
portal_of_flipper_i.c

@@ -0,0 +1,17 @@
+#include "portal_of_flipper_i.h"
+
+#include <furi.h>
+
+#define TAG "PoF"
+
+void pof_start(PoFApp* app) {
+    furi_assert(app);
+
+    app->pof_usb = pof_usb_start(app->virtual_portal);
+}
+
+void pof_stop(PoFApp* app) {
+    furi_assert(app);
+
+    pof_usb_stop(app->pof_usb);
+}

+ 46 - 0
portal_of_flipper_i.h

@@ -0,0 +1,46 @@
+#pragma once
+
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/widget.h>
+#include <notification/notification_messages.h>
+
+/* generated by fbt from .png files in images folder */
+#include <portal_of_flipper_icons.h>
+
+#include "scenes/pof_scene.h"
+
+#include "helpers/pof_usb.h"
+#include "virtual_portal.h"
+
+typedef struct PoFApp PoFApp;
+
+struct PoFApp {
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    SceneManager* scene_manager;
+    NotificationApp* notifications;
+    Submenu* submenu;
+    Popup* popup;
+    Loading* loading;
+    Widget* widget;
+
+    VirtualPortal* virtual_portal;
+
+    PoFUsb* pof_usb;
+};
+
+typedef enum {
+    PoFViewSubmenu,
+    PoFViewWidget,
+    PoFViewPopup,
+    PoFViewLoading,
+} PoFView;
+
+void pof_start(PoFApp* app);
+void pof_stop(PoFApp* app);
+void pof_show_loading_popup(void* context, bool show);

+ 30 - 0
scenes/pof_scene.c

@@ -0,0 +1,30 @@
+#include "../portal_of_flipper_i.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const pof_scene_on_enter_handlers[])(void*) = {
+#include "pof_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const pof_scene_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "pof_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const pof_scene_on_exit_handlers[])(void* context) = {
+#include "pof_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers pof_scene_handlers = {
+    .on_enter_handlers = pof_scene_on_enter_handlers,
+    .on_event_handlers = pof_scene_on_event_handlers,
+    .on_exit_handlers = pof_scene_on_exit_handlers,
+    .scene_num = PoFSceneNum,
+};

+ 29 - 0
scenes/pof_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) PoFScene##id,
+typedef enum {
+#include "pof_scene_config.h"
+    PoFSceneNum,
+} PoFScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers pof_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "pof_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "pof_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "pof_scene_config.h"
+#undef ADD_SCENE

+ 2 - 0
scenes/pof_scene_config.h

@@ -0,0 +1,2 @@
+ADD_SCENE(pof, main, Main)
+ADD_SCENE(pof, file_select, FileSelect)

+ 39 - 0
scenes/pof_scene_file_select.c

@@ -0,0 +1,39 @@
+#include "../portal_of_flipper_i.h"
+#include "../pof_token.h"
+
+#define TAG "PoFSceneFileSelect"
+
+void pof_scene_file_select_on_enter(void* context) {
+    PoFApp* pof = context;
+    VirtualPortal* virtual_portal = pof->virtual_portal;
+
+    PoFToken* pof_token = NULL;
+    for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
+        if(virtual_portal->tokens[i]->loaded == false) {
+            FURI_LOG_D(TAG, "Loading to slot %d", i);
+            pof_token = virtual_portal->tokens[i];
+            break;
+        }
+    }
+
+    // Process file_select return
+    pof_token_set_loading_callback(pof_token, pof_show_loading_popup, pof);
+
+    if(pof_token && pof_file_select(pof_token)) {
+        scene_manager_next_scene(pof->scene_manager, PoFSceneMain);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(pof->scene_manager, PoFSceneMain);
+    }
+
+    pof_token_set_loading_callback(pof_token, NULL, pof);
+}
+
+bool pof_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void pof_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 85 - 0
scenes/pof_scene_main.c

@@ -0,0 +1,85 @@
+#include "../portal_of_flipper_i.h"
+#include "../pof_token.h"
+
+enum SubmenuIndex {
+    // SubmenuIndexFigure* need to match POF_TOKEN_LIMIT
+    SubmenuIndexFigure1,
+    SubmenuIndexFigure2,
+    SubmenuIndexFigure3,
+    SubmenuIndexFigure4,
+    SubmenuIndexFigure5,
+    SubmenuIndexFigure6,
+    SubmenuIndexFigure7,
+    SubmenuIndexLoad,
+};
+
+void pof_scene_main_submenu_callback(void* context, uint32_t index) {
+    PoFApp* pof = context;
+    view_dispatcher_send_custom_event(pof->view_dispatcher, index);
+}
+
+void pof_scene_main_on_update(void* context) {
+    PoFApp* pof = context;
+    VirtualPortal* virtual_portal = pof->virtual_portal;
+
+    Submenu* submenu = pof->submenu;
+    submenu_reset(pof->submenu);
+
+    int count = 0;
+    for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
+        if(virtual_portal->tokens[i]->loaded) {
+            PoFToken* pof_token = virtual_portal->tokens[i];
+            // Unload figure
+            submenu_add_item(
+                submenu,
+                pof_token->dev_name,
+                SubmenuIndexFigure1 + i,
+                pof_scene_main_submenu_callback,
+                pof);
+            count++;
+        }
+    }
+
+    if(count < POF_TOKEN_LIMIT) {
+        submenu_add_item(
+            submenu, "<Load figure>", SubmenuIndexLoad, pof_scene_main_submenu_callback, pof);
+    }
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(pof->scene_manager, PoFSceneMain));
+    view_dispatcher_switch_to_view(pof->view_dispatcher, PoFViewSubmenu);
+}
+
+void pof_scene_main_on_enter(void* context) {
+    pof_scene_main_on_update(context);
+}
+
+bool pof_scene_main_on_event(void* context, SceneManagerEvent event) {
+    PoFApp* pof = context;
+    VirtualPortal* virtual_portal = pof->virtual_portal;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexLoad) {
+            // Explicitly save state so that the correct item is
+            // reselected if the user cancels loading a file.
+            scene_manager_set_scene_state(pof->scene_manager, PoFSceneMain, SubmenuIndexLoad);
+            scene_manager_next_scene(pof->scene_manager, PoFSceneFileSelect);
+            consumed = true;
+        } else {
+            pof_token_clear(virtual_portal->tokens[event.event], true);
+            pof_scene_main_on_update(context);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_stop(pof->scene_manager);
+        view_dispatcher_stop(pof->view_dispatcher);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void pof_scene_main_on_exit(void* context) {
+    PoFApp* pof = context;
+    submenu_reset(pof->submenu);
+}

+ 221 - 0
virtual_portal.c

@@ -0,0 +1,221 @@
+#include "virtual_portal.h"
+
+#define TAG "VirtualPortal"
+
+#define BLOCK_SIZE 16
+
+static const NotificationSequence pof_sequence_cyan = {
+    &message_blink_start_10,
+    &message_blink_set_color_cyan,
+    NULL,
+};
+
+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++) {
+        virtual_portal->tokens[i] = pof_token_alloc();
+    }
+    virtual_portal->sequence_number = 0;
+    virtual_portal->active = false;
+
+    return virtual_portal;
+}
+
+void virtual_portal_free(VirtualPortal* virtual_portal) {
+    for(int i = 0; i < POF_TOKEN_LIMIT; i++) {
+        pof_token_free(virtual_portal->tokens[i]);
+        virtual_portal->tokens[i] = NULL;
+    }
+
+    free(virtual_portal);
+}
+
+uint8_t virtual_portal_next_sequence(VirtualPortal* virtual_portal) {
+    if(virtual_portal->sequence_number == 0xff) {
+        virtual_portal->sequence_number = 0;
+    }
+    return virtual_portal->sequence_number++;
+}
+
+int virtual_portal_activate(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
+    virtual_portal->active = (message[1] == 1);
+
+    response[0] = message[0];
+    response[1] = message[1];
+    response[2] = 0xFF;
+    response[3] = 0x77;
+    return 4;
+}
+
+int virtual_portal_reset(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
+    UNUSED(message);
+    virtual_portal->active = false;
+    virtual_portal->sequence_number = 0;
+
+    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)
+    return index;
+}
+
+int virtual_portal_status(VirtualPortal* virtual_portal, uint8_t* response) {
+    response[0] = 'S';
+
+    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) {
+            response[1 + i / 4] |= 1 << (i * 2 + 0);
+        }
+        if(virtual_portal->tokens[i]->change) {
+            response[1 + i / 4] |= 1 << (i * 2 + 1);
+        }
+
+        virtual_portal->tokens[i]->change = false;
+    }
+    response[5] = virtual_portal_next_sequence(virtual_portal);
+    response[6] = 1;
+
+    return 7;
+}
+
+int virtual_portal_send_status(VirtualPortal* virtual_portal, uint8_t* response) {
+    if(virtual_portal->active) {
+        notification_message(virtual_portal->notifications, &pof_sequence_cyan);
+        return virtual_portal_status(virtual_portal, response);
+    }
+    return 0;
+}
+
+// 4d01ff0000d0077d6c2a77a400000000
+int virtual_portal_m(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
+    UNUSED(virtual_portal);
+    virtual_portal->speaker = (message[1] == 1);
+
+    char display[33] = {0};
+    for(size_t i = 0; i < BLOCK_SIZE; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", message[i]);
+    }
+    FURI_LOG_I(TAG, "M %s", display);
+
+    size_t index = 0;
+    response[index++] = 'M';
+    response[index++] = message[1];
+    response[index++] = 0x00;
+    response[index++] = 0x19;
+    return index;
+}
+
+int virtual_portal_j(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
+    UNUSED(virtual_portal);
+
+    char display[33] = {0};
+    for(size_t i = 0; i < BLOCK_SIZE; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", message[i]);
+    }
+    // FURI_LOG_I(TAG, "J %s", display);
+
+    // https://marijnkneppers.dev/posts/reverse-engineering-skylanders-toys-to-life-mechanics/
+    size_t index = 0;
+    response[index++] = 'J';
+    return index;
+}
+
+int virtual_portal_query(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
+    int index = message[1];
+    int blockNum = message[2];
+    int arrayIndex = index & 0x0f;
+    FURI_LOG_I(TAG, "Query %d %d", arrayIndex, blockNum);
+
+    PoFToken* pof_token = virtual_portal->tokens[arrayIndex];
+    NfcDevice* nfc_device = pof_token->nfc_device;
+    const MfClassicData* data = nfc_device_get_data(nfc_device, NfcProtocolMfClassic);
+    const MfClassicBlock block = data->block[blockNum];
+
+    response[0] = 'Q';
+    response[1] = 0x20 | arrayIndex;
+    response[2] = blockNum;
+    memcpy(response + 3, block.data, BLOCK_SIZE);
+    return 3 + BLOCK_SIZE;
+}
+
+int virtual_portal_write(VirtualPortal* virtual_portal, uint8_t* message, uint8_t* response) {
+    int index = message[1];
+    int blockNum = message[2];
+    int arrayIndex = index & 0x0f;
+
+    char display[33] = {0};
+    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];
+    NfcDevice* nfc_device = pof_token->nfc_device;
+
+    MfClassicData* data = mf_classic_alloc();
+    nfc_device_copy_data(nfc_device, NfcProtocolMfClassic, data);
+    MfClassicBlock* block = &data->block[blockNum];
+
+    memcpy(block->data, message + 3, BLOCK_SIZE);
+    nfc_device_set_data(nfc_device, NfcProtocolMfClassic, data);
+
+    mf_classic_free(data);
+
+    response[0] = 'W';
+    response[1] = index;
+    response[2] = blockNum;
+    return 3;
+}
+
+// 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':
+        FURI_LOG_D(TAG, "process %c", message[0]);
+        return virtual_portal_activate(virtual_portal, message, response);
+    case 'C': //Ring color R G B
+        return 0;
+    case 'J':
+        // https://github.com/flyandi/flipper_zero_rgb_led
+        return virtual_portal_j(virtual_portal, message, response);
+    case 'L':
+        return 0; //No response
+    case 'M':
+        return virtual_portal_m(virtual_portal, message, response);
+    case 'Q': //Query
+        FURI_LOG_D(TAG, "process %c", message[0]);
+        return virtual_portal_query(virtual_portal, message, response);
+    case 'R':
+        FURI_LOG_D(TAG, "process %c", message[0]);
+        return virtual_portal_reset(virtual_portal, message, response);
+    case 'S': //Status
+        FURI_LOG_D(TAG, "process %c", message[0]);
+        return virtual_portal_status(virtual_portal, response);
+    case 'V':
+        return 0;
+    case 'W': //Write
+        FURI_LOG_D(TAG, "process %c", message[0]);
+        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;
+}

+ 25 - 0
virtual_portal.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <notification/notification_messages.h>
+#include "pof_token.h"
+
+#define POF_TOKEN_LIMIT 7
+
+typedef struct {
+    PoFToken* tokens[POF_TOKEN_LIMIT];
+    uint8_t sequence_number;
+    bool active;
+    bool speaker;
+    NotificationApp* notifications;
+} VirtualPortal;
+
+VirtualPortal* virtual_portal_alloc(NotificationApp* notifications);
+
+void virtual_portal_free(VirtualPortal* virtual_portal);
+
+int virtual_portal_process_message(
+    VirtualPortal* virtual_portal,
+    uint8_t* message,
+    uint8_t* response);
+
+int virtual_portal_send_status(VirtualPortal* virtual_portal, uint8_t* response);