Bläddra i källkod

Merge portal_of_flipper from https://github.com/sanjay900/portal_of_flipper

# Conflicts:
#	portal_of_flipper/helpers/pof_usb.c
Willy-JL 9 månader sedan
förälder
incheckning
5d3909ce9f

+ 6 - 2
portal_of_flipper/.catalog/readme.md

@@ -1,5 +1,10 @@
+Original App by bettse
+
 App Icon by mikeonut
 
+Additional protocol reverse engineering and testing by norto
+
+
 ## Requirements
 
 NFC files must:
@@ -13,5 +18,4 @@ NFC files must:
 - Be sure you don't have qFlipper or lab.flipper.net connected to the flipper when you launch (this will cause the USB emulation to fail to start).
 - Use 'Load figure' to select a .nfc file to load
 - Figure, if loaded successfully, will appear in list
-- Press center when figure highlighted to remove (and save updated data to disk)
-- Figures are not saved to disk except on unload, so be sure to save before quitting app
+- Press center when figure highlighted to remove

+ 1 - 0
portal_of_flipper/.gitignore

@@ -5,3 +5,4 @@ dist/*
 .editorconfig
 .env
 .ufbt
+scripts

+ 6 - 0
portal_of_flipper/CHANGELOG.md

@@ -1,2 +1,8 @@
+## 1.2
+ - Several bug fixes and changes to make the portal emulation closer to the real portals
+ - Use the flippers speaker to emulate the portals speaker
+## 1.1
+ - Support for emulating xbox 360 portals
+ - Use the backlight and RGB led to emulate the portals LEDs
 ## 1.0
  - Public release

+ 5 - 3
portal_of_flipper/README.md

@@ -2,8 +2,12 @@
 
 USB Emulator
 
+Original App by bettse
+
 App Icon by mikeonut
 
+Additional protocol reverse engineering and testing by norto
+
 ## Requirements
 
 NFC files must:
@@ -17,10 +21,8 @@ NFC files must:
 - Be sure you don't have qFlipper or lab.flipper.net connected to the flipper when you launch (this will cause the USB emulation to fail to start).
 - Use 'Load figure' to select a .nfc file to load
 - Figure, if loaded successfully, will appear in list
-- Press center when figure highlighted to remove (and save updated data to disk)
-- Figures are not saved to disk except on unload, so be sure to save before quitting app
+- Press center when figure highlighted to remove
 
 ## 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

+ 3 - 3
portal_of_flipper/application.fam

@@ -8,10 +8,10 @@ App(
     stack_size=5 * 1024,
     fap_category="USB",
     # Optional values
-    fap_version="1.0",
+    fap_version="1.2",
     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_author="sanjay900",
+    fap_weburl="https://github.com/sanjay900/portal_of_flipper",
     fap_icon_assets="images",  # Image assets to compile for this application
 )

+ 543 - 0
portal_of_flipper/audio/g721.c

@@ -0,0 +1,543 @@
+/*
+ * This source code is a product of Sun Microsystems, Inc. and is provided
+ * for unrestricted use.  Users may copy or modify this source code without
+ * charge.
+ *
+ * SUN SOURCE CODE IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING
+ * THE WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
+ *
+ * Sun source code is provided with no support and without any obligation on
+ * the part of Sun Microsystems, Inc. to assist in its use, correction,
+ * modification or enhancement.
+ *
+ * SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE
+ * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THIS SOFTWARE
+ * OR ANY PART THEREOF.
+ *
+ * In no event will Sun Microsystems, Inc. be liable for any lost revenue
+ * or profits or other special, indirect and consequential damages, even if
+ * Sun has been advised of the possibility of such damages.
+ *
+ * Sun Microsystems, Inc.
+ * 2550 Garcia Avenue
+ * Mountain View, California  94043
+ */
+
+/*
+ * g721.c
+ *
+ * Description:
+ *
+ * g721_encoder(), g721_decoder()
+ *
+ * These routines comprise an implementation of the CCITT G.721 ADPCM
+ * coding algorithm.  Essentially, this implementation is identical to
+ * the bit level description except for a few deviations which
+ * take advantage of work station attributes, such as hardware 2's
+ * complement arithmetic and large memory.  Specifically, certain time
+ * consuming operations such as multiplications are replaced
+ * with lookup tables and software 2's complement operations are
+ * replaced with hardware 2's complement.
+ *
+ * The deviation from the bit level specification (lookup tables)
+ * preserves the bit level performance specifications.
+ *
+ * As outlined in the G.721 Recommendation, the algorithm is broken
+ * down into modules.  Each section of code below is preceded by
+ * the name of the module which it is implementing.
+ *
+ */
+#include "g721.h"
+#include <stdlib.h>
+
+static short qtab_721[7] = { -124, 80, 178, 246, 300, 349, 400 };
+/*
+ * Maps G.721 code word to reconstructed scale factor normalized log
+ * magnitude values.
+ */
+static short _dqlntab[16] = { -2048, 4,   135, 213, 273, 323, 373, 425,
+                              425,   373, 323, 273, 213, 135, 4,   -2048 };
+
+/* Maps G.721 code word to log of scale factor multiplier. */
+static short _witab[16] = { -12,  18,  41,  64,  112, 198, 355, 1122,
+                            1122, 355, 198, 112, 64,  41,  18,  -12 };
+/*
+ * Maps G.721 code words to a set of values whose long and short
+ * term averages are computed and then compared to give an indication
+ * how stationary (steady state) the signal is.
+ */
+static short _fitab[16] = { 0,     0,     0,     0x200, 0x200, 0x200, 0x600, 0xE00,
+                            0xE00, 0x600, 0x200, 0x200, 0x200, 0,     0,     0 };
+
+/*
+ * g721_encoder()
+ *
+ * Encodes the input value of linear PCM from sl and returns
+ * the resulting code. 
+ */
+int g721_encoder(int sl, struct g72x_state* state_ptr)
+{
+    short sezi, se, sez; /* ACCUM */
+    short d;             /* SUBTA */
+    short sr;            /* ADDB */
+    short y;             /* MIX */
+    short dqsez;         /* ADDC */
+    short dq, i;
+
+    sl >>= 2; /* linearize input sample to 14-bit PCM */
+
+    sezi = predictor_zero(state_ptr);
+    sez = sezi >> 1;
+    se = (sezi + predictor_pole(state_ptr)) >> 1; /* estimated signal */
+
+    d = sl - se; /* estimation difference */
+
+    /* quantize the prediction difference */
+    y = step_size(state_ptr);        /* quantizer step size */
+    i = quantize(d, y, qtab_721, 7); /* i = ADPCM code */
+
+    dq = reconstruct(i & 8, _dqlntab[i], y); /* quantized est diff */
+
+    sr = (dq < 0) ? se - (dq & 0x3FFF) : se + dq; /* reconst. signal */
+
+    dqsez = sr + sez - se; /* pole prediction diff. */
+
+    update(4, y, _witab[i] << 5, _fitab[i], dq, sr, dqsez, state_ptr);
+
+    return (i);
+}
+
+/*
+ * g721_decoder()
+ *
+ * Description:
+ *
+ * Decodes a 4-bit code of G.721 encoded data of i and
+ * returns the resulting linear PCM
+ */
+int g721_decoder(int i, struct g72x_state* state_ptr)
+{
+    short sezi, sei, sez, se; /* ACCUM */
+    short y;                  /* MIX */
+    short sr;                 /* ADDB */
+    short dq;
+    short dqsez;
+
+    i &= 0x0f; /* mask to get proper bits */
+    sezi = predictor_zero(state_ptr);
+    sez = sezi >> 1;
+    sei = sezi + predictor_pole(state_ptr);
+    se = sei >> 1; /* se = estimated signal */
+
+    y = step_size(state_ptr); /* dynamic quantizer step size */
+
+    dq = reconstruct(i & 0x08, _dqlntab[i], y); /* quantized diff. */
+
+    sr = (dq < 0) ? (se - (dq & 0x3FFF)) : se + dq; /* reconst. signal */
+
+    dqsez = sr - se + sez; /* pole prediction diff. */
+
+    update(4, y, _witab[i] << 5, _fitab[i], dq, sr, dqsez, state_ptr);
+
+    return (sr << 2);
+}
+
+
+static short power2[15] = { 1,     2,     4,     8,     0x10,   0x20,   0x40,  0x80,
+                            0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000 };
+
+/*
+ * quan()
+ *
+ * quantizes the input val against the table of size short integers.
+ * It returns i if table[i - 1] <= val < table[i].
+ *
+ * Using linear search for simple coding.
+ */
+static int quan(int val, short* table, int size)
+{
+    int i;
+
+    for (i = 0; i < size; i++)
+        if (val < *table++)
+            break;
+    return (i);
+}
+
+/*
+ * fmult()
+ *
+ * returns the integer product of the 14-bit integer "an" and
+ * "floating point" representation (4-bit exponent, 6-bit mantessa) "srn".
+ */
+static int fmult(int an, int srn)
+{
+    short anmag, anexp, anmant;
+    short wanexp, wanmant;
+    short retval;
+
+    anmag = (an > 0) ? an : ((-an) & 0x1FFF);
+    anexp = quan(anmag, power2, 15) - 6;
+    anmant = (anmag == 0) ? 32 : (anexp >= 0) ? anmag >> anexp : anmag << -anexp;
+    wanexp = anexp + ((srn >> 6) & 0xF) - 13;
+
+    wanmant = (anmant * (srn & 077) + 0x30) >> 4;
+    retval = (wanexp >= 0) ? ((wanmant << wanexp) & 0x7FFF) : (wanmant >> -wanexp);
+
+    return (((an ^ srn) < 0) ? -retval : retval);
+}
+
+
+
+/*
+ * update()
+ *
+ * updates the state variables for each output code
+ */
+void update(int code_size,                /* distinguish 723_40 with others */
+            int y,                        /* quantizer step size */
+            int wi,                       /* scale factor multiplier */
+            int fi,                       /* for long/short term energies */
+            int dq,                       /* quantized prediction difference */
+            int sr,                       /* reconstructed signal */
+            int dqsez,                    /* difference from 2-pole predictor */
+            struct g72x_state* state_ptr) /* coder state pointer */
+{
+    int cnt;
+    short mag, exp; /* Adaptive predictor, FLOAT A */
+    short a2p = 0;  /* LIMC */
+    short a1ul;     /* UPA1 */
+    short pks1;     /* UPA2 */
+    short fa1;
+    char tr; /* tone/transition detector */
+    short ylint, thr2, dqthr;
+    short ylfrac, thr1;
+    short pk0;
+
+    pk0 = (dqsez < 0) ? 1 : 0; /* needed in updating predictor poles */
+
+    mag = dq & 0x7FFF; /* prediction difference magnitude */
+    /* TRANS */
+    ylint = state_ptr->yl >> 15;           /* exponent part of yl */
+    ylfrac = (state_ptr->yl >> 10) & 0x1F; /* fractional part of yl */
+    thr1 = (32 + ylfrac) << ylint;         /* threshold */
+    thr2 = (ylint > 9) ? 31 << 10 : thr1;  /* limit thr2 to 31 << 10 */
+    dqthr = (thr2 + (thr2 >> 1)) >> 1;     /* dqthr = 0.75 * thr2 */
+    if (state_ptr->td == 0)                /* signal supposed voice */
+        tr = 0;
+    else if (mag <= dqthr) /* supposed data, but small mag */
+        tr = 0;            /* treated as voice */
+    else                   /* signal is data (modem) */
+        tr = 1;
+
+    /*
+     * Quantizer scale factor adaptation.
+     */
+
+    /* FUNCTW & FILTD & DELAY */
+    /* update non-steady state step size multiplier */
+    state_ptr->yu = y + ((wi - y) >> 5);
+
+    /* LIMB */
+    if (state_ptr->yu < 544) /* 544 <= yu <= 5120 */
+        state_ptr->yu = 544;
+    else if (state_ptr->yu > 5120)
+        state_ptr->yu = 5120;
+
+    /* FILTE & DELAY */
+    /* update steady state step size multiplier */
+    state_ptr->yl += state_ptr->yu + ((-state_ptr->yl) >> 6);
+
+    /*
+     * Adaptive predictor coefficients.
+     */
+    if (tr == 1) { /* reset a's and b's for modem signal */
+        state_ptr->a[0] = 0;
+        state_ptr->a[1] = 0;
+        state_ptr->b[0] = 0;
+        state_ptr->b[1] = 0;
+        state_ptr->b[2] = 0;
+        state_ptr->b[3] = 0;
+        state_ptr->b[4] = 0;
+        state_ptr->b[5] = 0;
+    } else {                           /* update a's and b's */
+        pks1 = pk0 ^ state_ptr->pk[0]; /* UPA2 */
+
+        /* update predictor pole a[1] */
+        a2p = state_ptr->a[1] - (state_ptr->a[1] >> 7);
+        if (dqsez != 0) {
+            fa1 = (pks1) ? state_ptr->a[0] : -state_ptr->a[0];
+            if (fa1 < -8191) /* a2p = function of fa1 */
+                a2p -= 0x100;
+            else if (fa1 > 8191)
+                a2p += 0xFF;
+            else
+                a2p += fa1 >> 5;
+
+            if (pk0 ^ state_ptr->pk[1])
+                /* LIMC */
+                if (a2p <= -12160)
+                    a2p = -12288;
+                else if (a2p >= 12416)
+                    a2p = 12288;
+                else
+                    a2p -= 0x80;
+            else if (a2p <= -12416)
+                a2p = -12288;
+            else if (a2p >= 12160)
+                a2p = 12288;
+            else
+                a2p += 0x80;
+        }
+
+        /* TRIGB & DELAY */
+        state_ptr->a[1] = a2p;
+
+        /* UPA1 */
+        /* update predictor pole a[0] */
+        state_ptr->a[0] -= state_ptr->a[0] >> 8;
+        if (dqsez != 0) {
+            if (pks1 == 0)
+                state_ptr->a[0] += 192;
+            else
+                state_ptr->a[0] -= 192;
+        }
+
+        /* LIMD */
+        a1ul = 15360 - a2p;
+        if (state_ptr->a[0] < -a1ul)
+            state_ptr->a[0] = -a1ul;
+        else if (state_ptr->a[0] > a1ul)
+            state_ptr->a[0] = a1ul;
+
+        /* UPB : update predictor zeros b[6] */
+        for (cnt = 0; cnt < 6; cnt++) {
+            if (code_size == 5) /* for 40Kbps G.723 */
+                state_ptr->b[cnt] -= state_ptr->b[cnt] >> 9;
+            else /* for G.721 and 24Kbps G.723 */
+                state_ptr->b[cnt] -= state_ptr->b[cnt] >> 8;
+            if (dq & 0x7FFF) { /* XOR */
+                if ((dq ^ state_ptr->dq[cnt]) >= 0)
+                    state_ptr->b[cnt] += 128;
+                else
+                    state_ptr->b[cnt] -= 128;
+            }
+        }
+    }
+
+    for (cnt = 5; cnt > 0; cnt--)
+        state_ptr->dq[cnt] = state_ptr->dq[cnt - 1];
+    /* FLOAT A : convert dq[0] to 4-bit exp, 6-bit mantissa f.p. */
+    if (mag == 0) {
+        state_ptr->dq[0] = (dq >= 0) ? 0x20 : 0xFC20;
+    } else {
+        exp = quan(mag, power2, 15);
+        state_ptr->dq[0] = (dq >= 0) ? (exp << 6) + ((mag << 6) >> exp)
+                                     : (exp << 6) + ((mag << 6) >> exp) - 0x400;
+    }
+
+    state_ptr->sr[1] = state_ptr->sr[0];
+    /* FLOAT B : convert sr to 4-bit exp., 6-bit mantissa f.p. */
+    if (sr == 0) {
+        state_ptr->sr[0] = 0x20;
+    } else if (sr > 0) {
+        exp = quan(sr, power2, 15);
+        state_ptr->sr[0] = (exp << 6) + ((sr << 6) >> exp);
+    } else if (sr > -32768) {
+        mag = -sr;
+        exp = quan(mag, power2, 15);
+        state_ptr->sr[0] = (exp << 6) + ((mag << 6) >> exp) - 0x400;
+    } else
+        state_ptr->sr[0] = 0xFC20;
+
+    /* DELAY A */
+    state_ptr->pk[1] = state_ptr->pk[0];
+    state_ptr->pk[0] = pk0;
+
+    /* TONE */
+    if (tr == 1)           /* this sample has been treated as data */
+        state_ptr->td = 0; /* next one will be treated as voice */
+    else if (a2p < -11776) /* small sample-to-sample correlation */
+        state_ptr->td = 1; /* signal may be data */
+    else                   /* signal is voice */
+        state_ptr->td = 0;
+
+    /*
+     * Adaptation speed control.
+     */
+    state_ptr->dms += (fi - state_ptr->dms) >> 5;          /* FILTA */
+    state_ptr->dml += (((fi << 2) - state_ptr->dml) >> 7); /* FILTB */
+
+    if (tr == 1)
+        state_ptr->ap = 256;
+    else if (y < 1536) /* SUBTC */
+        state_ptr->ap += (0x200 - state_ptr->ap) >> 4;
+    else if (state_ptr->td == 1)
+        state_ptr->ap += (0x200 - state_ptr->ap) >> 4;
+    else if (abs((state_ptr->dms << 2) - state_ptr->dml) >= (state_ptr->dml >> 3))
+        state_ptr->ap += (0x200 - state_ptr->ap) >> 4;
+    else
+        state_ptr->ap += (-state_ptr->ap) >> 4;
+}
+
+
+/*
+ * g72x_init_state()
+ *
+ * This routine initializes and/or resets the g72x_state structure
+ * pointed to by 'state_ptr'.
+ * All the initial state values are specified in the CCITT G.721 document.
+ */
+void g72x_init_state(struct g72x_state* state_ptr)
+{
+    int cnta;
+
+    state_ptr->yl = 34816;
+    state_ptr->yu = 544;
+    state_ptr->dms = 0;
+    state_ptr->dml = 0;
+    state_ptr->ap = 0;
+    for (cnta = 0; cnta < 2; cnta++) {
+        state_ptr->a[cnta] = 0;
+        state_ptr->pk[cnta] = 0;
+        state_ptr->sr[cnta] = 32;
+    }
+    for (cnta = 0; cnta < 6; cnta++) {
+        state_ptr->b[cnta] = 0;
+        state_ptr->dq[cnta] = 32;
+    }
+    state_ptr->td = 0;
+}
+
+/*
+ * predictor_zero()
+ *
+ * computes the estimated signal from 6-zero predictor.
+ *
+ */
+int predictor_zero(struct g72x_state* state_ptr)
+{
+    int i;
+    int sezi;
+
+    sezi = fmult(state_ptr->b[0] >> 2, state_ptr->dq[0]);
+    for (i = 1; i < 6; i++) /* ACCUM */
+        sezi += fmult(state_ptr->b[i] >> 2, state_ptr->dq[i]);
+    return (sezi);
+}
+/*
+ * predictor_pole()
+ *
+ * computes the estimated signal from 2-pole predictor.
+ *
+ */
+int predictor_pole(struct g72x_state* state_ptr)
+{
+    return (fmult(state_ptr->a[1] >> 2, state_ptr->sr[1]) +
+            fmult(state_ptr->a[0] >> 2, state_ptr->sr[0]));
+}
+/*
+ * step_size()
+ *
+ * computes the quantization step size of the adaptive quantizer.
+ *
+ */
+int step_size(struct g72x_state* state_ptr)
+{
+    int y;
+    int dif;
+    int al;
+
+    if (state_ptr->ap >= 256)
+        return (state_ptr->yu);
+    else {
+        y = state_ptr->yl >> 6;
+        dif = state_ptr->yu - y;
+        al = state_ptr->ap >> 2;
+        if (dif > 0)
+            y += (dif * al) >> 6;
+        else if (dif < 0)
+            y += (dif * al + 0x3F) >> 6;
+        return (y);
+    }
+}
+
+/*
+ * quantize()
+ *
+ * Given a raw sample, 'd', of the difference signal and a
+ * quantization step size scale factor, 'y', this routine returns the
+ * ADPCM codeword to which that sample gets quantized.  The step
+ * size scale factor division operation is done in the log base 2 domain
+ * as a subtraction.
+ */
+int quantize(int d,        /* Raw difference signal sample */
+             int y,        /* Step size multiplier */
+             short* table, /* quantization table */
+             int size)     /* table size of short integers */
+{
+    short dqm;  /* Magnitude of 'd' */
+    short exp;  /* Integer part of base 2 log of 'd' */
+    short mant; /* Fractional part of base 2 log */
+    short dl;   /* Log of magnitude of 'd' */
+    short dln;  /* Step size scale factor normalized log */
+    int i;
+
+    /*
+     * LOG
+     *
+     * Compute base 2 log of 'd', and store in 'dl'.
+     */
+    dqm = abs(d);
+    exp = quan(dqm >> 1, power2, 15);
+    mant = ((dqm << 7) >> exp) & 0x7F; /* Fractional portion. */
+    dl = (exp << 7) + mant;
+
+    /*
+     * SUBTB
+     *
+     * "Divide" by step size multiplier.
+     */
+    dln = dl - (y >> 2);
+
+    /*
+     * QUAN
+     *
+     * Obtain codword i for 'd'.
+     */
+    i = quan(dln, table, size);
+    if (d < 0) /* take 1's complement of i */
+        return ((size << 1) + 1 - i);
+    else if (i == 0)              /* take 1's complement of 0 */
+        return ((size << 1) + 1); /* new in 1988 */
+    else
+        return (i);
+}
+/*
+ * reconstruct()
+ *
+ * Returns reconstructed difference signal 'dq' obtained from
+ * codeword 'i' and quantization step size scale factor 'y'.
+ * Multiplication is performed in log base 2 domain as addition.
+ */
+int reconstruct(int sign, /* 0 for non-negative value */
+                int dqln, /* G.72x codeword */
+                int y)    /* Step size multiplier */
+{
+    short dql; /* Log of 'dq' magnitude */
+    short dex; /* Integer part of log */
+    short dqt;
+    short dq; /* Reconstructed difference signal sample */
+
+    dql = dqln + (y >> 2); /* ADDA */
+
+    if (dql < 0) {
+        return ((sign) ? -0x8000 : 0);
+    } else { /* ANTILOG */
+        dex = (dql >> 7) & 15;
+        dqt = 128 + (dql & 127);
+        dq = (dqt << 7) >> (14 - dex);
+        return ((sign) ? (dq - 0x8000) : dq);
+    }
+}

+ 96 - 0
portal_of_flipper/audio/g721.h

@@ -0,0 +1,96 @@
+/*
+ * This source code is a product of Sun Microsystems, Inc. and is provided
+ * for unrestricted use.  Users may copy or modify this source code without
+ * charge.
+ *
+ * SUN SOURCE CODE IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING
+ * THE WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
+ *
+ * Sun source code is provided with no support and without any obligation on
+ * the part of Sun Microsystems, Inc. to assist in its use, correction,
+ * modification or enhancement.
+ *
+ * SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE
+ * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THIS SOFTWARE
+ * OR ANY PART THEREOF.
+ *
+ * In no event will Sun Microsystems, Inc. be liable for any lost revenue
+ * or profits or other special, indirect and consequential damages, even if
+ * Sun has been advised of the possibility of such damages.
+ *
+ * Sun Microsystems, Inc.
+ * 2550 Garcia Avenue
+ * Mountain View, California  94043
+ */
+
+/*
+ * g72x.h
+ *
+ * Header file for CCITT conversion routines.
+ *
+ */
+#ifndef _G72X_H
+#define _G72X_H
+
+/*
+ * The following is the definition of the state structure
+ * used by the G.721/G.723 encoder and decoder to preserve their internal
+ * state between successive calls.  The meanings of the majority
+ * of the state structure fields are explained in detail in the
+ * CCITT Recommendation G.721.  The field names are essentially identical
+ * to variable names in the bit level description of the coding algorithm
+ * included in this Recommendation.
+ */
+struct g72x_state {
+    long yl;   /* Locked or steady state step size multiplier. */
+    short yu;  /* Unlocked or non-steady state step size multiplier. */
+    short dms; /* Short term energy estimate. */
+    short dml; /* Long term energy estimate. */
+    short ap;  /* Linear weighting coefficient of 'yl' and 'yu'. */
+
+    short a[2];  /* Coefficients of pole portion of prediction filter. */
+    short b[6];  /* Coefficients of zero portion of prediction filter. */
+    short pk[2]; /*
+                  * Signs of previous two samples of a partially
+                  * reconstructed signal.
+                  */
+    short dq[6]; /*
+                  * Previous 6 samples of the quantized difference
+                  * signal represented in an internal floating point
+                  * format.
+                  */
+    short sr[2]; /*
+                  * Previous 2 samples of the quantized difference
+                  * signal represented in an internal floating point
+                  * format.
+                  */
+    char td;     /* delayed tone detect, new in 1988 version */
+};
+
+/* External function definitions. */
+
+void g72x_init_state(struct g72x_state*);
+int g721_encoder(int sample, struct g72x_state* state_ptr);
+int g721_decoder(int code, struct g72x_state* state_ptr);
+
+
+int quantize(int d, int y, short* table, int size);
+int reconstruct(int, int, int);
+void
+
+    update(int code_size,
+                  int y,
+                  int wi,
+                  int fi,
+                  int dq,
+                  int sr,
+                  int dqsez,
+                  struct g72x_state* state_ptr);
+
+int predictor_zero(struct g72x_state* state_ptr);
+
+int predictor_pole(struct g72x_state* state_ptr);
+int step_size(struct g72x_state* state_ptr);
+#endif /* !_G72X_H */
+

+ 112 - 0
portal_of_flipper/audio/wav_player_hal.c

@@ -0,0 +1,112 @@
+#include "wav_player_hal.h"
+#include <stm32wbxx_ll_tim.h>
+#include <stm32wbxx_ll_dma.h>
+
+#include <stm32wbxx_ll_gpio.h>
+#include <furi_hal.h>
+#include <furi_hal_gpio.h>
+#include <furi_hal_resources.h>
+
+//#define FURI_HAL_SPEAKER_TIMER TIM16
+
+#define FURI_HAL_SPEAKER_TIMER TIM16
+
+#define SAMPLE_RATE_TIMER TIM2
+
+#define FURI_HAL_SPEAKER_CHANNEL LL_TIM_CHANNEL_CH1
+#define DMA_INSTANCE DMA1, LL_DMA_CHANNEL_1
+
+void wav_player_speaker_init(uint32_t sample_rate) {
+    // Enable bus
+    furi_hal_bus_enable(FuriHalBusTIM2);
+
+    LL_TIM_InitTypeDef TIM_InitStruct = {0};
+    //TIM_InitStruct.Prescaler = 4;
+    TIM_InitStruct.Prescaler = 1;
+    TIM_InitStruct.Autoreload =
+        255; //in this fork used purely as PWM timer, the DMA now is triggered by SAMPLE_RATE_TIMER
+    LL_TIM_Init(FURI_HAL_SPEAKER_TIMER, &TIM_InitStruct);
+
+    LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    TIM_OC_InitStruct.CompareValue = 127;
+    LL_TIM_OC_Init(FURI_HAL_SPEAKER_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct);
+
+    //======================================================
+
+    TIM_InitStruct.Prescaler = 0;
+    //TIM_InitStruct.Autoreload = 1451; //64 000 000 / 1451 ~= 44100 Hz
+
+    TIM_InitStruct.Autoreload = SystemCoreClock / sample_rate - 1; //to support various sample rates
+
+    LL_TIM_Init(SAMPLE_RATE_TIMER, &TIM_InitStruct);
+
+    //LL_TIM_OC_InitTypeDef TIM_OC_InitStruct = {0};
+    TIM_OC_InitStruct.OCMode = LL_TIM_OCMODE_PWM1;
+    TIM_OC_InitStruct.OCState = LL_TIM_OCSTATE_ENABLE;
+    TIM_OC_InitStruct.CompareValue = 0;
+    LL_TIM_OC_Init(SAMPLE_RATE_TIMER, FURI_HAL_SPEAKER_CHANNEL, &TIM_OC_InitStruct);
+
+    //=========================================================
+    //configuring PA6 pin as TIM16 output
+
+    furi_hal_gpio_init_ex(
+        &gpio_ext_pa6,
+        GpioModeAltFunctionPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFn14TIM16);
+}
+
+void wav_player_hal_deinit() {
+    furi_hal_gpio_init(&gpio_ext_pa6, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
+
+    // Disable bus
+    furi_hal_bus_disable(FuriHalBusTIM2);
+}
+
+void wav_player_speaker_start() {
+    LL_TIM_EnableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+    LL_TIM_EnableCounter(FURI_HAL_SPEAKER_TIMER);
+
+    LL_TIM_EnableAllOutputs(SAMPLE_RATE_TIMER);
+    LL_TIM_EnableCounter(SAMPLE_RATE_TIMER);
+}
+
+void wav_player_speaker_stop() {
+    LL_TIM_DisableAllOutputs(FURI_HAL_SPEAKER_TIMER);
+    LL_TIM_DisableCounter(FURI_HAL_SPEAKER_TIMER);
+
+    LL_TIM_DisableAllOutputs(SAMPLE_RATE_TIMER);
+    LL_TIM_DisableCounter(SAMPLE_RATE_TIMER);
+}
+
+void wav_player_dma_init(uint32_t address, size_t size) {
+    uint32_t dma_dst = (uint32_t) & (FURI_HAL_SPEAKER_TIMER->CCR1);
+
+    LL_DMA_ConfigAddresses(DMA_INSTANCE, address, dma_dst, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetDataLength(DMA_INSTANCE, size);
+
+    LL_DMA_SetPeriphRequest(DMA_INSTANCE, LL_DMAMUX_REQ_TIM2_UP);
+    LL_DMA_SetDataTransferDirection(DMA_INSTANCE, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
+    LL_DMA_SetChannelPriorityLevel(DMA_INSTANCE, LL_DMA_PRIORITY_VERYHIGH);
+    LL_DMA_SetMode(DMA_INSTANCE, LL_DMA_MODE_CIRCULAR);
+    LL_DMA_SetPeriphIncMode(DMA_INSTANCE, LL_DMA_PERIPH_NOINCREMENT);
+    LL_DMA_SetMemoryIncMode(DMA_INSTANCE, LL_DMA_MEMORY_INCREMENT);
+    LL_DMA_SetPeriphSize(DMA_INSTANCE, LL_DMA_PDATAALIGN_HALFWORD);
+    LL_DMA_SetMemorySize(DMA_INSTANCE, LL_DMA_MDATAALIGN_BYTE);
+
+    LL_DMA_EnableIT_TC(DMA_INSTANCE);
+    LL_DMA_EnableIT_HT(DMA_INSTANCE);
+}
+
+void wav_player_dma_start() {
+    LL_DMA_EnableChannel(DMA_INSTANCE);
+    LL_TIM_EnableDMAReq_UPDATE(SAMPLE_RATE_TIMER);
+}
+
+void wav_player_dma_stop() {
+    LL_DMA_DisableChannel(DMA_INSTANCE);
+}
+

+ 25 - 0
portal_of_flipper/audio/wav_player_hal.h

@@ -0,0 +1,25 @@
+#pragma once
+#include <stdint.h>
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void wav_player_speaker_init(uint32_t sample_rate);
+
+void wav_player_speaker_start();
+
+void wav_player_speaker_stop();
+
+void wav_player_dma_init(uint32_t address, size_t size);
+
+void wav_player_dma_start();
+
+void wav_player_dma_stop();
+
+void wav_player_hal_deinit();
+
+#ifdef __cplusplus
+}
+#endif

+ 21 - 49
portal_of_flipper/helpers/pof_usb.c

@@ -12,12 +12,6 @@
 #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 =
@@ -36,38 +30,6 @@ static usbd_respond
 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) {
@@ -78,15 +40,13 @@ static int32_t pof_thread_worker(void* context) {
 
     uint32_t len_data = 0;
     uint8_t tx_data[POF_USB_TX_MAX_SIZE] = {0};
-    uint32_t timeout = 30; // FuriWaitForever; //ms
-    uint32_t lastStatus = 0x0;
+    uint32_t timeout = TIMEOUT_NORMAL; // FuriWaitForever; //ms
+    uint32_t last = 0;
 
     while(true) {
         uint32_t now = furi_get_tick();
         uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, timeout);
-        if(flags & EventRx) { //fast flag
-            UNUSED(pof_usb_receive);
-
+        if(flags & EventRx) { // fast flag
             if(virtual_portal->speaker) {
                 uint8_t buf[POF_USB_RX_MAX_SIZE];
                 len_data = pof_usb_receive(dev, buf, POF_USB_RX_MAX_SIZE);
@@ -99,27 +59,36 @@ static int32_t pof_thread_worker(void* context) {
                     }
                     FURI_LOG_RAW_I("\r\n");
                     */
+                    virtual_portal_process_audio(virtual_portal, buf, len_data);
                 }
             }
-
             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);
+                    timeout = TIMEOUT_AFTER_RESPONSE;
+                    if(virtual_portal->speaker) {
+                        timeout = TIMEOUT_AFTER_MUSIC;
+                    }
+                    last = now;
                 }
                 pof_usb->dataAvailable = 0;
             }
 
             // Check next status time since the timeout based one might be starved by incoming packets.
-            if(now > lastStatus + timeout) {
-                lastStatus = now;
+            if(now > last + 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);
                 }
+                last = now;
+                timeout = TIMEOUT_NORMAL;
+                if(virtual_portal->speaker) {
+                    timeout = TIMEOUT_AFTER_MUSIC;
+                }
             }
 
             flags &= ~EventRx; // clear flag
@@ -156,7 +125,11 @@ static int32_t pof_thread_worker(void* context) {
             if(len_data > 0) {
                 pof_usb_send(dev, tx_data, POF_USB_ACTUAL_OUTPUT_SIZE);
             }
-            lastStatus = now;
+            last = now;
+            timeout = TIMEOUT_NORMAL;
+            if(virtual_portal->speaker) {
+                timeout = TIMEOUT_AFTER_MUSIC;
+            }
         }
     }
 
@@ -412,7 +385,7 @@ PoFUsb* pof_usb_start(VirtualPortal* virtual_portal) {
     PoFUsb* pof_usb = malloc(sizeof(PoFUsb));
     pof_usb->virtual_portal = virtual_portal;
     pof_usb->dataAvailable = 0;
-
+    furi_hal_usb_unlock();
     pof_usb->usb_prev = furi_hal_usb_get_config();
     pof_usb->usb.init = pof_usb_init;
     pof_usb->usb.deinit = pof_usb_deinit;
@@ -424,7 +397,6 @@ PoFUsb* pof_usb_start(VirtualPortal* virtual_portal) {
     pof_usb->usb.str_serial_descr = NULL;
     pof_usb->usb.cfg_descr = (void*)&usb_pof_cfg_descr;
 
-    furi_hal_usb_unlock();
     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) {

+ 32 - 0
portal_of_flipper/helpers/pof_usb.h

@@ -13,11 +13,24 @@
 #define HID_REPORT_TYPE_OUTPUT  2
 #define HID_REPORT_TYPE_FEATURE 3
 
+#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 TIMEOUT_NORMAL 32
+#define TIMEOUT_AFTER_RESPONSE 100
+#define TIMEOUT_AFTER_MUSIC 300
+
 typedef struct PoFUsb PoFUsb;
 
 PoFUsb* pof_usb_start(VirtualPortal* virtual_portal);
 void pof_usb_stop(PoFUsb* pof);
 
+PoFUsb* pof_usb_start_xbox360(VirtualPortal* virtual_portal);
+void pof_usb_stop_xbox360(PoFUsb* pof);
+
 /*descriptor type*/
 typedef enum {
     PoFDescriptorTypeDevice = 0x01,
@@ -76,3 +89,22 @@ typedef enum {
     PoFControlRequestsOut = (PoFControlTypeVendor | PoFControlRecipientDevice | PoFControlOut),
     PoFControlRequestsIn = (PoFControlTypeVendor | PoFControlRecipientDevice | PoFControlIn),
 } PoFControlRequests;
+
+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];
+};

+ 600 - 0
portal_of_flipper/helpers/pof_usb_xbox360.c

@@ -0,0 +1,600 @@
+#include "pof_usb.h"
+
+#define TAG "POF USB XBOX360"
+
+#define HID_INTERVAL 1
+
+#define USB_EP0_SIZE 8
+
+#define POF_USB_VID (0x1430)
+#define POF_USB_PID (0x1F17)
+
+#define POF_USB_EP_IN (0x81)
+#define POF_USB_EP_OUT (0x02)
+#define POF_USB_X360_AUDIO_EP_IN1 (0x83)
+#define POF_USB_X360_AUDIO_EP_OUT1 (0x04)
+#define POF_USB_X360_AUDIO_EP_IN2 (0x85)
+#define POF_USB_X360_AUDIO_EP_OUT2 (0x06)
+#define POF_USB_X360_PLUGIN_MODULE_EP_IN (0x87)
+
+#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 struct usb_string_descriptor dev_security_desc =
+    USB_ARRAY_DESC(0x58, 0x62, 0x6f, 0x78, 0x20, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
+                   0x79, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x20, 0x33, 0x2c, 0x20,
+                   0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x31, 0x2e, 0x30, 0x30,
+                   0x2c, 0x20, 0xa9, 0x20, 0x32, 0x30, 0x30, 0x35, 0x20, 0x4d, 0x69, 0x63,
+                   0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x43, 0x6f, 0x72, 0x70, 0x6f,
+                   0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x20, 0x41, 0x6c, 0x6c, 0x20,
+                   0x72, 0x69, 0x67, 0x68, 0x74, 0x73, 0x20, 0x72, 0x65, 0x73, 0x65, 0x72,
+                   0x76, 0x65, 0x64, 0x2e);
+
+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);
+
+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 = TIMEOUT_NORMAL;  // FuriWaitForever; //ms
+    uint32_t last = 0;
+
+    while (true) {
+        uint32_t now = furi_get_tick();
+        uint32_t flags = furi_thread_flags_wait(EventAll, FuriFlagWaitAny, timeout);
+        if (flags & EventRx) {  // fast flag
+
+            uint8_t buf[POF_USB_RX_MAX_SIZE];
+            len_data = pof_usb_receive(dev, buf, POF_USB_RX_MAX_SIZE);
+            // 360 controller packets have a header of 0x0b 0x14
+            if (len_data > 0 && buf[0] == 0x0b && buf[1] == 0x14) {
+                memset(tx_data, 0, sizeof(tx_data));
+                // prepend packet with xinput header
+                int send_len =
+                    virtual_portal_process_message(virtual_portal, buf + 2, tx_data + 2);
+                if (send_len > 0) {
+                    tx_data[0] = 0x0b;
+                    tx_data[1] = 0x14;
+                    pof_usb_send(dev, tx_data, POF_USB_ACTUAL_OUTPUT_SIZE);
+                    timeout = TIMEOUT_AFTER_RESPONSE;
+                    last = now;
+                    if (virtual_portal->speaker) {
+                        timeout = TIMEOUT_AFTER_MUSIC;
+                    }
+                }
+            } else if (len_data > 0 && buf[0] == 0x0b && buf[1] == 0x17) {
+                // 360 audio packets start with 0b 17, samples start after the two byte header
+                /*
+                FURI_LOG_RAW_I("pof_usb_receive: ");
+                for(uint32_t i = 2; i < len_data; i++) {
+                    FURI_LOG_RAW_I("%02x", buf[i]);
+                }
+                FURI_LOG_RAW_I("\r\n");
+                */
+                virtual_portal_process_audio_360(virtual_portal, buf + 2, len_data - 2);
+            }
+
+            // Check next status time since the timeout based one might be starved by incoming packets.
+            if (now > last + timeout) {
+                memset(tx_data, 0, sizeof(tx_data));
+                len_data = virtual_portal_send_status(virtual_portal, tx_data + 2);
+                if (len_data > 0) {
+                    tx_data[0] = 0x0b;
+                    tx_data[1] = 0x14;
+                    pof_usb_send(dev, tx_data, POF_USB_ACTUAL_OUTPUT_SIZE);
+                }
+                last = now;
+                timeout = TIMEOUT_NORMAL;
+                if (virtual_portal->speaker) {
+                    timeout = TIMEOUT_AFTER_MUSIC;
+                }
+            }
+
+            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 + 2);
+            if (len_data > 0) {
+                tx_data[0] = 0x0b;
+                tx_data[1] = 0x14;
+                pof_usb_send(dev, tx_data, POF_USB_ACTUAL_OUTPUT_SIZE);
+            }
+            last = now;
+            timeout = TIMEOUT_NORMAL;
+            if (virtual_portal->speaker) {
+                timeout = TIMEOUT_AFTER_MUSIC;
+            }
+        }
+    }
+
+    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);
+            usbd_reg_endpoint(dev, POF_USB_X360_AUDIO_EP_IN1, NULL);
+            usbd_reg_endpoint(dev, POF_USB_X360_AUDIO_EP_IN2, NULL);
+            usbd_reg_endpoint(dev, POF_USB_X360_AUDIO_EP_OUT1, NULL);
+            usbd_reg_endpoint(dev, POF_USB_X360_AUDIO_EP_OUT2, NULL);
+            usbd_reg_endpoint(dev, POF_USB_X360_PLUGIN_MODULE_EP_IN, NULL);
+            usbd_ep_deconfig(dev, POF_USB_X360_AUDIO_EP_IN1);
+            usbd_ep_deconfig(dev, POF_USB_X360_AUDIO_EP_IN2);
+            usbd_ep_deconfig(dev, POF_USB_X360_AUDIO_EP_OUT1);
+            usbd_ep_deconfig(dev, POF_USB_X360_AUDIO_EP_OUT2);
+            usbd_ep_deconfig(dev, POF_USB_X360_PLUGIN_MODULE_EP_IN);
+            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);
+            usbd_ep_config(dev, POF_USB_X360_AUDIO_EP_IN1, USB_EPTYPE_INTERRUPT, POF_USB_EP_IN_SIZE);
+            usbd_ep_config(dev, POF_USB_X360_AUDIO_EP_IN2, USB_EPTYPE_INTERRUPT, POF_USB_EP_OUT_SIZE);
+            usbd_ep_config(dev, POF_USB_X360_AUDIO_EP_OUT1, USB_EPTYPE_INTERRUPT, POF_USB_EP_IN_SIZE);
+            usbd_ep_config(dev, POF_USB_X360_AUDIO_EP_OUT2, USB_EPTYPE_INTERRUPT, POF_USB_EP_OUT_SIZE);
+            usbd_ep_config(dev, POF_USB_X360_PLUGIN_MODULE_EP_IN, USB_EPTYPE_INTERRUPT, POF_USB_EP_OUT_SIZE);
+            return usbd_ack;
+    }
+    return usbd_fail;
+}
+
+struct usb_xbox_intf_descriptor {
+    uint8_t bLength;
+    uint8_t bDescriptorType;
+    uint8_t reserved[2];
+    uint8_t subtype;
+    uint8_t reserved2;
+    uint8_t bEndpointAddressIn;
+    uint8_t bMaxDataSizeIn;
+    uint8_t reserved3[5];
+    uint8_t bEndpointAddressOut;
+    uint8_t bMaxDataSizeOut;
+    uint8_t reserved4[2];
+} __attribute__((packed));
+
+struct PoFUsbDescriptorXbox360 {
+    struct usb_config_descriptor config;
+    struct usb_interface_descriptor intf;
+    struct usb_xbox_intf_descriptor xbox_desc;
+    struct usb_endpoint_descriptor ep_in;
+    struct usb_endpoint_descriptor ep_out;
+    struct usb_interface_descriptor intfAudio;
+    uint8_t audio_desc[0x1B];
+    struct usb_endpoint_descriptor ep_in_audio1;
+    struct usb_endpoint_descriptor ep_out_audio1;
+    struct usb_endpoint_descriptor ep_in_audio2;
+    struct usb_endpoint_descriptor ep_out_audio2;
+    struct usb_interface_descriptor intfPluginModule;
+    uint8_t plugin_module_desc[0x09];
+    struct usb_endpoint_descriptor ep_in_plugin_module;
+    struct usb_interface_descriptor intfSecurity;
+    uint8_t security_desc[0x06];
+} __attribute__((packed));
+struct XInputVibrationCapabilities_t {
+    uint8_t rid;
+    uint8_t rsize;
+    uint8_t padding;
+    uint8_t left_motor;
+    uint8_t right_motor;
+    uint8_t padding_2[3];
+} __attribute__((packed));
+
+struct XInputInputCapabilities_t {
+    uint8_t rid;
+    uint8_t rsize;
+    uint16_t buttons;
+    uint8_t leftTrigger;
+    uint8_t rightTrigger;
+    uint16_t leftThumbX;
+    uint16_t leftThumbY;
+    uint16_t rightThumbX;
+    uint16_t rightThumbY;
+    uint8_t reserved[4];
+    uint16_t flags;
+} __attribute__((packed));
+
+static const struct usb_device_descriptor usb_pof_dev_descr_xbox_360 = {
+    .bLength = sizeof(struct usb_device_descriptor),
+    .bDescriptorType = USB_DTYPE_DEVICE,
+    .bcdUSB = VERSION_BCD(2, 0, 0),
+    .bDeviceClass = 0xFF,
+    .bDeviceSubClass = 0xFF,
+    .bDeviceProtocol = 0xFF,
+    .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 uint8_t xbox_serial[] = {0x12, 0x14, 0x32, 0xEF};
+
+static const struct XInputVibrationCapabilities_t XInputVibrationCapabilities = {
+    rid : 0x00,
+    rsize : sizeof(struct XInputVibrationCapabilities_t),
+    padding : 0x00,
+    left_motor : 0x00,
+    right_motor : 0x00,
+    padding_2 : {0x00, 0x00, 0x00}
+};
+static const struct XInputInputCapabilities_t XInputInputCapabilities = {
+    rid : 0x00,
+    rsize : sizeof(struct XInputInputCapabilities_t),
+    buttons : 0x0000,
+    leftTrigger : 0x00,
+    rightTrigger : 0x00,
+    leftThumbX : 0x0000,
+    leftThumbY : 0x0000,
+    rightThumbX : 0x0000,
+    rightThumbY : 0x0000,
+    reserved : {0x00, 0x00, 0x00, 0x00},
+    flags : 0x00
+};
+
+static const struct PoFUsbDescriptorXbox360 usb_pof_cfg_descr_x360 = {
+    .config =
+        {
+            .bLength = sizeof(struct usb_config_descriptor),
+            .bDescriptorType = USB_DTYPE_CONFIGURATION,
+            .wTotalLength = sizeof(struct PoFUsbDescriptorXbox360),
+            .bNumInterfaces = 4,
+            .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 = 0xFF,
+            .bInterfaceSubClass = 0x5D,
+            .bInterfaceProtocol = 0x01,
+            .iInterface = NO_DESCRIPTOR,
+        },
+    .xbox_desc =
+        {
+            .bLength = sizeof(struct usb_xbox_intf_descriptor),
+            .bDescriptorType = 0x21,
+            .reserved = {0x10, 0x01},
+            .subtype = 0x24,
+            .reserved2 = 0x25,
+            .bEndpointAddressIn = POF_USB_EP_IN,
+            .bMaxDataSizeIn = 0x14,
+            .reserved3 = {0x03, 0x03, 0x03, 0x04, 0x13},
+            .bEndpointAddressOut = POF_USB_EP_OUT,
+            .bMaxDataSizeOut = 0x08,
+            .reserved4 = {0x03, 0x03},
+        },
+    .ep_in =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_EP_IN,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x20,
+            .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,
+        },
+    .intfAudio =
+        {
+            .bLength = sizeof(struct usb_interface_descriptor),
+            .bDescriptorType = USB_DTYPE_INTERFACE,
+            .bInterfaceNumber = 1,
+            .bAlternateSetting = 0,
+            .bNumEndpoints = 4,
+            .bInterfaceClass = 0xFF,
+            .bInterfaceSubClass = 0x5D,
+            .bInterfaceProtocol = 0x03,
+            .iInterface = NO_DESCRIPTOR,
+        },
+    .audio_desc =
+        {0x1B, 0x21, 0x00, 0x01, 0x01, 0x01, POF_USB_X360_AUDIO_EP_IN1, 0x40, 0x01, POF_USB_X360_AUDIO_EP_OUT1,
+         0x20, 0x16, POF_USB_X360_AUDIO_EP_IN2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16,
+         POF_USB_X360_AUDIO_EP_OUT2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
+    .ep_in_audio1 =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_X360_AUDIO_EP_IN1,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x20,
+            .bInterval = 2,
+        },
+    .ep_out_audio1 =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_X360_AUDIO_EP_OUT1,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x20,
+            .bInterval = 4,
+        },
+    .ep_in_audio2 =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_X360_AUDIO_EP_IN2,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x20,
+            .bInterval = 0x40,
+        },
+    .ep_out_audio2 =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_X360_AUDIO_EP_OUT2,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x20,
+            .bInterval = 0x10,
+        },
+
+    .intfPluginModule =
+        {
+            .bLength = sizeof(struct usb_interface_descriptor),
+            .bDescriptorType = USB_DTYPE_INTERFACE,
+            .bInterfaceNumber = 2,
+            .bAlternateSetting = 0,
+            .bNumEndpoints = 1,
+            .bInterfaceClass = 0xFF,
+            .bInterfaceSubClass = 0x5D,
+            .bInterfaceProtocol = 0x02,
+            .iInterface = NO_DESCRIPTOR,
+        },
+    .plugin_module_desc =
+        {0x09, 0x21, 0x00, 0x01, 0x01, 0x22, POF_USB_X360_PLUGIN_MODULE_EP_IN, 0x07, 0x00},
+    .ep_in_plugin_module =
+        {
+            .bLength = sizeof(struct usb_endpoint_descriptor),
+            .bDescriptorType = USB_DTYPE_ENDPOINT,
+            .bEndpointAddress = POF_USB_X360_PLUGIN_MODULE_EP_IN,
+            .bmAttributes = USB_EPTYPE_INTERRUPT,
+            .wMaxPacketSize = 0x20,
+            .bInterval = 16,
+        },
+    .intfSecurity =
+        {
+            .bLength = sizeof(struct usb_interface_descriptor),
+            .bDescriptorType = USB_DTYPE_INTERFACE,
+            .bInterfaceNumber = 3,
+            .bAlternateSetting = 0,
+            .bNumEndpoints = 0,
+            .bInterfaceClass = 0xFF,
+            .bInterfaceSubClass = 0xFD,
+            .bInterfaceProtocol = 0x13,
+            .iInterface = 4,
+        },
+    .security_desc =
+        {0x06, 0x41, 0x00, 0x01, 0x01, 0x03},
+};
+
+/* 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;
+    uint8_t wValueL = req->wValue & 0xFF;
+
+    if (req->bmRequestType == 0xC0 && req->bRequest == USB_HID_GETREPORT && req->wValue == 0x0000) {
+        dev->status.data_ptr = (uint8_t*)xbox_serial;
+        dev->status.data_count = sizeof(xbox_serial);
+        return usbd_ack;
+    }
+    if (req->bmRequestType == 0xC1 && req->bRequest == USB_HID_GETREPORT && req->wValue == 0x0100) {
+        dev->status.data_ptr = (uint8_t*)&(XInputInputCapabilities);
+        dev->status.data_count = sizeof(XInputInputCapabilities);
+        return usbd_ack;
+    }
+    if (req->bmRequestType == 0xC1 && req->bRequest == USB_HID_GETREPORT && req->wValue == 0x0000) {
+        dev->status.data_ptr = (uint8_t*)&(XInputVibrationCapabilities);
+        dev->status.data_count = sizeof(XInputVibrationCapabilities);
+        return usbd_ack;
+    }
+    if (req->bmRequestType == 0x41 && req->bRequest == 00 && (req->wValue == 0x1F || req->wValue == 0x1E)) {
+        return usbd_ack;
+    }
+
+    if (((USB_REQ_RECIPIENT | USB_REQ_TYPE) & req->bmRequestType) ==
+            (USB_REQ_DEVICE | USB_REQ_STANDARD) &&
+        req->bRequest == USB_STD_GET_DESCRIPTOR) {
+        switch (wValueH) {
+            case USB_DTYPE_STRING:
+                if (wValueL == 4) {
+                    dev->status.data_ptr = (uint8_t*)&dev_security_desc;
+                    dev->status.data_count = dev_security_desc.bLength;
+                    return usbd_ack;
+                }
+                return usbd_fail;
+            default:
+                return usbd_fail;
+        }
+    }
+    return usbd_fail;
+}
+
+PoFUsb* pof_usb_start_xbox360(VirtualPortal* virtual_portal) {
+    PoFUsb* pof_usb = malloc(sizeof(PoFUsb));
+    pof_usb->virtual_portal = virtual_portal;
+    pof_usb->dataAvailable = 0;
+
+    furi_hal_usb_unlock();
+    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_xbox_360;
+    pof_usb->usb.cfg_descr = (void*)&usb_pof_cfg_descr_x360;
+    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;
+    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);
+        pof_usb = NULL;
+        return NULL;
+    }
+    return pof_usb;
+}
+
+void pof_usb_stop_xbox360(PoFUsb* pof_usb) {
+    if (pof_usb) {
+        furi_hal_usb_set_config(pof_usb->usb_prev, NULL);
+    }
+}

+ 1 - 3
portal_of_flipper/portal_of_flipper.c

@@ -70,10 +70,8 @@ PoFApp* pof_app_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);
+    scene_manager_next_scene(app->scene_manager, PoFSceneTypeSelect);
     return app;
 }
 

+ 12 - 2
portal_of_flipper/portal_of_flipper_i.c

@@ -7,11 +7,21 @@
 void pof_start(PoFApp* app) {
     furi_assert(app);
 
-    app->pof_usb = pof_usb_start(app->virtual_portal);
+    if (app->virtual_portal->type == PoFHid) {
+        app->pof_usb = pof_usb_start(app->virtual_portal);
+    }
+    if (app->virtual_portal->type == PoFXbox360) {
+        app->pof_usb = pof_usb_start_xbox360(app->virtual_portal);
+    }
 }
 
 void pof_stop(PoFApp* app) {
     furi_assert(app);
 
-    pof_usb_stop(app->pof_usb);
+    if (app->virtual_portal->type == PoFHid) {
+        pof_usb_stop(app->pof_usb);
+    }
+    if (app->virtual_portal->type == PoFXbox360) {
+        pof_usb_stop_xbox360(app->pof_usb);
+    }
 }

+ 1 - 0
portal_of_flipper/portal_of_flipper_i.h

@@ -32,6 +32,7 @@ struct PoFApp {
     VirtualPortal* virtual_portal;
 
     PoFUsb* pof_usb;
+    
 };
 
 typedef enum {

+ 1 - 0
portal_of_flipper/scenes/pof_scene_config.h

@@ -1,2 +1,3 @@
 ADD_SCENE(pof, main, Main)
 ADD_SCENE(pof, file_select, FileSelect)
+ADD_SCENE(pof, type_select, TypeSelect)

+ 56 - 0
portal_of_flipper/scenes/pof_scene_type_select.c

@@ -0,0 +1,56 @@
+#include "../portal_of_flipper_i.h"
+#include "../pof_token.h"
+
+#define TAG "PoFSceneTypeSelect"
+
+enum SubmenuIndex {
+    SubmenuIndexSwapHid,
+    SubmenuIndexSwapXbox360
+};
+
+void pof_scene_type_select_submenu_callback(void* context, uint32_t index) {
+    PoFApp* pof = context;
+    view_dispatcher_send_custom_event(pof->view_dispatcher, index);
+}
+
+void pof_scene_type_select_on_enter(void* context) {
+    PoFApp* pof = context;
+    Submenu* submenu = pof->submenu;
+    submenu_reset(pof->submenu);
+    submenu_add_item(
+    submenu,
+    "Emulate Xbox 360",
+    SubmenuIndexSwapXbox360,
+    pof_scene_type_select_submenu_callback,
+    pof);
+    submenu_add_item(
+    submenu,
+    "Emulate HID",
+    SubmenuIndexSwapHid,
+    pof_scene_type_select_submenu_callback,
+    pof);
+    view_dispatcher_switch_to_view(pof->view_dispatcher, PoFViewSubmenu);
+}
+
+bool pof_scene_type_select_on_event(void* context, SceneManagerEvent event) {
+    PoFApp* pof = context;
+    if(event.type == SceneManagerEventTypeCustom) {
+        if (event.event == SubmenuIndexSwapHid) {
+            virtual_portal_set_type(pof->virtual_portal, PoFHid);
+            pof_start(pof);
+            scene_manager_next_scene(pof->scene_manager, PoFSceneMain);
+            return true;
+        } else if (event.event == SubmenuIndexSwapXbox360) {
+            virtual_portal_set_type(pof->virtual_portal, PoFXbox360);
+            pof_start(pof);
+            scene_manager_next_scene(pof->scene_manager, PoFSceneMain);
+            return true;
+        } 
+    }
+    return false;
+}
+
+void pof_scene_type_select_on_exit(void* context) {
+    PoFApp* pof = context;
+    submenu_reset(pof->submenu);
+}

+ 420 - 145
portal_of_flipper/virtual_portal.c

@@ -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;
-}
+}

+ 65 - 1
portal_of_flipper/virtual_portal.h

@@ -1,28 +1,92 @@
 #pragma once
 
-#include <notification/notification_messages.h>
 #include <furi_hal_light.h>
+#include <notification/notification_messages.h>
+
 #include "pof_token.h"
+#include "audio/g721.h"
 
+#define SAMPLE_RATE 8000
 #define POF_TOKEN_LIMIT 16
+#define SAMPLES_COUNT 2048
+#define SAMPLES_COUNT_BUFFERED SAMPLES_COUNT * 8
+
+typedef enum {
+    PoFHid,
+    PoFXbox360
+} PoFType;
+
+typedef enum {
+    EventExit = (1 << 0),
+    EventReset = (1 << 1),
+    EventRx = (1 << 2),
+    EventTx = (1 << 3),
+    EventTxComplete = (1 << 4),
+    EventResetSio = (1 << 5),
+    EventTxImmediate = (1 << 6),
+    WavPlayerEventHalfTransfer = (1 << 2),
+    WavPlayerEventFullTransfer = (1 << 3),
+
+    EventAll = EventExit | EventReset | EventRx | EventTx | EventTxComplete | EventResetSio |
+               EventTxImmediate,
+} PoFEvent;
+
+typedef struct {
+    uint8_t r;
+    uint8_t g;
+    uint8_t b;
+    uint8_t target_r;
+    uint8_t target_g;
+    uint8_t target_b;
+    uint8_t last_r;
+    uint8_t last_g;
+    uint8_t last_b;
+    uint16_t delay;
+    uint32_t start_time;
+    bool two_phase;
+    bool running;
+    int current_phase;
+} VirtualPortalLed;
 
 typedef struct {
     PoFToken* tokens[POF_TOKEN_LIMIT];
     uint8_t sequence_number;
+    float volume;
+    bool playing_audio;
+    uint8_t audio_buffer[SAMPLES_COUNT];
+    uint8_t current_audio_buffer[SAMPLES_COUNT_BUFFERED];
+    uint8_t* head;
+    uint8_t* tail;
+    uint8_t* end;
+    uint16_t count;
+    uint8_t m;
     bool active;
     bool speaker;
     NotificationApp* notifications;
+    PoFType type;
+    VirtualPortalLed left;
+    VirtualPortalLed right;
+    VirtualPortalLed trap;
+    FuriTimer* led_timer;
+    FuriThread* thread;
+    struct g72x_state state;
 } VirtualPortal;
 
 VirtualPortal* virtual_portal_alloc(NotificationApp* notifications);
+void virtual_portal_set_type(VirtualPortal* virtual_portal, PoFType type);
 
 void virtual_portal_free(VirtualPortal* virtual_portal);
 void virtual_portal_cleanup(VirtualPortal* virtual_portal);
 void virtual_portal_load_token(VirtualPortal* virtual_portal, PoFToken* pof_token);
+void virtual_portal_tick();
 
 int virtual_portal_process_message(
     VirtualPortal* virtual_portal,
     uint8_t* message,
     uint8_t* response);
+void virtual_portal_process_audio(VirtualPortal* virtual_portal,
+                                  uint8_t* message, uint8_t len);
+void virtual_portal_process_audio_360(VirtualPortal* virtual_portal,
+                                  uint8_t* message, uint8_t len);
 
 int virtual_portal_send_status(VirtualPortal* virtual_portal, uint8_t* response);