usb: add usb iAP driver

rockbox-gerrit-noreply--- via rockbox-cvs <[email protected]> Sun, 3 May 2026 12:40:54 -0400
Newsgroups gmane.comp.systems.archos.rockbox.cvs
Message-ID <[email protected]>
commit 3bb656625b94b7515c7577d58deaae07de159101
Author: mojyack <[email protected]>
Date:   Fri Dec 19 20:45:16 2025 +0900

    usb: add usb iAP driver
    
    add class driver source files.
    also register iap audio sink.
    usbstack/iap/libiap directory is imported from libiap.
    
    Change-Id: I776c5caec33fe9efadc448e2e3b37d500bf19c9f

diff --git a/firmware/SOURCES b/firmware/SOURCES
index 888d296fd0..b80dc1a272 100644
--- a/firmware/SOURCES
+++ b/firmware/SOURCES
@@ -957,6 +957,20 @@ usbstack/usb_charging_only.c
 #ifdef USB_ENABLE_HID
 usbstack/usb_hid.c
 #endif
+#ifdef USB_ENABLE_IAP
+usbstack/iap/audio.c
+usbstack/iap/buffer.c
+usbstack/iap/debug.c
+usbstack/iap/libiap/debug.c
+usbstack/iap/libiap/fid-token-values.c
+usbstack/iap/libiap/hid.c
+usbstack/iap/libiap/iap.c
+usbstack/iap/libiap/notification.c
+usbstack/iap/libiap/span.c
+usbstack/iap/notification.c
+usbstack/iap/platform.c
+usbstack/usb_iap.c
+#endif
 #if CONFIG_USBOTG == USBOTG_M66591
 drivers/m66591.c
 #elif CONFIG_USBOTG == USBOTG_ARC
diff --git a/firmware/export/config.h b/firmware/export/config.h
index bcea581141..4b0a16f0f1 100644
--- a/firmware/export/config.h
+++ b/firmware/export/config.h
@@ -1400,6 +1400,10 @@ Lyre prototype 1 */
 #define USB_ENABLE_IAP
 #endif
 
+#if defined(USB_ENABLE_IAP)
+#define HAVE_MULTIMEDIA_KEYS
+#endif
+
 #endif /* BOOTLOADER */
 
 #endif /* HAVE_USBSTACK */
diff --git a/firmware/export/iap-usb.h b/firmware/export/iap-usb.h
new file mode 100644
index 0000000000..24f837981b
--- /dev/null
+++ b/firmware/export/iap-usb.h
@@ -0,0 +1,39 @@
+#pragma once
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "config.h"
+
+#ifdef USB_ENABLE_IAP
+/* usbstack/iap/notification.c */
+void iap_on_track_time_position(uint32_t pos_ms);
+void iap_on_track_playback_index(uint32_t index, bool track_ready);
+void iap_on_tracks_count(uint32_t count);
+void iap_on_play_status(int status /* AUDIO_STATUS_* */);
+void iap_on_volume(int volume);
+void iap_on_shuffle_state(bool state);
+void iap_on_repeat_state(int state);
+#else
+static inline void iap_on_track_time_position(uint32_t pos_ms) {
+    (void)pos_ms;
+}
+static inline void iap_on_track_playback_index(uint32_t index, bool track_ready) {
+    (void)index;
+    (void)track_ready;
+}
+static inline void iap_on_tracks_count(uint32_t count) {
+    (void)count;
+}
+static inline void iap_on_play_status(int status) {
+    (void)status;
+}
+static inline void iap_on_volume(int volume) {
+    (void)volume;
+}
+static inline void iap_on_shuffle_state(bool state) {
+    (void)state;
+}
+static inline void iap_on_repeat_state(int state) {
+    (void)state;
+}
+#endif
diff --git a/firmware/export/pcm_sink.h b/firmware/export/pcm_sink.h
index 24a1962189..a4945d3534 100644
--- a/firmware/export/pcm_sink.h
+++ b/firmware/export/pcm_sink.h
@@ -21,6 +21,8 @@
 #include <stddef.h>
 #include <stdint.h>
 
+#include "config.h"
+
 struct pcm_sink_caps {
     const unsigned long* samprs;
     uint16_t             num_samprs;
@@ -52,6 +54,9 @@ struct pcm_sink {
 
 enum pcm_sink_ids {
     PCM_SINK_BUILTIN = 0,
+#ifdef USB_ENABLE_IAP
+    PCM_SINK_IAP,
+#endif
     PCM_SINK_NUM
 };
 
diff --git a/firmware/export/usb.h b/firmware/export/usb.h
index 6d9784f93e..ed76ade521 100644
--- a/firmware/export/usb.h
+++ b/firmware/export/usb.h
@@ -172,6 +172,9 @@ enum {
 #endif
 #ifdef USB_ENABLE_AUDIO
     USB_DRIVER_AUDIO,
+#endif
+#ifdef USB_ENABLE_IAP
+    USB_DRIVER_IAP,
 #endif
     USB_NUM_DRIVERS
 };
diff --git a/firmware/pcm.c b/firmware/pcm.c
index bb4fd588b5..be5b47673a 100644
--- a/firmware/pcm.c
+++ b/firmware/pcm.c
@@ -77,8 +77,15 @@
  *
  */
 
+#ifdef USB_ENABLE_IAP
+extern struct pcm_sink iap_pcm_sink;
+#endif
+
 static struct pcm_sink* sinks[PCM_SINK_NUM] = {
     [PCM_SINK_BUILTIN] = &builtin_pcm_sink,
+#ifdef USB_ENABLE_IAP
+    [PCM_SINK_IAP] = &iap_pcm_sink,
+#endif
 };
 static enum pcm_sink_ids cur_sink = PCM_SINK_BUILTIN;
 
diff --git a/firmware/usb.c b/firmware/usb.c
index 2645d3b290..c3e35d22b3 100644
--- a/firmware/usb.c
+++ b/firmware/usb.c
@@ -214,6 +214,9 @@ static inline void usb_configure_drivers(int for_state)
 #ifdef USB_ENABLE_AUDIO
         usb_core_enable_driver(USB_DRIVER_AUDIO, (usb_audio == 1) || (usb_audio == 2)); // while "always" or "only in charge-only mode"
 #endif /* USB_ENABLE_AUDIO */
+#ifdef USB_ENABLE_IAP
+        usb_core_enable_driver(USB_DRIVER_IAP, false);
+#endif
 
 #ifdef USB_ENABLE_CHARGING_ONLY
         usb_core_enable_driver(USB_DRIVER_CHARGING_ONLY, true);
@@ -233,6 +236,9 @@ static inline void usb_configure_drivers(int for_state)
 #ifdef USB_ENABLE_AUDIO
         usb_core_enable_driver(USB_DRIVER_AUDIO, (usb_audio == 1) || (usb_audio == 3)); // while "always" or "only in mass-storage mode"
 #endif /* USB_ENABLE_AUDIO */
+#ifdef USB_ENABLE_IAP
+        usb_core_enable_driver(USB_DRIVER_IAP, true);
+#endif
 #ifdef USB_ENABLE_CHARGING_ONLY
         usb_core_enable_driver(USB_DRIVER_CHARGING_ONLY, false);
 #endif
diff --git a/firmware/usbstack/iap/audio.c b/firmware/usbstack/iap/audio.c
new file mode 100644
index 0000000000..d01b133d69
--- /dev/null
+++ b/firmware/usbstack/iap/audio.c
@@ -0,0 +1,268 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include "core_alloc.h"
+#include "pcm-internal.h"
+#include "pcm_sampr.h"
+#include "pcm_sink.h"
+#include "system.h"
+#include "usb_drv.h"
+
+#include "../usb_iap.h"
+#include "buffer.h"
+#include "libiap/iap.h"
+#include "macros.h"
+#include "platform.h"
+
+static const unsigned long samprs[] = {
+    SAMPR_48,
+    SAMPR_44,
+};
+
+struct StagingBuffer {
+    struct IAPAllocResult buf;
+    uint16_t              cursor;
+};
+static struct StagingBuffer staging_buffers[USB_BATCH_SLOTS + 1]; /* +1 for silent buffer */
+static int                  staging_buffer_index;
+
+#define zero_buffer (staging_buffers[USB_BATCH_SLOTS])
+
+static const uint8_t* pulled_buf;
+static size_t         pulled_buf_size;
+static size_t         pulled_buf_cursor;
+static int8_t         set_freq; /* requested freq from rockbox */
+static int8_t         cur_freq; /* requested freq from accessory */
+static uint8_t        packet_count;
+
+static bool enabled;
+static bool exhausted;
+static bool track_attrs_sent;
+
+extern struct pcm_sink iap_pcm_sink;
+
+static void sink_set_freq(uint16_t freq) {
+    LOG("freq=%d", freq);
+
+    track_attrs_sent = true;
+
+    set_freq = freq;
+
+    struct IAPContext* ctx = _iap_acquire_ctx(true);
+    check_act(iap_select_sampr(ctx, samprs[freq]), );
+    _iap_release_ctx();
+}
+
+static size_t calc_packet_size(uint8_t cur_sampr, uint8_t packet_count) {
+    /* packet size calculation
+     * (4 = sizeof(int16_t) * channels)
+     * (1000 = usb frames per second)
+     * 48000Hz:
+     *   48000 * 4 / 1000 = 192.0
+     *   => 192 + ...
+     * 44100Hz:
+     *   44100 * 4 / 1000 = 176.4
+     *   => (9 * 176 + 180) + (...
+     */
+    if(cur_sampr == 0) {
+        /*48k*/
+        return 192;
+    } else {
+        /*44.1k*/
+        return packet_count % 10 == 0 ? 180 : 176;
+    }
+}
+
+static void batch_get_more(const void** ptr, size_t* len) {
+    const size_t packet_size = calc_packet_size(cur_freq, packet_count);
+#if 0
+    const int cur_frame = usb_drv_get_frame_number();
+    LOG("ex=%d set=%d cur=%d", exhausted, set_freq, cur_freq);
+#endif
+
+start:
+    if(exhausted || cur_freq != set_freq) {
+        *ptr = zero_buffer.buf.ptr;
+        *len = packet_size;
+        packet_count += 1;
+        return;
+    }
+
+    if(pulled_buf_cursor == pulled_buf_size) {
+        /* run out of previously pulled data.
+         * let's start filling them, by requesting new data from upstream */
+        if(!pcm_play_dma_complete_callback(PCM_DMAST_OK, (const void**)&pulled_buf, &pulled_buf_size)) {
+            /* no more data, but we have to keep sending something as long as the audio stream interface is enabled */
+            exhausted = true;
+            goto start;
+        }
+
+        /* pushing_{buf,buf_size} are filled. reset cursor and continue filling */
+        pcm_play_dma_status_callback(PCM_DMAST_STARTED);
+        pulled_buf_cursor = 0;
+    }
+
+    /* fill this single packet */
+    struct StagingBuffer* stage = &staging_buffers[staging_buffer_index];
+    const size_t          copy  = MIN(packet_size - stage->cursor, pulled_buf_size - pulled_buf_cursor);
+    memcpy(stage->buf.ptr + stage->cursor, pulled_buf + pulled_buf_cursor, copy);
+    pulled_buf_cursor += copy;
+    stage->cursor += copy;
+#define AUDIO_STAT 0
+#if AUDIO_STAT == 1
+    static int last_hz;
+    static int sample;
+#endif
+    if(stage->cursor == packet_size) {
+        *ptr                 = stage->buf.ptr;
+        *len                 = stage->cursor;
+        stage->cursor        = 0;
+        staging_buffer_index = (staging_buffer_index + 1) % USB_BATCH_SLOTS;
+        packet_count += 1;
+#if AUDIO_STAT == 1
+        sample += packet_size / 4;
+        if(current_tick >= last_hz + HZ) {
+            logf("pushed %d %d", packet_size, sample);
+            sample  = 0;
+            last_hz = current_tick;
+        }
+#endif
+    } else { /* pushing_buf_cursor == pushing_buf_size */
+        goto start;
+    }
+}
+
+static void sink_play(const void* addr, size_t size) {
+    LOG("play");
+
+    pulled_buf        = addr;
+    pulled_buf_size   = size;
+    pulled_buf_cursor = 0;
+    exhausted         = false;
+
+    /* resolve pending play request before sending TrackNewAudioAttributes (by sink_set_freq).
+     * some accessories will get confused when receiving it while waiting for an ack */
+    struct IAPContext* ctx = _iap_acquire_ctx(true);
+    struct Platform*   plt = ctx->platform;
+    if(plt->control_pending) {
+        check_act(iap_control_response(ctx, plt->pending_control, true), );
+        plt->control_pending = false;
+    }
+    _iap_release_ctx();
+
+    /* rockbox only calls set_freq when changes occur, but we must call
+     * set_freq(and iap_select_sampr) at least once per connection. */
+    if(!track_attrs_sent) {
+        sink_set_freq(iap_pcm_sink.configured_freq);
+    }
+}
+
+static void sink_stop(void) {
+    LOG("stop");
+    /* we don't call usb_drv_batch_stop() here,
+     * because we need to send something, even not playing. */
+}
+
+static void sink_nop(void) {
+}
+
+struct pcm_sink iap_pcm_sink = {
+    .caps = {
+        .samprs       = samprs,
+        .num_samprs   = ARRAYLEN(samprs),
+        .default_freq = 0,
+    },
+    .ops = {
+        .init     = sink_nop,
+        .postinit = sink_nop,
+        .set_freq = sink_set_freq,
+        .lock     = sink_nop,
+        .unlock   = sink_nop,
+        .play     = sink_play,
+        .stop     = sink_stop,
+    },
+};
+
+bool iap_audio_init(void) {
+    check_act(usb_drv_batch_init(AS_EP_IN, batch_get_more) == 0, return false);
+
+    for(size_t i = 0; i < ARRAYLEN(staging_buffers); i += 1) {
+        check_act(iap_alloc_usb_send_buffer(AS_PACKET_SIZE, &staging_buffers[i].buf), goto error);
+        staging_buffers[i].cursor = 0;
+    }
+    staging_buffer_index = 0;
+
+    memset(zero_buffer.buf.ptr, 0, AS_PACKET_SIZE);
+
+    set_freq         = -1;
+    cur_freq         = -2; /* something <0 && !=set_freq */
+    enabled          = false;
+    exhausted        = true;
+    track_attrs_sent = false;
+    packet_count = 0;
+
+    return true;
+
+error:
+    for(size_t i = 0; i < ARRAYLEN(staging_buffers); i += 1) {
+        if(staging_buffers[i].buf.ptr != NULL) {
+            core_free(staging_buffers[i].buf.handle);
+            staging_buffers[i].buf.ptr = NULL;
+        }
+    }
+    return false;
+}
+
+bool iap_audio_deinit(void) {
+    check_act(usb_drv_batch_deinit() == 0, );
+    for(size_t i = 0; i < ARRAYLEN(staging_buffers); i += 1) {
+        core_free(staging_buffers[i].buf.handle);
+        staging_buffers[i].buf.ptr = NULL;
+    }
+    return true;
+}
+
+bool iap_audio_enable(void) {
+    LOG("enabled=%d", enabled);
+    check_act(enabled || usb_drv_batch_start() == 0, return false);
+    enabled = true;
+    return true;
+}
+
+bool iap_audio_disable(void) {
+    LOG("enabled=%d", enabled);
+    check_act(!enabled || usb_drv_batch_stop() == 0, return false);
+    enabled = false;
+    return true;
+}
+
+bool iap_audio_set_sampr(uint32_t sampr) {
+    uint16_t freq = 0;
+    for(; freq < ARRAYLEN(samprs); freq += 1) {
+        if(samprs[freq] == sampr) {
+            break;
+        }
+    }
+    check_act(freq < ARRAYLEN(samprs), return false);
+    cur_freq = freq;
+
+    LOG("sampr=%lu, set_freq=%d cur_freq=%d", sampr, set_freq, cur_freq);
+
+    return true;
+}
diff --git a/firmware/usbstack/iap/audio.h b/firmware/usbstack/iap/audio.h
new file mode 100644
index 0000000000..334ce41ebb
--- /dev/null
+++ b/firmware/usbstack/iap/audio.h
@@ -0,0 +1,28 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#pragma once
+#include <stdbool.h>
+#include <stdint.h>
+
+bool iap_audio_init(void);
+bool iap_audio_deinit(void);
+bool iap_audio_enable(void);
+bool iap_audio_disable(void);
+bool iap_audio_set_sampr(uint32_t sampr);
diff --git a/firmware/usbstack/iap/buffer.c b/firmware/usbstack/iap/buffer.c
new file mode 100644
index 0000000000..32b6cbdcfd
--- /dev/null
+++ b/firmware/usbstack/iap/buffer.c
@@ -0,0 +1,47 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include "core_alloc.h"
+
+#include "buffer.h"
+#include "macros.h"
+
+bool iap_alloc_buffer(size_t size, struct IAPAllocResult* result) {
+    int handle = core_alloc_ex(size, &buflib_ops_locked);
+    check_act(handle > 0, return false);
+    uint8_t* ptr = core_get_data(handle);
+
+    result->handle = handle;
+    result->ptr    = ptr;
+    return true;
+}
+
+bool iap_alloc_usb_send_buffer(size_t size, struct IAPAllocResult* result) {
+    /* + 31 to handle worst-case misalignment */
+    check_act(iap_alloc_buffer(size + 31, result), return false);
+
+    /* align to 32 */
+    result->ptr = (void*)((uintptr_t)(result->ptr + 31) & 0xffffffe0);
+    /* uncached */
+#if defined(UNCACHED_ADDR) && CONFIG_CPU != AS3525
+    result->ptr = UNCACHED_ADDR(result->ptr);
+#endif
+
+    return true;
+}
diff --git a/firmware/usbstack/iap/buffer.h b/firmware/usbstack/iap/buffer.h
new file mode 100644
index 0000000000..2620fe3960
--- /dev/null
+++ b/firmware/usbstack/iap/buffer.h
@@ -0,0 +1,30 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#pragma once
+#include <stdbool.h>
+#include <stddef.h>
+
+struct IAPAllocResult {
+    void* ptr;
+    int   handle;
+};
+
+bool iap_alloc_buffer(size_t size, struct IAPAllocResult* result);
+bool iap_alloc_usb_send_buffer(size_t size, struct IAPAllocResult* result);
diff --git a/firmware/usbstack/iap/debug.c b/firmware/usbstack/iap/debug.c
new file mode 100644
index 0000000000..80aa02741e
--- /dev/null
+++ b/firmware/usbstack/iap/debug.c
@@ -0,0 +1,89 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "logf.h"
+#include "system.h"
+#include "tick.h"
+
+#include "font.h"
+#include "lcd.h"
+
+#define MAX_COLS 64
+
+static int      rows;
+static int      columns;
+static unsigned count;
+
+static void update_color(void) {
+    unsigned avail = rows - 1;
+    if((count % (avail * 2)) > avail) {
+        lcd_set_drawinfo(DRMODE_SOLID, LCD_BLACK, LCD_WHITE);
+    } else {
+        lcd_set_drawinfo(DRMODE_SOLID, LCD_WHITE, LCD_BLACK);
+    }
+}
+
+void iap_lcd_scatter(const char* fmt, ...) {
+    if(rows == 0) {
+        int w, h;
+        font_getstringsize((unsigned char*)"A", &w, &h, FONT_SYSFIXED);
+        columns = MIN(LCD_WIDTH / w, MAX_COLS);
+        rows    = LCD_HEIGHT / h;
+    }
+
+    va_list ap;
+    char    buf[256];
+    va_start(ap, fmt);
+    int len = vsnprintf(buf, sizeof(buf), (char*)fmt, ap);
+    va_end(ap);
+
+    logf("%s", buf);
+
+    lcd_set_backdrop(NULL);
+    lcd_setfont(FONT_SYSFIXED);
+
+    update_color();
+    lcd_putsf(0, 0, (unsigned char*)"count %u", count);
+    for(int i = 0; i < len;) {
+        char line[MAX_COLS];
+        int  copy = MIN(len - i, columns - 1);
+        memcpy(line, buf + i, copy);
+        memset(line + copy, ' ', columns - 1 - copy);
+        line[columns - 1] = '\0';
+        update_color();
+        lcd_puts(0, 1 + count % (rows - 1), (unsigned char*)line);
+        count += 1;
+        i += copy;
+    }
+    lcd_update();
+}
+
+static unsigned long timestamp_epoch;
+
+unsigned long iap_debug_timestamp(void) {
+    return current_tick - timestamp_epoch;
+}
+
+void iap_debug_reset_timestamp(void) {
+    timestamp_epoch = current_tick;
+}
diff --git a/firmware/usbstack/iap/debug.h b/firmware/usbstack/iap/debug.h
new file mode 100644
index 0000000000..034a050f7f
--- /dev/null
+++ b/firmware/usbstack/iap/debug.h
@@ -0,0 +1,42 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#pragma once
+
+/* debug switches */
+#define DEBUG_ENABLE_INFO     0
+#define DEBUG_ENABLE_ERROR    1
+#define DEBUG_LCD_PRINT       0
+#define DEBUG_DUMP_TX         0
+#define DEBUG_DUMP_RX         0
+#define DEBUG_HEXDUMP_NOLIMIT 0
+
+#if DEBUG_ENABLE_INFO == 1 || DEBUG_ENABLE_ERROR == 1
+#define LOGF_ENABLE
+#include "logf.h"
+#endif
+
+void          iap_lcd_scatter(const char* fmt, ...);
+unsigned long iap_debug_timestamp(void);
+void          iap_debug_reset_timestamp(void);
+
+#if DEBUG_LCD_PRINT == 1
+#undef logf
+#define logf(...) iap_lcd_scatter(__VA_ARGS__)
+#endif
diff --git a/firmware/usbstack/iap/libiap-sync.sh b/firmware/usbstack/iap/libiap-sync.sh
new file mode 100755
index 0000000000..030c29f370
--- /dev/null
+++ b/firmware/usbstack/iap/libiap-sync.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -e
+
+url=https://github.com/mojyack/libiap.git
+src=/tmp/libiap
+dst=$PWD/libiap
+
+if [[ ! -e $src ]]; then
+    git clone "$url" "$src"
+else
+    git -C "$src" pull
+fi
+
+rsync --recursive --delete "$src/src/iap/" "$dst/" --exclude /README.rockbox
+cp "$src/LICENSE" "$dst/LICENSE"
+git -C "$src" rev-parse HEAD > "$dst/HEAD"
diff --git a/firmware/usbstack/iap/libiap/HEAD b/firmware/usbstack/iap/libiap/HEAD
new file mode 100644
index 0000000000..30d689eb0d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/HEAD
@@ -0,0 +1 @@
+ad06cdfc66f5db55d16b741df265269309105af3
diff --git a/firmware/usbstack/iap/libiap/LICENSE b/firmware/usbstack/iap/libiap/LICENSE
new file mode 100644
index 0000000000..fb320d67e7
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 mojyack
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/firmware/usbstack/iap/libiap/README.rockbox b/firmware/usbstack/iap/libiap/README.rockbox
new file mode 100644
index 0000000000..e6dbf7f555
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/README.rockbox
@@ -0,0 +1,5 @@
+Files under this directory were imported from libiap(https://github.com/mojyack/libiap) except for:
+- README.rockbox: this file, Rockbox specific notes
+- HEAD: libiap commit hash
+
+To sync with the upstream, use ../libiap-sync.sh script.
diff --git a/firmware/usbstack/iap/libiap/bool.h b/firmware/usbstack/iap/libiap/bool.h
new file mode 100644
index 0000000000..f94595ede1
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/bool.h
@@ -0,0 +1,5 @@
+#pragma once
+
+typedef unsigned char IAPBool;
+#define iap_true  1
+#define iap_false 0
diff --git a/firmware/usbstack/iap/libiap/constants.h b/firmware/usbstack/iap/libiap/constants.h
new file mode 100644
index 0000000000..2dca15d05f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/constants.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#define HID_BUFFER_SIZE  1024
+#define SEND_BUFFER_SIZE 767 /* 0x2ff - 1: input_report_size_table_hs[0x0C].size - sizeof(report_id) */
+
+enum IAPPhase {
+    IAPPhase_Connected = 0, /* initial state, waiting for StartIDPS */
+    IAPPhase_IDPS,          /* idps started */
+    IAPPhase_Auth,          /* idps completed, authenticating accessory */
+    IAPPhase_Authed,        /* authentication completed, processing requests */
+};
diff --git a/firmware/usbstack/iap/libiap/context.h b/firmware/usbstack/iap/libiap/context.h
new file mode 100644
index 0000000000..f7e6ca3600
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/context.h
@@ -0,0 +1,107 @@
+#pragma once
+#include <stddef.h>
+#include <stdint.h>
+
+#include "bool.h"
+#include "notification.h"
+#include "platform.h"
+
+struct IAPContext;
+struct IAPSpan;
+
+typedef IAPBool (*IAPOnSendComplete)(struct IAPContext* ctx);
+
+struct IAPOpts {
+    /* indicate usb transport is highspeed */
+    IAPBool usb_highspeed : 1;
+    /* needed to support some accessories which don't set correct hid report id */
+    IAPBool ignore_hid_report_id : 1;
+    /* limit packet size while sending artworks for stability */
+    IAPBool artwork_single_report : 1;
+    /* dump each packets */
+    IAPBool enable_packet_dump : 1;
+};
+
+struct IAPContextButtons {
+    IAPBool play_pause : 1;
+    IAPBool volume_up : 1;
+    IAPBool volume_down : 1;
+    IAPBool next_track : 1;
+    IAPBool prev_track : 1;
+    IAPBool next_album : 1;
+    IAPBool prev_album : 1;
+    IAPBool stop : 1;
+    IAPBool play : 1;
+    IAPBool pause : 1;
+    IAPBool mute_toggle : 1;
+    IAPBool next_chapter : 1;
+    IAPBool prev_chapter : 1;
+    IAPBool next_playlist : 1;
+    IAPBool prev_playlist : 1;
+    IAPBool shuffle_advance : 1;
+    IAPBool repeat_advance : 1;
+};
+
+struct IAPActiveEvent;
+
+typedef IAPBool (*IAPActiveEventReadyCallback)(struct IAPContext* ctx, struct IAPActiveEvent* event);
+
+struct IAPActiveEvent {
+    IAPActiveEventReadyCallback callback;
+    union {
+        uint32_t sampr;
+        struct {
+            struct IAPPlatformPendingControl control;
+            IAPBool                          result;
+        } control_response;
+    };
+};
+
+struct IAPContext {
+    /* set by user */
+    void*          platform; /* opaque to platform functions */
+    struct IAPOpts opts;     /* options */
+
+    /* iap_feed_hid_report */
+    uint8_t* hid_recv_buf;
+    size_t   hid_recv_buf_cursor;
+    /* _iap_send_packet, _iap_send_hid_reports */
+    uint8_t* send_buf;
+    size_t   send_buf_sending_cursor;
+    size_t   send_buf_sending_range_begin;
+    size_t   send_buf_sending_range_end;
+    /* _iap_send_next_report */
+    /* for library-oriented, manageable events */
+    IAPOnSendComplete on_send_complete;
+    /* for user-oriented, unexpectable events */
+    struct IAPActiveEvent active_events[2];
+    /* _iap_feed_packet */
+    int32_t handling_trans_id;
+    /* iap.c */
+    struct IAPContextButtons context_button_state;
+    struct IAPPlatformArtwork    artwork;
+    size_t                       artwork_cursor;
+    uint16_t                     trans_id;
+    uint16_t                     artwork_chunk_index;
+    int32_t                      artwork_trans_id;
+    uint16_t                     artwork_data_command;
+    uint8_t                      artwork_data_lingo;
+    uint8_t                      trans_id_support; /* TransIDSupport */
+    /* notification.c */
+    /* DisplayRemote::SetRemoteEventNotification */
+    uint32_t enabled_notifications_3;
+    uint32_t notifications_3;
+    /* ExtendedInterface::SetPlayStatusChangeNotification */
+    uint32_t enabled_notifications_4;
+    uint32_t notifications_4;
+    /* notification data */
+    struct _IAPNotifyState notification_data;
+    uint8_t                notification_tick;
+    /* _iap_send_hid_reports */
+    uint8_t* hid_send_staging_buf;
+    IAPBool  send_busy : 1;
+    IAPBool  flushing_notifications : 1;
+    IAPBool  waiting_for_audio_attrs_ack : 1;
+
+    uint8_t phase; /* IAPPhase */
+};
diff --git a/firmware/usbstack/iap/libiap/datetime.h b/firmware/usbstack/iap/libiap/datetime.h
new file mode 100644
index 0000000000..00f0b13336
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/datetime.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPDateTime {
+    uint16_t year;
+    uint8_t  month;
+    uint8_t  day;
+    uint8_t  hour;
+    uint8_t  minute;
+    uint8_t  seconds;
+};
diff --git a/firmware/usbstack/iap/libiap/debug.c b/firmware/usbstack/iap/libiap/debug.c
new file mode 100644
index 0000000000..c6386eb82a
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/debug.c
@@ -0,0 +1,1052 @@
+#include <inttypes.h>
+#include <stdint.h>
+
+#include "endian.h"
+#include "macros.h"
+#include "span.h"
+#include "spec/iap.h"
+
+#define entry(lingo, command) [IAP##lingo##CommandID_##command] = #command
+
+static const char* strs_general[] = {
+    entry(General, RequestIdentify),
+    entry(General, Identify),
+    entry(General, IPodAck),
+    entry(General, RequestExtendedInterfaceMode),
+    entry(General, ReturnExtendedInterfaceMode),
+    entry(General, EnterExtendedInterfaceMode),
+    entry(General, ExitExtendedInterfaceMode),
+    entry(General, RequestIPodName),
+    entry(General, ReturnIPodName),
+    entry(General, RequestIPodSoftwareVersion),
+    entry(General, ReturnIPodSoftwareVersion),
+    entry(General, RequestIPodSerialNum),
+    entry(General, ReturnIPodSerialNum),
+    entry(General, RequestIPodModelNum),
+    entry(General, ReturnIPodModelNum),
+    entry(General, RequestLingoProtocolVersion),
+    entry(General, ReturnLingoProtocolVersion),
+    entry(General, RequestTransportMaxPayloadSize),
+    entry(General, ReturnTransportMaxPayloadSize),
+    entry(General, IdentifyDeviceLingoes),
+    entry(General, GetAccessoryAuthenticationInfo),
+    entry(General, RetAccessoryAuthenticationInfo),
+    entry(General, AckAccessoryAuthenticationInfo),
+    entry(General, GetAccessoryAuthenticationSignature),
+    entry(General, RetAccessoryAuthenticationSignature),
+    entry(General, AckAccessoryAuthenticationStatus),
+    entry(General, GetIPodAuthenticationInfo),
+    entry(General, RetIPodAuthenticationInfo),
+    entry(General, AckIPodAuthenticationInfo),
+    entry(General, GetIPodAuthenticationSignature),
+    entry(General, RetIPodAuthenticationSignature),
+    entry(General, AckIPodAuthenticationStatus),
+    entry(General, NotifyIPodStateChange),
+    entry(General, GetIPodOptions),
+    entry(General, RetIPodOptions),
+    entry(General, GetAccessoryInfo),
+    entry(General, RetAccessoryInfo),
+    entry(General, GetIPodPreferences),
+    entry(General, RetIPodPreferences),
+    entry(General, SetIPodPreferences),
+    entry(General, GetUIMode),
+    entry(General, RetUIMode),
+    entry(General, SetUIMode),
+    entry(General, StartIDPS),
+    entry(General, SetFIDTokenValues),
+    entry(General, AckFIDTokenValues),
+    entry(General, EndIDPS),
+    entry(General, IDPSStatus),
+    entry(General, OpenDataSessionForProtocol),
+    entry(General, CloseDataSession),
+    entry(General, AccessoryAck),
+    entry(General, AccessoryDataTransfer),
+    entry(General, IPodDataTransfer),
+    entry(General, SetAccessoryStatusNotification),
+    entry(General, RetAccessoryStatusNotification),
+    entry(General, AccessoryStatusNotification),
+    entry(General, SetEventNotification),
+    entry(General, IPodNotification),
+    entry(General, GetIPodOptionsForLingo),
+    entry(General, RetIPodOptionsForLingo),
+    entry(General, GetEventNotification),
+    entry(General, RetEventNotification),
+    entry(General, GetSupportedEventNotification),
+    entry(General, CancelCommand),
+    entry(General, RetSupportedEventNotification),
+    entry(General, SetAvailableCurrent),
+    entry(General, SetInternalBatteryChargingState),
+    entry(General, RequestApplicationLaunch),
+    entry(General, GetNowPlayingApplicationBundleName),
+    entry(General, RetNowPlayingApplicationBundleName),
+    entry(General, GetLocalizationInfo),
+    entry(General, RetLocalizationInfo),
+    entry(General, RequestWiFiConnectionInfo),
+    entry(General, WiFiConnectionInfo),
+};
+static const char* strs_simple[] = {
+    entry(SimpleRemote, ContextButtonStatus),
+    entry(SimpleRemote, IPodAck),
+    entry(SimpleRemote, ImageButtonStatus),
+    entry(SimpleRemote, VideoButtonStatus),
+    entry(SimpleRemote, AudioButtonStatus),
+    entry(SimpleRemote, IPodOutButtonStatus),
+    entry(SimpleRemote, RotationInputStatus),
+    entry(SimpleRemote, RadioButtonStatus),
+    entry(SimpleRemote, CameraButtonStatus),
+    entry(SimpleRemote, RegisterDescriptor),
+    entry(SimpleRemote, IPodHIDReport),
+    entry(SimpleRemote, AccessoryHIDReport),
+    entry(SimpleRemote, UnregisterDescriptor),
+    entry(SimpleRemote, VoiceOverEvent),
+    entry(SimpleRemote, GetVoiceOverParameter),
+    entry(SimpleRemote, RetVoiceOverParameter),
+    entry(SimpleRemote, SetVoiceOverParameter),
+    entry(SimpleRemote, GetCurrentVoiceOverItemProperty),
+    entry(SimpleRemote, RetCurrentVoiceOverItemProperty),
+    entry(SimpleRemote, SetVoiceOverContext),
+    entry(SimpleRemote, VoiceOverParameterChanged),
+    entry(SimpleRemote, AccessoryAck),
+};
+static const char* strs_display[] = {
+    entry(DisplayRemote, IPodAck),
+    entry(DisplayRemote, GetCurrentEQProfileIndex),
+    entry(DisplayRemote, RetCurrentEQProfileIndex),
+    entry(DisplayRemote, SetCurrentEQProfileIndex),
+    entry(DisplayRemote, GetNumEQProfiles),
+    entry(DisplayRemote, RetNumEQProfiles),
+    entry(DisplayRemote, GetIndexedEQProfileName),
+    entry(DisplayRemote, RetIndexedEQProfileName),
+    entry(DisplayRemote, SetRemoteEventNotification),
+    entry(DisplayRemote, RemoteEventNotification),
+    entry(DisplayRemote, GetRemoteEventStatus),
+    entry(DisplayRemote, RetRemoteEventStatus),
+    entry(DisplayRemote, GetIPodStateInfo),
+    entry(DisplayRemote, RetIPodStateInfo),
+    entry(DisplayRemote, SetIPodStateInfo),
+    entry(DisplayRemote, GetPlayStatus),
+    entry(DisplayRemote, RetPlayStatus),
+    entry(DisplayRemote, SetCurrentPlayingTrack),
+    entry(DisplayRemote, GetIndexedPlayingTrackInfo),
+    entry(DisplayRemote, RetIndexedPlayingTrackInfo),
+    entry(DisplayRemote, GetNumPlayingTracks),
+    entry(DisplayRemote, RetNumPlayingTracks),
+    entry(DisplayRemote, GetArtworkFormats),
+    entry(DisplayRemote, RetArtworkFormats),
+    entry(DisplayRemote, GetTrackArtworkData),
+    entry(DisplayRemote, RetTrackArtworkData),
+    entry(DisplayRemote, GetPowerBatteryState),
+    entry(DisplayRemote, RetPowerBatteryState),
+    entry(DisplayRemote, GetSoundCheckState),
+    entry(DisplayRemote, RetSoundCheckState),
+    entry(DisplayRemote, SetSoundCheckState),
+    entry(DisplayRemote, GetTrackArtworkTimes),
+    entry(DisplayRemote, RetTrackArtworkTimes),
+    entry(DisplayRemote, CreateGeniusPlaylist),
+    entry(DisplayRemote, IsGeniusAvailableForTrack),
+};
+static const char* strs_ext[] = {
+    entry(ExtendedInterface, IPodAck),
+    entry(ExtendedInterface, GetCurrentPlayingTrackChapterInfo),
+    entry(ExtendedInterface, ReturnCurrentPlayingTrackChapterInfo),
+    entry(ExtendedInterface, SetCurrentPlayingTrackChapter),
+    entry(ExtendedInterface, GetCurrentPlayingTrackChapterPlayStatus),
+    entry(ExtendedInterface, ReturnCurrentPlayingTrackChapterPlayStatus),
+    entry(ExtendedInterface, GetCurrentPlayingTrackChapterName),
+    entry(ExtendedInterface, ReturnCurrentPlayingTrackChapterName),
+    entry(ExtendedInterface, GetAudiobookSpeed),
+    entry(ExtendedInterface, RetAudiobookSpeed),
+    entry(ExtendedInterface, SetAudiobookSpeed),
+    entry(ExtendedInterface, GetIndexedPlayingTrackInfo),
+    entry(ExtendedInterface, ReturnIndexedPlayingTrackInfo),
+    entry(ExtendedInterface, GetArtworkFormats),
+    entry(ExtendedInterface, RetArtworkFormats),
+    entry(ExtendedInterface, GetTrackArtworkData),
+    entry(ExtendedInterface, RetTrackArtworkData),
+    entry(ExtendedInterface, RequestProtocolVersion),
+    entry(ExtendedInterface, ReturnProtocolVersion),
+    entry(ExtendedInterface, RequestIPodName),
+    entry(ExtendedInterface, ReturnIPodName),
+    entry(ExtendedInterface, ResetDBSelection),
+    entry(ExtendedInterface, SelectDBRecord),
+    entry(ExtendedInterface, GetNumberCategorizedDBRecords),
+    entry(ExtendedInterface, ReturnNumberCategorizedDBRecords),
+    entry(ExtendedInterface, RetrieveCategorizedDatabaseRecords),
+    entry(ExtendedInterface, ReturnCategorizedDatabaseRecords),
+    entry(ExtendedInterface, GetPlayStatus),
+    entry(ExtendedInterface, ReturnPlayStatus),
+    entry(ExtendedInterface, GetCurrentPlayingTrackIndex),
+    entry(ExtendedInterface, ReturnCurrentPlayingTrackIndex),
+    entry(ExtendedInterface, GetIndexedPlayingTrackTitle),
+    entry(ExtendedInterface, ReturnIndexedPlayingTrackTitle),
+    entry(ExtendedInterface, GetIndexedPlayingTrackArtistName),
+    entry(ExtendedInterface, ReturnIndexedPlayingTrackArtistName),
+    entry(ExtendedInterface, GetIndexedPlayingTrackAlbumName),
+    entry(ExtendedInterface, ReturnIndexedPlayingTrackAlbumName),
+    entry(ExtendedInterface, SetPlayStatusChangeNotification),
+    entry(ExtendedInterface, PlayStatusChangeNotification),
+    entry(ExtendedInterface, PlayCurrentSelection),
+    entry(ExtendedInterface, PlayControl),
+    entry(ExtendedInterface, GetTrackArtworkTimes),
+    entry(ExtendedInterface, RetTrackArtworkTimes),
+    entry(ExtendedInterface, GetShuffle),
+    entry(ExtendedInterface, ReturnShuffle),
+    entry(ExtendedInterface, SetShuffle),
+    entry(ExtendedInterface, GetRepeat),
+    entry(ExtendedInterface, ReturnRepeat),
+    entry(ExtendedInterface, SetRepeat),
+    entry(ExtendedInterface, SetDisplayImage),
+    entry(ExtendedInterface, GetMonoDisplayImageLimits),
+    entry(ExtendedInterface, ReturnMonoDisplayImageLimits),
+    entry(ExtendedInterface, GetNumPlayingTracks),
+    entry(ExtendedInterface, ReturnNumPlayingTracks),
+    entry(ExtendedInterface, SetCurrentPlayingTrack),
+    entry(ExtendedInterface, SelectSortDBRecord),
+    entry(ExtendedInterface, GetColorDisplayImageLimits),
+    entry(ExtendedInterface, ReturnColorDisplayImageLimits),
+    entry(ExtendedInterface, ResetDBSelectionHierarchy),
+    entry(ExtendedInterface, GetDBITunesInfo),
+    entry(ExtendedInterface, RetDBITunesInfo),
+    entry(ExtendedInterface, GetUIDTrackInfo),
+    entry(ExtendedInterface, RetUIDTrackInfo),
+    entry(ExtendedInterface, GetDBTrackInfo),
+    entry(ExtendedInterface, RetDBTrackInfo),
+    entry(ExtendedInterface, GetPBTrackInfo),
+    entry(ExtendedInterface, RetPBTrackInfo),
+    entry(ExtendedInterface, CreateGeniusPlaylist),
+    entry(ExtendedInterface, RefreshGeniusPlaylist),
+    entry(ExtendedInterface, IsGeniusAvailableForTrack),
+    entry(ExtendedInterface, GetPlaylistInfo),
+    entry(ExtendedInterface, RetPlaylistInfo),
+    entry(ExtendedInterface, PrepareUIDList),
+    entry(ExtendedInterface, PlayPreparedUIDList),
+    entry(ExtendedInterface, GetArtworkTimes),
+    entry(ExtendedInterface, RetArtworkTimes),
+    entry(ExtendedInterface, GetArtworkData),
+    entry(ExtendedInterface, RetArtworkData),
+};
+static const char* strs_da[] = {
+    entry(DigitalAudio, AccessoryAck),
+    entry(DigitalAudio, IPodAck),
+    entry(DigitalAudio, GetAccessorySampleRateCaps),
+    entry(DigitalAudio, RetAccessorySampleRateCaps),
+    entry(DigitalAudio, TrackNewAudioAttributes),
+    entry(DigitalAudio, SetVideoDelay),
+};
+
+#undef entry
+
+#define entry(lingo, strs, size) [IAPLingoID_##lingo] = {#lingo, \
+                                                         strs,   \
+                                                         size}
+static struct {
+    const char*  lingo;
+    const char** strs;
+    size_t       size;
+} strs[] = {
+    entry(General, strs_general, array_size(strs_general)),
+    entry(Microphone, NULL, 0),
+    entry(SimpleRemote, strs_simple, array_size(strs_simple)),
+    entry(DisplayRemote, strs_display, array_size(strs_display)),
+    entry(ExtendedInterface, strs_ext, array_size(strs_ext)),
+    entry(AccessoryPower, NULL, 0),
+    entry(USBHostMode, NULL, 0),
+    entry(RFTuner, NULL, 0),
+    entry(AccessoryEqualizer, NULL, 0),
+    entry(Sports, NULL, 0),
+    entry(DigitalAudio, strs_da, array_size(strs_da)),
+    entry(Storage, NULL, 0),
+    entry(IPodOut, NULL, 0),
+    entry(Location, NULL, 0),
+};
+
+#undef entry
+
+const char* _iap_lingo_str_or_null(uint8_t lingo) {
+    check_ret(lingo < array_size(strs), NULL);
+    return strs[lingo].lingo;
+}
+
+const char* _iap_lingo_str(uint8_t lingo) {
+    const char* ret = _iap_lingo_str_or_null(lingo);
+    return ret ? ret : "?";
+}
+
+const char* _iap_command_str_or_null(uint8_t lingo, uint16_t command) {
+    check_ret(lingo < array_size(strs), NULL);
+    check_ret(command < strs[lingo].size, NULL);
+    return strs[lingo].strs[command];
+}
+
+const char* _iap_command_str(uint8_t lingo, uint16_t command) {
+    const char* ret = _iap_command_str_or_null(lingo, command);
+    return ret ? ret : "?";
+}
+
+IAPBool _iap_span_is_str(const struct IAPSpan* span) {
+    return span->size > 0 && span->ptr[span->size - 1] == '\0';
+}
+
+const char* _iap_span_as_str(const struct IAPSpan* span) {
+    return _iap_span_is_str(span) ? (char*)span->ptr : "(invalid)";
+}
+
+#if !defined(IAP_LOGF_MUTED)
+void _iap_dump_packet(uint8_t lingo, uint16_t command, int32_t trans_id, struct IAPSpan span) {
+    const char* lingo_str   = _iap_lingo_str_or_null(lingo);
+    const char* command_str = _iap_command_str_or_null(lingo, command);
+    if(lingo_str == NULL) {
+        IAP_LOGF("?(0x%02" PRIX8 ") trans=%" PRIi32, lingo, trans_id);
+        return;
+    } else if(command_str == NULL) {
+        IAP_LOGF("%s:?(0x%02" PRIX8 ") trans=%" PRIi32, lingo_str, command, trans_id);
+        return;
+    } else {
+        IAP_LOGF("%s:%s trans=%" PRIi32, lingo_str, command_str, trans_id);
+    }
+
+#define span_read(Type)                                                  \
+    const struct Type* payload = iap_span_read(&span, sizeof(*payload)); \
+    check_act(payload != NULL, return);
+
+    switch(lingo) {
+    case IAPLingoID_General:
+        switch(command) {
+        case IAPGeneralCommandID_IPodAck: {
+            span_read(IAPIPodAckPayload);
+            IAP_LOGF("  id=%s", _iap_command_str(lingo, payload->id));
+            IAP_LOGF("  status=0x%02" PRIX8, payload->status);
+        } break;
+        case IAPGeneralCommandID_ReturnExtendedInterfaceMode: {
+            span_read(IAPReturnExtendedInterfaceModePayload);
+            IAP_LOGF("  mode=%" PRIu8, payload->is_ext_mode);
+        } break;
+        case IAPGeneralCommandID_ReturnIPodSoftwareVersion: {
+            span_read(IAPReturnIPodSoftwareVersionPayload);
+            IAP_LOGF("  version=%" PRIu8 ".%" PRIu8 ".%" PRIu8, payload->major, payload->minor, payload->revision);
+        } break;
+        case IAPGeneralCommandID_ReturnIPodSerialNum: {
+            IAP_LOGF("  serial=%s", _iap_span_as_str(&span));
+        } break;
+        case IAPGeneralCommandID_ReturnIPodModelNum: {
+            IAP_LOGF("  model=%s", _iap_span_as_str(&span));
+        } break;
+        case IAPGeneralCommandID_RequestLingoProtocolVersion: {
+            span_read(IAPRequestLingoProtocolVersionPayload);
+            IAP_LOGF("  lingo=%s", _iap_lingo_str(payload->lingo));
+        } break;
+        case IAPGeneralCommandID_ReturnLingoProtocolVersion: {
+            span_read(IAPReturnLingoProtocolVersionPayload);
+            IAP_LOGF("  lingo=%s", _iap_lingo_str(payload->lingo));
+            IAP_LOGF("  version=%" PRIu8 ".%" PRIu8, payload->major, payload->minor);
+        } break;
+        case IAPGeneralCommandID_ReturnTransportMaxPayloadSize: {
+            span_read(IAPReturnTransportMaxPayloadSizePayload);
+            IAP_LOGF("  size=%" PRIu16, swap_16(payload->max_payload_size));
+        } break;
+        case IAPGeneralCommandID_IdentifyDeviceLingoes: {
+            span_read(IAPIdentifyDeviceLingoesPayload);
+            uint32_t bits = swap_32(payload->lingoes_bits);
+            for(int i = 0; i < 16; i += 1) {
+                if(bits & (1u << i)) {
+                    IAP_LOGF("  supports %s", _iap_lingo_str(i));
+                }
+            }
+            IAP_LOGF("  option=0x%02" PRIX32, swap_32(payload->options));
+            IAP_LOGF("  device_id=0x%04" PRIX32, swap_32(payload->device_id));
+        } break;
+        case IAPGeneralCommandID_RetIPodOptions: {
+            span_read(IAPRetIPodOptionsPayload);
+            IAP_LOGF("  state=0x%08" PRIX64, swap_64(payload->state));
+        } break;
+        case IAPGeneralCommandID_GetIPodPreferences: {
+            span_read(IAPGetIPodPreferencesPayload);
+            IAP_LOGF("  class=0x%02" PRIX8, payload->class_id);
+        } break;
+        case IAPGeneralCommandID_RetIPodPreferences: {
+            span_read(IAPSetIPodPreferencesPayload);
+            IAP_LOGF("  class=0x%02X", payload->class_id);
+            IAP_LOGF("  setting=0x%02X", payload->setting_id);
+        } break;
+        case IAPGeneralCommandID_SetIPodPreferences: {
+            span_read(IAPSetIPodPreferencesPayload);
+            IAP_LOGF("  class=0x%02" PRIX8, payload->class_id);
+            IAP_LOGF("  setting=0x%02" PRIX8, payload->setting_id);
+            IAP_LOGF("  roe=%" PRIu8, payload->restore_on_exit);
+        } break;
+        case IAPGeneralCommandID_SetUIMode: {
+            span_read(IAPSetUIModePayload);
+            IAP_LOGF("  mode=0x%02" PRIX8, payload->ui_mode);
+        } break;
+        case IAPGeneralCommandID_SetFIDTokenValues: {
+            span_read(IAPSetFIDTokenValuesPayload);
+            for(int i = 0; i < payload->num_token_values; i += 1) {
+                check_act(span.size > sizeof(struct IAPFIDTokenValuesToken), return);
+                struct IAPFIDTokenValuesToken* token_header = (void*)span.ptr;
+                struct IAPSpan                 token_span   = {
+                    span.ptr,
+                    token_header->length + 1 /* length does not include sizeof itself */,
+                };
+                check_act(span.size >= token_span.size, return);
+                iap_span_read(&span, token_span.size);
+
+                switch(token_header->type << 8 | token_header->subtype) {
+                case IAPFIDTokenTypes_Identify: {
+                    /* IAPFIDTokenValuesIdentifyToken contains vla, need to parse manually */
+                    const struct IAPFIDTokenValuesIdentifyTokenHead* token_head = iap_span_read(&token_span, sizeof(*token_head));
+                    check_act(token_head != NULL, return);
+                    print("accessory supported lingoes(%" PRIu8 "):", token_head->num_lingoes);
+                    for(int i = 0; i < token_head->num_lingoes; i += 1) {
+                        uint8_t lingo_id;
+                        check_act(iap_span_read_8(&token_span, &lingo_id), return);
+                        IAP_LOGF("  %s(0x%" PRIX8 ")", _iap_lingo_str(lingo_id), lingo_id);
+                    }
+                    const struct IAPFIDTokenValuesIdentifyTokenTail* token_tail = iap_span_read(&token_span, sizeof(*token_tail));
+                    check_act(token_tail != NULL, return);
+                    const uint32_t opt = swap_32(token_tail->device_option);
+                    const uint32_t id  = swap_32(token_tail->device_id);
+                    print("options=%04" PRIX32 " device_id=%04" PRIX32, opt, id);
+                } break;
+                case IAPFIDTokenTypes_AccCaps: {
+                    const struct IAPFIDTokenValuesAccCapsToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    const uint64_t caps = swap_64(token->caps_bits);
+                    print("accessory caps: %" PRIX64, caps);
+                } break;
+                case IAPFIDTokenTypes_AccInfo: {
+                    const struct IAPFIDTokenValuesAccInfoToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    switch(token->info_type) {
+                    case IAPFIDTokenValuesAccInfoTypes_AccName:
+                        print("accessory name: %s", _iap_span_as_str(&token_span));
+                        break;
+                    case IAPFIDTokenValuesAccInfoTypes_FirmwareVersion:
+                        check_act(token_span.size == 3, return);
+                        print("accessory firmware version: %" PRIX8 ".%" PRIX8 ".%" PRIX8, token_span.ptr[0], token_span.ptr[1], token_span.ptr[2]);
+                        break;
+                    case IAPFIDTokenValuesAccInfoTypes_HardwareVersion:
+                        check_act(token_span.size == 3, return);
+                        print("accessory hardware version: %" PRIX8 ".%" PRIX8 ".%" PRIX8, token_span.ptr[0], token_span.ptr[1], token_span.ptr[2]);
+                        break;
+                    case IAPFIDTokenValuesAccInfoTypes_Manufacture:
+                        print("accessory manufacture: %s", _iap_span_as_str(&token_span));
+                        break;
+                    case IAPFIDTokenValuesAccInfoTypes_ModelNumber:
+                        print("accessory model number: %s", _iap_span_as_str(&token_span));
+                        break;
+                    case IAPFIDTokenValuesAccInfoTypes_SerialNumber:
+                        print("accessory serial number: %s", _iap_span_as_str(&token_span));
+                        break;
+                    case IAPFIDTokenValuesAccInfoTypes_MaxPayloadSize: {
+                        uint16_t val;
+                        check_act(iap_span_read_16(&token_span, &val), return);
+                        print("accessory max payload size: %" PRIu16, val);
+                    } break;
+                    case IAPFIDTokenValuesAccInfoTypes_AccStatus: {
+                        uint32_t val;
+                        check_act(iap_span_read_32(&token_span, &val), return);
+                        print("accessory status: %" PRIX32, val);
+                    } break;
+                    case IAPFIDTokenValuesAccInfoTypes_RFCerts: {
+                        uint32_t val;
+                        check_act(iap_span_read_32(&token_span, &val), return);
+                        print("accessory rf cert: %" PRIX32, val);
+                    } break;
+                    }
+                } break;
+                case IAPFIDTokenTypes_IPodPreference: {
+                    const struct IAPFIDTokenValuesIPodPreferenceToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("accessory setting %" PRIX8 "=%" PRIX8, token->class_id, token->setting_id);
+                } break;
+                case IAPFIDTokenTypes_EAProtocol: {
+                    const struct IAPFIDTokenValuesEAProtocolToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("ea protocol %" PRIX8 "=%s", token->protocol_index, _iap_span_as_str(&token_span));
+                } break;
+                case IAPFIDTokenTypes_BundleSeedIDPref: {
+                    const struct IAPFIDTokenValuesBundleSeedIDPrefToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("bundle seed id %.10s", token->bundle_seed_id_string);
+                } break;
+                case IAPFIDTokenTypes_ScreenInfo: {
+                    const struct IAPFIDTokenValuesScreenInfoToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("screen info:");
+                    IAP_LOGF("  screen size(inch): %" PRIu16 "x%" PRIu16, swap_16(token->total_screen_width_inches), swap_16(token->total_screen_height_inches));
+                    IAP_LOGF("  screen size(pixel): %" PRIu16 "x%" PRIu16, swap_16(token->total_screen_width_pixels), swap_16(token->total_screen_height_pixels));
+                    IAP_LOGF("  ipod out size(pixel): %" PRIu16 "x%" PRIu16, swap_16(token->ipod_out_screen_width_pixels), swap_16(token->ipod_out_screen_height_pixels));
+                    IAP_LOGF("  feature: %" PRIX8, token->screen_feature_mask);
+                    IAP_LOGF("  gamma: %" PRIu8, token->screen_gamma_value);
+                } break;
+                case IAPFIDTokenTypes_EAProtocolMetadata: {
+                    const struct IAPFIDTokenValuesEAProtocolMetadataToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("ea protocol metadata %" PRIX8 "=%" PRIX8, token->protocol_index, token->metadata_type);
+                } break;
+                case IAPFIDTokenTypes_AccDigitalAudioSampleRates: {
+                    const struct IAPFIDTokenValuesAccDigitalAudioSampleRatesToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("accessory supported audio sample rates:");
+                    while(token_span.size > 0) {
+                        uint32_t rate;
+                        check_act(iap_span_read_32(&token_span, &rate), return);
+                        IAP_LOGF("  %" PRIu32, rate);
+                    }
+                } break;
+                case IAPFIDTokenTypes_AccDigitalAudioVideoDelay: {
+                    const struct IAPFIDTokenValuesAccDigitalAudioVideoDelayToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("accessory video delay: %" PRIu32, token->delay);
+                } break;
+                case IAPFIDTokenTypes_MicrophoneCaps: {
+                    const struct IAPFIDTokenValuesMicrophoneCapsToken* token = iap_span_read(&token_span, sizeof(*token));
+                    check_act(token != NULL, return);
+                    print("accessory microphone caps: %" PRIX32, token->caps_bits);
+                } break;
+                default:
+                    print("unknown fid %04" PRIX16, token_header->type << 8 | token_header->subtype);
+                }
+            }
+        } break;
+        case IAPGeneralCommandID_EndIDPS: {
+            span_read(IAPEndIDPSPayload);
+            IAP_LOGF("  status=0x%02" PRIX8, payload->status);
+        } break;
+        case IAPGeneralCommandID_GetIPodOptionsForLingo: {
+            span_read(IAPGetIPodOptionsForLingoPayload);
+            IAP_LOGF("  lingo=%s", _iap_lingo_str(payload->lingo_id));
+        } break;
+        case IAPGeneralCommandID_RetSupportedEventNotification: {
+            span_read(IAPRetSupportedEventNotificationPayload);
+            IAP_LOGF("  mask=0x%08" PRIX64, swap_64(payload->mask));
+        } break;
+        case IAPGeneralCommandID_SetAvailableCurrent: {
+            span_read(IAPSetAvailableCurrentPayload);
+            IAP_LOGF("  current=%" PRIu16 "mA", swap_16(payload->current_limit_ma));
+        } break;
+        case IAPGeneralCommandID_SetEventNotification: {
+            span_read(IAPSetEventNotificationPayload);
+            IAP_LOGF("  mask=0x%08" PRIX64, swap_64(payload->mask));
+        } break;
+        }
+        break;
+    case IAPLingoID_SimpleRemote:
+        switch(command) {
+        case IAPSimpleRemoteCommandID_ContextButtonStatus: {
+            uint8_t bits;
+            check_ret(iap_span_read_8(&span, &bits), );
+            IAP_LOGF("  bits0=0x%02" PRIX8, bits);
+            uint8_t index = 1;
+            while(span.size > 0) {
+                iap_span_read_8(&span, &bits);
+                IAP_LOGF("  bits%d=0x%02" PRIX8, index, bits);
+                index += 1;
+            }
+        } break;
+        case IAPSimpleRemoteCommandID_IPodAck: {
+            span_read(IAPIPodAckPayload);
+            IAP_LOGF("  id=%s", _iap_command_str(lingo, payload->id));
+            IAP_LOGF("  status=0x%02" PRIX8, payload->status);
+        } break;
+        }
+        break;
+    case IAPLingoID_DisplayRemote:
+        switch(command) {
+        case IAPDisplayRemoteCommandID_IPodAck: {
+            span_read(IAPIPodAckPayload);
+            IAP_LOGF("  id=%s", _iap_command_str(lingo, payload->id));
+            IAP_LOGF("  status=0x%02" PRIX8, payload->status);
+        } break;
+        case IAPDisplayRemoteCommandID_SetCurrentEQProfileIndex: {
+            span_read(IAPSetCurrentEQProfileIndexPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+            IAP_LOGF("  roe=%" PRIu8, payload->restore_on_exit);
+        } break;
+        case IAPDisplayRemoteCommandID_SetRemoteEventNotification: {
+            span_read(IAPSetRemoteEventNotificationPayload);
+            IAP_LOGF("  mask=0x%04" PRIX32, swap_32(payload->mask));
+        } break;
+        case IAPDisplayRemoteCommandID_RemoteEventNotification: {
+            check_ret(span.size >= 1, );
+            IAP_LOGF("  type=0x%02" PRIX8, span.ptr[0]);
+            switch(span.ptr[0]) {
+            case IAPIPodStateType_TrackTimePositionMSec: {
+                span_read(IAPIPodStateTrackTimePositionMSecPayload);
+                IAP_LOGF("  position=%" PRIu32 "ms", swap_32(payload->position_ms));
+            } break;
+            case IAPIPodStateType_TrackPlaybackIndex: {
+                span_read(IAPIPodStateTrackPlaybackIndexPayload);
+                IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+            } break;
+            case IAPIPodStateType_ChapterIndex: {
+                span_read(IAPIPodStateChapterIndexPayload);
+                IAP_LOGF("  chapter_index=%" PRIu16, swap_16(payload->chapter_index));
+                IAP_LOGF("  chapter_count=%" PRIu16, swap_16(payload->chapter_count));
+            } break;
+            case IAPIPodStateType_PlayStatus: {
+                span_read(IAPIPodStatePlayStatusPayload);
+                IAP_LOGF("  play_status=%" PRIu8, payload->status);
+            } break;
+            case IAPIPodStateType_Volume: {
+                span_read(IAPIPodStateVolumePayload);
+                IAP_LOGF("  mute=%" PRIu8, payload->mute_state);
+                IAP_LOGF("  volume=%" PRIu8, payload->ui_volume);
+            } break;
+            case IAPIPodStateType_Power: {
+                span_read(IAPIPodStatePowerPayload);
+                IAP_LOGF("  power_state=%" PRIu8, payload->power_state);
+                IAP_LOGF("  battery_level=%" PRIu8, payload->battery_level);
+            } break;
+            case IAPIPodStateType_EQSetting: {
+                span_read(IAPIPodStateEQSettingPayload);
+                IAP_LOGF("  eq_index=%" PRIu32, swap_32(payload->eq_index));
+            } break;
+            case IAPIPodStateType_ShuffleSetting: {
+                span_read(IAPIPodStateShuffleSettingPayload);
+                IAP_LOGF("  shuffle_state=%" PRIu8, payload->shuffle_state);
+            } break;
+            case IAPIPodStateType_RepeatSetting: {
+                span_read(IAPIPodStateRepeatSettingPayload);
+                IAP_LOGF("  repeat_state=%" PRIu8, payload->repeat_state);
+            } break;
+            case IAPIPodStateType_DateTimeSetting: {
+                span_read(IAPIPodStateDateTimeSettingPayload);
+                IAP_LOGF("  time=%04" PRIu16 "-%02" PRIu8 "-%02" PRIu8 " %02" PRIu8 ":%02" PRIu8, swap_16(payload->year), payload->month, payload->day, payload->hour, payload->minute);
+            } break;
+            case IAPIPodStateType_AlarmSetting: {
+                span_read(IAPIPodStateAlarmSettingPayload);
+                IAP_LOGF("  alarm");
+            } break;
+            case IAPIPodStateType_BacklightLevel: {
+                span_read(IAPIPodStateBacklightLevelPayload);
+                IAP_LOGF("  backlight_level=%" PRIu8, payload->level);
+            } break;
+            case IAPIPodStateType_HoldSwitchState: {
+                span_read(IAPIPodStateHoldSwitchStatePayload);
+                IAP_LOGF("  hold_switch_state=%" PRIu8, payload->state);
+            } break;
+            case IAPIPodStateType_SoundCheckState: {
+                span_read(IAPIPodStateSoundCheckStatePayload);
+                IAP_LOGF("  sound_check_state=%" PRIu8, payload->state);
+            } break;
+            case IAPIPodStateType_AudiobookSpeeed: {
+                span_read(IAPIPodStateAudiobookSpeeedPayload);
+                IAP_LOGF("  audio_book_speed=%" PRIu8, payload->speed);
+            } break;
+            case IAPIPodStateType_TrackTimePositionSec: {
+                span_read(IAPIPodStateTrackTimePositionSecPayload);
+                IAP_LOGF("  position=%us", swap_16(payload->position_s));
+            } break;
+            case IAPIPodStateType_AbsoluteVolume: {
+                span_read(IAPIPodStateAbsoluteVolumePayload);
+                IAP_LOGF("  mute=%" PRIu8, payload->mute_state);
+                IAP_LOGF("  volume=%" PRIu8, payload->ui_volume);
+                IAP_LOGF("  absolute_volume=%" PRIu8, payload->absolute_volume);
+            } break;
+            case IAPIPodStateType_TrackCaps: {
+                span_read(IAPIPodStateTrackCapsPayload);
+                IAP_LOGF("  track_caps=0x%04" PRIX32, swap_32(payload->caps));
+            } break;
+            case IAPIPodStateType_PlaybackEngineContents: {
+                span_read(IAPIPodStatePlaybackEngineContentsPayload);
+                IAP_LOGF("  playback_engine_contents=%" PRIu32, swap_32(payload->count));
+            } break;
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_GetIPodStateInfo: {
+            span_read(IAPGetIPodStateInfoPayload);
+            IAP_LOGF("  type=0x%02" PRIX8, payload->type);
+        } break;
+        case IAPDisplayRemoteCommandID_RetIPodStateInfo: {
+            check_ret(span.size > 1, );
+            IAP_LOGF("  type=0x%02" PRIX8, span.ptr[0]);
+            switch(span.ptr[0]) {
+            case IAPIPodStateType_TrackTimePositionMSec: {
+                span_read(IAPIPodStateTrackTimePositionMSecPayload);
+                IAP_LOGF("  position=%" PRIu32 "ms", swap_32(payload->position_ms));
+            } break;
+            case IAPIPodStateType_TrackPlaybackIndex: {
+                span_read(IAPIPodStateTrackPlaybackIndexPayload);
+                IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+            } break;
+            case IAPIPodStateType_ChapterIndex: {
+                span_read(IAPIPodStateChapterIndexPayload);
+                IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+                IAP_LOGF("  cindex=%" PRIu16, swap_16(payload->chapter_index));
+                IAP_LOGF("  ccount=%" PRIu16, swap_16(payload->chapter_count));
+            } break;
+            case IAPIPodStateType_PlayStatus: {
+                span_read(IAPIPodStatePlayStatusPayload);
+                IAP_LOGF("  status=%" PRIu8, payload->status);
+            } break;
+            case IAPIPodStateType_Volume: {
+                span_read(IAPIPodStateVolumePayload);
+                IAP_LOGF("  ui_volume=%" PRIu8, payload->ui_volume);
+                IAP_LOGF("  mute_state=%" PRIu8, payload->mute_state);
+            } break;
+            case IAPIPodStateType_Power: {
+                span_read(IAPIPodStatePowerPayload);
+                IAP_LOGF("  power_state=0x%02" PRIX8, payload->power_state);
+                IAP_LOGF("  battery_level=%" PRIu8, payload->battery_level);
+            } break;
+            case IAPIPodStateType_EQSetting: {
+                span_read(IAPIPodStateEQSettingPayload);
+                IAP_LOGF("  eq_index=%" PRIu32, swap_32(payload->eq_index));
+            } break;
+            case IAPIPodStateType_ShuffleSetting: {
+                span_read(IAPIPodStateShuffleSettingPayload);
+                IAP_LOGF("  shuffle_state=%" PRIu8, payload->shuffle_state);
+            } break;
+            case IAPIPodStateType_RepeatSetting: {
+                span_read(IAPIPodStateRepeatSettingPayload);
+                IAP_LOGF("  repeat_state=%" PRIu8, payload->repeat_state);
+            } break;
+            case IAPIPodStateType_DateTimeSetting: {
+                span_read(IAPIPodStateDateTimeSettingPayload);
+                IAP_LOGF("  time=%04" PRIu16 "-%02" PRIu8 "-%02" PRIu8 " %02" PRIu8 ":%02" PRIu8, swap_16(payload->year), payload->month, payload->day, payload->hour, payload->minute);
+            } break;
+            case IAPIPodStateType_AlarmSetting: {
+            } break;
+            case IAPIPodStateType_BacklightLevel: {
+                span_read(IAPIPodStateBacklightLevelPayload);
+                IAP_LOGF("  level=%" PRIu8, payload->level);
+            } break;
+            case IAPIPodStateType_HoldSwitchState: {
+                span_read(IAPIPodStateHoldSwitchStatePayload);
+                IAP_LOGF("  state=%" PRIu8, payload->state);
+            } break;
+            case IAPIPodStateType_SoundCheckState: {
+                span_read(IAPIPodStateSoundCheckStatePayload);
+                IAP_LOGF("  state=%" PRIu8, payload->state);
+            } break;
+            case IAPIPodStateType_AudiobookSpeeed: {
+                span_read(IAPIPodStateAudiobookSpeeedPayload);
+                IAP_LOGF("  speed=%" PRIu8, payload->speed);
+            } break;
+            case IAPIPodStateType_TrackTimePositionSec: {
+                span_read(IAPIPodStateTrackTimePositionSecPayload);
+                IAP_LOGF("  position=%" PRIu16 "s", swap_16(payload->position_s));
+            } break;
+            case IAPIPodStateType_AbsoluteVolume: {
+                span_read(IAPIPodStateAbsoluteVolumePayload);
+                IAP_LOGF("  mute=%" PRIu8, payload->mute_state);
+                IAP_LOGF("  ui_volume=%" PRIu8, payload->ui_volume);
+                IAP_LOGF("  absolute_volume=%" PRIu8, payload->absolute_volume);
+            } break;
+            case IAPIPodStateType_TrackCaps: {
+                span_read(IAPIPodStateTrackCapsPayload);
+                IAP_LOGF("  caps=0x%04" PRIX32, swap_32(payload->caps));
+            } break;
+            case IAPIPodStateType_PlaybackEngineContents: {
+                span_read(IAPIPodStatePlaybackEngineContentsPayload);
+                IAP_LOGF("  count=%" PRIu32, swap_32(payload->count));
+            } break;
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_RetPlayStatus: {
+            span_read(IAPRetPlayStatusPayload);
+            IAP_LOGF("  state=0x%02" PRIX8, payload->state);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->track_index));
+            IAP_LOGF("  pos=%" PRIu32 "ms", swap_32(payload->track_pos_ms));
+            IAP_LOGF("  total=%" PRIu32 "ms", swap_32(payload->track_total_ms));
+        } break;
+        case IAPDisplayRemoteCommandID_GetIndexedPlayingTrackInfo: {
+            span_read(IAPGetIndexedPlayingTrackInfoPayload);
+            IAP_LOGF("  type=0x%02" PRIX8, payload->type);
+            IAP_LOGF("  track=%" PRIu32, swap_32(payload->track_index));
+            IAP_LOGF("  chapter=%" PRIu16, swap_16(payload->chapter_index));
+        } break;
+        case IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo: {
+            check_ret(span.size > 1, );
+            IAP_LOGF("  type=0x%02" PRIX8, span.ptr[0]);
+            switch(span.ptr[0]) {
+            case IAPIndexedPlayingTrackInfoType_TrackCapsInfo: {
+                span_read(IAPRetIndexedPlayingTrackInfoTrackCapsInfoPayload);
+                IAP_LOGF("  caps=0x%04" PRIX32, swap_32(payload->track_caps));
+                IAP_LOGF("  total=%" PRIu32 "ms", swap_32(payload->track_total_ms));
+                IAP_LOGF("  chapter_count=%" PRIu16, swap_16(payload->chapter_count));
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ChapterTimeName: {
+                span_read(IAPRetIndexedPlayingTrackInfoChapterTimeNamePayload);
+                IAP_LOGF("  offset=%" PRIu32 "ms", swap_32(payload->offset_ms));
+                IAP_LOGF("  name=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ArtistName:
+            case IAPIndexedPlayingTrackInfoType_AlbumName:
+            case IAPIndexedPlayingTrackInfoType_GenreName:
+            case IAPIndexedPlayingTrackInfoType_ComposerName: {
+                IAP_LOGF("  str=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPIndexedPlayingTrackInfoType_Lyrics: {
+                span_read(IAPRetIndexedPlayingTrackInfoLyricsPayload);
+                IAP_LOGF("  info=0x%02" PRIX8, payload->info_bits);
+                IAP_LOGF("  index=%" PRIu16, swap_16(payload->index));
+                IAP_LOGF("  lyrics=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ArtworkCount: {
+                span_read(IAPRetIndexedPlayingTrackInfoArtworkCountPayload);
+                while(span.size >= sizeof(struct IAPArtworkCount)) {
+                    const struct IAPArtworkCount* count = iap_span_read(&span, sizeof(*count));
+                    IAP_LOGF("  format=%" PRIu16 ", count=%" PRIu16, swap_16(count->format), swap_16(count->count));
+                }
+            } break;
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_RetNumPlayingTracks: {
+            span_read(IAPRetNumPlayingTracksPayload);
+            IAP_LOGF("  tracks=%" PRIu32, swap_32(payload->num_playing_tracks));
+        } break;
+        case IAPDisplayRemoteCommandID_RetArtworkFormats: {
+            while(span.size >= sizeof(struct IAPArtworkFormat)) {
+                const struct IAPArtworkFormat* format = iap_span_read(&span, sizeof(*format));
+                IAP_LOGF("  id=%" PRIu16 ", format=%" PRIu8 ", width=%" PRIu16 ", height=%" PRIu16, swap_16(format->format_id), format->pixel_format, swap_16(format->image_width), swap_16(format->image_height));
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_GetTrackArtworkData: {
+            span_read(IAPGetTrackArtworkDataPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->track_index));
+            IAP_LOGF("  format=%" PRIu8, payload->format_id);
+            IAP_LOGF("  offset=%" PRIu32 "ms", swap_32(payload->offset_ms));
+        } break;
+        case IAPDisplayRemoteCommandID_RetTrackArtworkData: {
+            uint16_t index;
+            check_ret(iap_span_peek_16(&span, &index), );
+            if(index == 0) {
+                span_read(IAPRetTrackArtworkDataFirstPayload);
+                IAP_LOGF("  index=%" PRIu16, swap_16(payload->index));
+                IAP_LOGF("  format=%" PRIu16, swap_16(payload->pixel_format));
+                IAP_LOGF("  width=%" PRIu16, swap_16(payload->pixel_width));
+                IAP_LOGF("  height=%" PRIu16, swap_16(payload->pixel_height));
+            } else {
+                span_read(IAPRetTrackArtworkDataSubsequenctPayload);
+                IAP_LOGF("  index=%" PRIu16, swap_16(payload->index));
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_RetPowerBatteryState: {
+            span_read(IAPRetPowerBatteryStatePayload);
+            IAP_LOGF("  power_state=%" PRIu8, payload->power_state);
+            IAP_LOGF("  battery_level=%" PRIu8, payload->battery_level);
+        } break;
+        case IAPDisplayRemoteCommandID_GetTrackArtworkTimes: {
+            span_read(IAPGetTrackArtworkTimesPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->track_index));
+            IAP_LOGF("  format=%" PRIu16, swap_16(payload->format_id));
+            IAP_LOGF("  artwork_index=%" PRIu32, swap_32(payload->artwork_index));
+            IAP_LOGF("  artwork_count=%" PRIu32, swap_32(payload->artwork_count));
+        } break;
+        case IAPDisplayRemoteCommandID_RetTrackArtworkTimes: {
+            while(span.size >= sizeof(uint16_t)) {
+                uint16_t time;
+                iap_span_read_16(&span, &time);
+                IAP_LOGF("  time=%" PRIu16, time);
+            }
+        } break;
+        }
+        break;
+    case IAPLingoID_ExtendedInterface:
+        switch(command) {
+        case IAPExtendedInterfaceCommandID_IPodAck: {
+            span_read(IAPExtendedIPodAckPayload);
+            IAP_LOGF("  id=%s", _iap_command_str(lingo, swap_16(payload->id)));
+            IAP_LOGF("  status=0x%02" PRIX8, payload->status);
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackChapterInfo: {
+            span_read(IAPReturnCurrentPlayingTrackChapterInfoPayload);
+            IAP_LOGF("  count=%" PRIu32, swap_32(payload->count));
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+        } break;
+        case IAPExtendedInterfaceCommandID_RetAudiobookSpeed: {
+            span_read(IAPRetAudiobookSpeedPayload);
+            IAP_LOGF("  speed=0x%02" PRIX8, payload->speed);
+        } break;
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackInfo: {
+            span_read(IAPExtendedGetIndexedPlayingTrackInfoPayload);
+            IAP_LOGF("  type=0x%02" PRIX8, payload->type);
+            IAP_LOGF("  track=%" PRIu32, swap_32(payload->track_index));
+            IAP_LOGF("  chapter=%" PRIu16, swap_16(payload->chapter_index));
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo: {
+            check_ret(span.size > 1, );
+            IAP_LOGF("  type=0x%02" PRIX8, span.ptr[0]);
+            switch(span.ptr[0]) {
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackCapsInfo: {
+                span_read(IAPExtendedRetIndexedPlayingTrackInfoTrackCapsInfoPayload);
+                IAP_LOGF("  caps=0x%04" PRIX32, swap_32(payload->track_caps));
+                IAP_LOGF("  total=%" PRIu32 "ms", swap_32(payload->track_total_ms));
+                IAP_LOGF("  chapter_count=%" PRIu16, swap_16(payload->chapter_count));
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_PodcastName: {
+                IAP_LOGF("  str=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackReleaseDate: {
+                span_read(IAPExtendedRetIndexedPlayingTrackInfoTrackReleaseDatePayload);
+                IAP_LOGF("  release=%04" PRIu16 "-%02" PRIu8 "-%02" PRIu8 " %02" PRIu8 ":%02" PRIu8 ".%02" PRIu8, swap_16(payload->year), payload->month, payload->day, payload->hours, payload->minutes, payload->seconds);
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackDescription: {
+                span_read(IAPExtendedRetIndexedPlayingTrackInfoTrackDescriptionPayload);
+                IAP_LOGF("  info=0x%02" PRIX8, payload->info_bits);
+                IAP_LOGF("  index=%" PRIu8, swap_16(payload->index));
+                IAP_LOGF("  desc=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackSongLyrics: {
+                span_read(IAPExtendedRetIndexedPlayingTrackInfoTrackSongLyricsPayload);
+                IAP_LOGF("  info=0x%02" PRIX8, payload->info_bits);
+                IAP_LOGF("  index=%u" PRIu16, swap_16(payload->index));
+                IAP_LOGF("  lyrics=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackGenre: {
+                IAP_LOGF("  str=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackComposer: {
+                IAP_LOGF("  str=%s", _iap_span_as_str(&span));
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackArtworkCount: {
+                span_read(IAPRetIndexedPlayingTrackInfoArtworkCountPayload);
+                while(span.size >= sizeof(struct IAPArtworkCount)) {
+                    const struct IAPArtworkCount* count = iap_span_read(&span, sizeof(*count));
+                    IAP_LOGF("  format=%" PRIu16 ", count=%" PRIu16, swap_16(count->format), swap_16(count->count));
+                }
+            } break;
+            }
+        } break;
+        case IAPExtendedInterfaceCommandID_RetArtworkFormats: {
+            while(span.size >= sizeof(struct IAPArtworkFormat)) {
+                const struct IAPArtworkFormat* format = iap_span_read(&span, sizeof(*format));
+                IAP_LOGF("  id=%" PRIu16 ", format=%" PRIu8 ", width=%" PRIu16 ", height=%" PRIu16, swap_16(format->format_id), format->pixel_format, swap_16(format->image_width), swap_16(format->image_height));
+            }
+        } break;
+        case IAPExtendedInterfaceCommandID_GetTrackArtworkData: {
+            span_read(IAPGetTrackArtworkDataPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->track_index));
+            IAP_LOGF("  format=%" PRIu8, payload->format_id);
+            IAP_LOGF("  offset=%" PRIu32 "ms", swap_32(payload->offset_ms));
+        } break;
+        case IAPExtendedInterfaceCommandID_RetTrackArtworkData: {
+            uint16_t index;
+            check_ret(iap_span_peek_16(&span, &index), );
+            if(index == 0) {
+                span_read(IAPRetTrackArtworkDataFirstPayload);
+                IAP_LOGF("  index=%" PRIu16, swap_16(payload->index));
+                IAP_LOGF("  format=%" PRIu16, swap_16(payload->pixel_format));
+                IAP_LOGF("  width=%" PRIu16, swap_16(payload->pixel_width));
+                IAP_LOGF("  height=%" PRIu16, swap_16(payload->pixel_height));
+            } else {
+                span_read(IAPRetTrackArtworkDataSubsequenctPayload);
+                IAP_LOGF("  index=%" PRIu16, swap_16(payload->index));
+            }
+        } break;
+        case IAPExtendedInterfaceCommandID_GetNumberCategorizedDBRecords: {
+            span_read(IAPGetNumberCategorizedDBRecordsPayload);
+            IAP_LOGF("  type=0x%02" PRIX8, payload->type);
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnNumberCategorizedDBRecords: {
+            span_read(IAPReturnNumberCategorizedDBRecordsPayload);
+            IAP_LOGF("  count=%" PRIu32, swap_32(payload->count));
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnPlayStatus: {
+            span_read(IAPExtendedRetPlayStatusPayload);
+            IAP_LOGF("  state=0x%02" PRIX8, payload->state);
+            IAP_LOGF("  pos=%" PRIu32 "ms", swap_32(payload->track_pos_ms));
+            IAP_LOGF("  total=%" PRIu32 "ms", swap_32(payload->track_total_ms));
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackIndex: {
+            span_read(IAPReturnCurrentPlayingTrackIndexPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+        } break;
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackTitle:
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackArtistName:
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackAlbumName: {
+            span_read(IAPGetIndexedPlayingTrackStringPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackTitle:
+        case IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackArtistName:
+        case IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackAlbumName: {
+            IAP_LOGF("  str=%s", _iap_span_as_str(&span));
+        } break;
+        case IAPExtendedInterfaceCommandID_SetPlayStatusChangeNotification: {
+            if(span.size == sizeof(struct IAPSetPlayStatusChangeNotification1BytePayload)) {
+                span_read(IAPSetPlayStatusChangeNotification1BytePayload);
+                IAP_LOGF("  enable=%" PRIu8, payload->enable);
+            } else {
+                span_read(IAPSetPlayStatusChangeNotification4BytesPayload);
+                IAP_LOGF("  mask=0x%08" PRIX32, swap_32(payload->mask));
+            }
+        } break;
+        case IAPExtendedInterfaceCommandID_PlayControl: {
+            span_read(IAPPlayControlPayload);
+            IAP_LOGF("  code=0x%02" PRIX8, payload->code);
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnShuffle: {
+            span_read(IAPReturnShufflePayload);
+            IAP_LOGF("  mode=%" PRIu8, payload->mode);
+        } break;
+        case IAPExtendedInterfaceCommandID_SetShuffle: {
+            span_read(IAPSetShufflePayload);
+            IAP_LOGF("  mode=%" PRIu8, payload->mode);
+            IAP_LOGF("  roe=%" PRIu8, payload->restore_on_exit);
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnRepeat: {
+            span_read(IAPReturnRepeatPayload);
+            IAP_LOGF("  mode=%" PRIu8, payload->mode);
+        } break;
+        case IAPExtendedInterfaceCommandID_SetRepeat: {
+            span_read(IAPSetRepeatPayload);
+            IAP_LOGF("  mode=%" PRIu8, payload->mode);
+            IAP_LOGF("  roe=%" PRIu8, payload->restore_on_exit);
+        } break;
+        case IAPExtendedInterfaceCommandID_SetDisplayImage: {
+            uint16_t index;
+            check_ret(iap_span_peek_16(&span, &index), );
+            IAP_LOGF("  index=%" PRIu16, index);
+            if(index == 0) {
+                span_read(IAPSetDisplayImageFirstPayload);
+                IAP_LOGF("  format=%02" PRIX8, payload->pixel_format);
+                IAP_LOGF("  width=%" PRIu16, swap_16(payload->pixel_width));
+                IAP_LOGF("  height=%" PRIu16, swap_16(payload->pixel_height));
+            }
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnNumPlayingTracks: {
+            span_read(IAPRetNumPlayingTracksPayload);
+            IAP_LOGF("  tracks=%" PRIu32, swap_32(payload->num_playing_tracks));
+        } break;
+        case IAPExtendedInterfaceCommandID_SetCurrentPlayingTrack: {
+            span_read(IAPSetCurrentPlayingTrackPayload);
+            IAP_LOGF("  index=%" PRIu32, swap_32(payload->index));
+        } break;
+        case IAPExtendedInterfaceCommandID_ReturnColorDisplayImageLimits: {
+            span_read(IAPColorDisplayImageLimit);
+            IAP_LOGF("  width=%" PRIu16, swap_16(payload->max_width));
+            IAP_LOGF("  height=%" PRIu16, swap_16(payload->max_height));
+            IAP_LOGF("  format=0x%02" PRIX8, payload->pixel_format);
+        } break;
+        }
+        break;
+    case IAPLingoID_DigitalAudio:
+        switch(command) {
+        case IAPDigitalAudioCommandID_AccessoryAck: {
+            span_read(IAPAccAckPayload);
+            IAP_LOGF("  id=%s", _iap_command_str(lingo, payload->id));
+            IAP_LOGF("  status=0x%02" PRIX8, payload->status);
+        } break;
+        case IAPDigitalAudioCommandID_RetAccessorySampleRateCaps: {
+            while(span.size >= sizeof(uint32_t)) {
+                uint32_t sample_rate;
+                iap_span_read_32(&span, &sample_rate);
+                IAP_LOGF("  rate=%" PRIu32, sample_rate);
+            }
+        } break;
+        }
+        break;
+    }
+#undef span_read
+}
+#else
+void _iap_dump_packet(uint8_t lingo, uint16_t command, int32_t trans_id, struct IAPSpan span) {
+    (void)lingo;
+    (void)command;
+    (void)trans_id;
+    (void)span;
+}
+#endif
diff --git a/firmware/usbstack/iap/libiap/endian.h b/firmware/usbstack/iap/libiap/endian.h
new file mode 100644
index 0000000000..1a3f011ee8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/endian.h
@@ -0,0 +1,28 @@
+#pragma once
+#include <stdint.h>
+
+__attribute__((unused)) static uint8_t swap_8(uint8_t num) {
+    return num;
+}
+
+__attribute__((unused)) static uint16_t swap_16(uint16_t num) {
+    return (num >> 8) | (num << 8);
+}
+
+__attribute__((unused)) static uint32_t swap_32(uint32_t num) {
+    return (num & 0xFF000000) >> 24 |
+           (num & 0x00FF0000) >> 8 |
+           (num & 0x0000FF00) << 8 |
+           (num & 0x000000FF) << 24;
+}
+
+__attribute__((unused)) static uint64_t swap_64(uint64_t num) {
+    return (num & 0xFF00000000000000) >> 56 |
+           (num & 0x00FF000000000000) >> 40 |
+           (num & 0x0000FF0000000000) << 24 |
+           (num & 0x000000FF00000000) << 8 |
+           (num & 0x000000000FF00000) << 8 |
+           (num & 0x00000000000FF000) << 24 |
+           (num & 0x000000000000FF00) << 40 |
+           (num & 0x00000000000000FF) << 56;
+}
diff --git a/firmware/usbstack/iap/libiap/fid-token-values.c b/firmware/usbstack/iap/libiap/fid-token-values.c
new file mode 100644
index 0000000000..eeca46533b
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/fid-token-values.c
@@ -0,0 +1,141 @@
+#include "endian.h"
+#include "iap.h"
+#include "macros.h"
+#include "span.h"
+#include "spec/iap.h"
+
+#define pack_accepted(Ack)                                          \
+    struct Ack* const ack = iap_span_alloc(response, sizeof(*ack)); \
+    check_ret(ack != NULL, -IAPAckStatus_EOutOfResource);           \
+    ack->length  = sizeof(struct Ack) - 1;                          \
+    ack->type    = token_header->type;                              \
+    ack->subtype = token_header->subtype;                           \
+    ack->status  = IAPFIDTokenValuesIdentifyAckStatus_Accepted;
+
+int _iap_hanlde_set_fid_token_values(struct IAPSpan* request, struct IAPSpan* response) {
+    const struct IAPSetFIDTokenValuesPayload* request_payload = iap_span_read(request, sizeof(*request_payload));
+    check_ret(request_payload != NULL, -IAPAckStatus_EBadParameter);
+    check_ret(iap_span_write_8(response, request_payload->num_token_values), -IAPAckStatus_EOutOfResource);
+    for(int i = 0; i < request_payload->num_token_values; i += 1) {
+        check_ret(request->size > sizeof(struct IAPFIDTokenValuesToken), -IAPAckStatus_EBadParameter);
+        struct IAPFIDTokenValuesToken* token_header = (void*)request->ptr;
+        struct IAPSpan                 token_span   = {
+            request->ptr,
+            token_header->length + 1 /* length does not include sizeof itself */,
+        };
+        check_ret(request->size >= token_span.size, -IAPAckStatus_EBadParameter);
+        iap_span_read(request, token_span.size);
+
+        switch(token_header->type << 8 | token_header->subtype) {
+        case IAPFIDTokenTypes_Identify: {
+            /* IAPFIDTokenValuesIdentifyToken contains vla, need to parse manually */
+            const struct IAPFIDTokenValuesIdentifyTokenHead* token_head = iap_span_read(&token_span, sizeof(*token_head));
+            check_ret(token_head != NULL, -IAPAckStatus_EBadParameter);
+            for(int i = 0; i < token_head->num_lingoes; i += 1) {
+                uint8_t lingo_id;
+                check_ret(iap_span_read_8(&token_span, &lingo_id), -IAPAckStatus_EBadParameter);
+            }
+            const struct IAPFIDTokenValuesIdentifyTokenTail* token_tail = iap_span_read(&token_span, sizeof(*token_tail));
+            check_ret(token_tail != NULL, -IAPAckStatus_EBadParameter);
+            const uint32_t opt = swap_32(token_tail->device_option);
+            const uint32_t id  = swap_32(token_tail->device_id);
+            pack_accepted(IAPFIDTokenValuesIdentifyAck);
+            if(opt != IAPIdentifyDeviceLingoesOptions_ImmediateAuth) {
+                ack->status = IAPFIDTokenValuesIdentifyAckStatus_RequiredFailed;
+            }
+            (void)id;
+        } break;
+        case IAPFIDTokenTypes_AccCaps: {
+            const struct IAPFIDTokenValuesAccCapsToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            const uint64_t caps = swap_64(token->caps_bits);
+            (void)caps;
+            pack_accepted(IAPFIDTokenValuesAccCapsAck);
+        } break;
+        case IAPFIDTokenTypes_AccInfo: {
+            const struct IAPFIDTokenValuesAccInfoToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            switch(token->info_type) {
+            case IAPFIDTokenValuesAccInfoTypes_AccName:
+                break;
+            case IAPFIDTokenValuesAccInfoTypes_FirmwareVersion:
+                check_ret(token_span.size == 3, -IAPAckStatus_EBadParameter);
+                break;
+            case IAPFIDTokenValuesAccInfoTypes_HardwareVersion:
+                check_ret(token_span.size == 3, -IAPAckStatus_EBadParameter);
+                break;
+            case IAPFIDTokenValuesAccInfoTypes_Manufacture:
+                break;
+            case IAPFIDTokenValuesAccInfoTypes_ModelNumber:
+                break;
+            case IAPFIDTokenValuesAccInfoTypes_SerialNumber:
+                break;
+            case IAPFIDTokenValuesAccInfoTypes_MaxPayloadSize: {
+                uint16_t val;
+                check_ret(iap_span_read_16(&token_span, &val), -IAPAckStatus_EBadParameter);
+            } break;
+            case IAPFIDTokenValuesAccInfoTypes_AccStatus: {
+                uint32_t val;
+                check_ret(iap_span_read_32(&token_span, &val), -IAPAckStatus_EBadParameter);
+            } break;
+            case IAPFIDTokenValuesAccInfoTypes_RFCerts: {
+                uint32_t val;
+                check_ret(iap_span_read_32(&token_span, &val), -IAPAckStatus_EBadParameter);
+            } break;
+            }
+            pack_accepted(IAPFIDTokenValuesAccInfoAck);
+            ack->info_type = token->info_type;
+        } break;
+        case IAPFIDTokenTypes_IPodPreference: {
+            const struct IAPFIDTokenValuesIPodPreferenceToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesIPodPreferenceAck);
+            ack->class_id = token->class_id;
+        } break;
+        case IAPFIDTokenTypes_EAProtocol: {
+            const struct IAPFIDTokenValuesEAProtocolToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesEAProtocolAck);
+            ack->protocol_index = token->protocol_index;
+        } break;
+        case IAPFIDTokenTypes_BundleSeedIDPref: {
+            const struct IAPFIDTokenValuesBundleSeedIDPrefToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesBundleSeedIDPrefAck);
+        } break;
+        case IAPFIDTokenTypes_ScreenInfo: {
+            const struct IAPFIDTokenValuesScreenInfoToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesScreenInfoAck);
+        } break;
+        case IAPFIDTokenTypes_EAProtocolMetadata: {
+            const struct IAPFIDTokenValuesEAProtocolMetadataToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesEAProtocolMetadataAck);
+        } break;
+        case IAPFIDTokenTypes_AccDigitalAudioSampleRates: {
+            const struct IAPFIDTokenValuesAccDigitalAudioSampleRatesToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            while(token_span.size > 0) {
+                uint32_t rate;
+                check_ret(iap_span_read_32(&token_span, &rate), -IAPAckStatus_EBadParameter);
+            }
+            pack_accepted(IAPFIDTokenValuesAccDigitalAudioSampleRatesAck);
+        } break;
+        case IAPFIDTokenTypes_AccDigitalAudioVideoDelay: {
+            const struct IAPFIDTokenValuesAccDigitalAudioVideoDelayToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesAccDigitalAudioVideoDelayAck);
+        } break;
+        case IAPFIDTokenTypes_MicrophoneCaps: {
+            const struct IAPFIDTokenValuesMicrophoneCapsToken* token = iap_span_read(&token_span, sizeof(*token));
+            check_ret(token != NULL, -IAPAckStatus_EBadParameter);
+            pack_accepted(IAPFIDTokenValuesMicrophoneCapsAck);
+        } break;
+        default:
+            warn("unknown fid %04X", token_header->type << 8 | token_header->subtype);
+            return -IAPAckStatus_EBadParameter;
+        }
+    }
+    return 0;
+}
diff --git a/firmware/usbstack/iap/libiap/hid.c b/firmware/usbstack/iap/libiap/hid.c
new file mode 100644
index 0000000000..4be2ff389c
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/hid.c
@@ -0,0 +1,181 @@
+#include <string.h>
+
+#include "constants.h"
+#include "iap.h"
+#include "macros.h"
+#include "platform.h"
+#include "spec/hid.h"
+
+struct ReportSize {
+    uint8_t  id;
+    uint16_t size; /* including link control byte */
+};
+
+/* must match to in-driver hid report descriptor */
+
+static struct ReportSize input_report_size_table_fs[] = {
+    {.id = 0x01, .size = 0x0C},
+    {.id = 0x02, .size = 0x0E},
+    {.id = 0x03, .size = 0x14},
+    {.id = 0x04, .size = 0x3F},
+};
+
+static struct ReportSize output_report_size_table_fs[] = {
+    {.id = 0x05, .size = 0x08},
+    {.id = 0x06, .size = 0x0A},
+    {.id = 0x07, .size = 0x0E},
+    {.id = 0x08, .size = 0x14},
+    {.id = 0x09, .size = 0x3F},
+};
+
+static struct ReportSize input_report_size_table_hs[] = {
+    {.id = 0x01, .size = 0x0005},
+    {.id = 0x02, .size = 0x0009},
+    {.id = 0x03, .size = 0x000D},
+    {.id = 0x04, .size = 0x0011},
+    {.id = 0x05, .size = 0x0019},
+    {.id = 0x06, .size = 0x0031},
+    {.id = 0x07, .size = 0x005F},
+    {.id = 0x08, .size = 0x00C1},
+    {.id = 0x09, .size = 0x0101},
+    {.id = 0x0A, .size = 0x0181},
+    {.id = 0x0B, .size = 0x0201},
+    {.id = 0x0C, .size = 0x02FF},
+};
+
+static struct ReportSize output_report_size_table_hs[] = {
+    {.id = 0x0D, .size = 0x05},
+    {.id = 0x0E, .size = 0x09},
+    {.id = 0x1F, .size = 0x0D},
+    {.id = 0x10, .size = 0x11},
+    {.id = 0x11, .size = 0x19},
+    {.id = 0x12, .size = 0x31},
+    {.id = 0x13, .size = 0x5F},
+    {.id = 0x14, .size = 0xC1},
+    {.id = 0x15, .size = 0xFF},
+};
+
+static int find_output_report_size(struct IAPContext* ctx, uint8_t id) {
+    const struct ReportSize* ptr = ctx->opts.usb_highspeed ? output_report_size_table_hs : output_report_size_table_fs;
+    const size_t             len = ctx->opts.usb_highspeed ? array_size(output_report_size_table_hs) : array_size(output_report_size_table_fs);
+    for(size_t i = 0; i < len; i += 1) {
+        if(ptr[i].id == id) {
+            return ptr[i].size;
+        }
+    }
+    return -1;
+}
+
+IAPBool iap_feed_hid_report(struct IAPContext* ctx, const uint8_t* const data, const size_t size) {
+    check_ret(size > sizeof(struct IAPHIDReport), iap_false);
+    struct IAPHIDReport* report = (struct IAPHIDReport*)data;
+
+    int report_size;
+    if(ctx->opts.ignore_hid_report_id) {
+        report_size = size - 1;
+    } else {
+        report_size = find_output_report_size(ctx, report->report_id);
+        check_ret(report_size == (int)size - 1, iap_false, "%d != %d", report_size, (int)size - 1);
+    }
+
+    const uint8_t payload_size = report_size - 1;
+    check_act(ctx->hid_recv_buf_cursor + payload_size <= HID_BUFFER_SIZE, { ctx->hid_recv_buf_cursor = 0; return iap_false; }, "hid buffer overflow");
+    if(!(report->link_control & IAPHIDReportLinkControlBits_Continue)) {
+        /* not continue, first packet */
+        if(ctx->hid_recv_buf_cursor != 0) {
+            warn("not continue and cursor was set");
+            ctx->hid_recv_buf_cursor = 0;
+        }
+    } else {
+        check_ret(ctx->hid_recv_buf_cursor > 0, iap_false);
+    }
+    memcpy(ctx->hid_recv_buf + ctx->hid_recv_buf_cursor, report->data, payload_size);
+    ctx->hid_recv_buf_cursor += payload_size;
+    if(!(report->link_control & IAPHIDReportLinkControlBits_MoreToFollow)) {
+        /* no more to follow, last packet */
+        const IAPBool ret        = _iap_feed_packet(ctx, ctx->hid_recv_buf, ctx->hid_recv_buf_cursor);
+        ctx->hid_recv_buf_cursor = 0;
+        check_ret(ret, iap_false);
+    }
+    return iap_true;
+}
+
+IAPBool iap_notify_send_complete(struct IAPContext* ctx) {
+    ctx->send_busy = iap_false;
+    check_ret(_iap_send_next_report(ctx), iap_false);
+    return iap_true;
+}
+
+IAPBool _iap_send_hid_reports(struct IAPContext* ctx, size_t begin, size_t end) {
+    if(ctx->send_buf_sending_cursor < ctx->send_buf_sending_range_end) {
+        warn("another transmission in progress, aborting it");
+    }
+    ctx->send_buf_sending_cursor      = begin;
+    ctx->send_buf_sending_range_begin = begin;
+    ctx->send_buf_sending_range_end   = end;
+    if(!ctx->send_busy) {
+        check_ret(_iap_send_next_report(ctx), iap_false);
+    }
+    return iap_true;
+}
+
+static const struct ReportSize* find_optimal_report_size(struct IAPContext* ctx, size_t size) {
+    const struct ReportSize* ptr = ctx->opts.usb_highspeed ? input_report_size_table_hs : input_report_size_table_fs;
+    const size_t             len = ctx->opts.usb_highspeed ? array_size(input_report_size_table_hs) : array_size(input_report_size_table_fs);
+    for(size_t i = 0; i < len; i += 1) {
+        if(ptr[i].size >= size + 1 /* link control byte*/) {
+            return &ptr[i];
+        }
+    }
+    return &ptr[len - 1];
+}
+
+IAPBool _iap_send_next_report(struct IAPContext* ctx) {
+    if(ctx->send_buf_sending_cursor >= ctx->send_buf_sending_range_end) {
+        if(ctx->on_send_complete != NULL) {
+            IAPOnSendComplete cb  = ctx->on_send_complete;
+            ctx->on_send_complete = NULL;
+            check_ret(cb(ctx), iap_false);
+        } else if(ctx->active_events[0].callback != NULL) {
+            struct IAPActiveEvent event = ctx->active_events[0];
+            /* shift queue */
+            size_t i = 0;
+            for(; i < array_size(ctx->active_events) - 1 && ctx->active_events[i + 1].callback != NULL; i += 1) {
+                ctx->active_events[i] = ctx->active_events[i + 1];
+            }
+            ctx->active_events[i].callback = NULL;
+            /* process event */
+            check_ret(event.callback(ctx, &event), iap_false);
+        } else if(ctx->flushing_notifications) {
+            check_ret(_iap_flush_notification(ctx), iap_false);
+        }
+        return iap_true;
+    }
+
+    check_ret(!ctx->send_busy, iap_false);
+
+    const size_t                   send_buf_left = ctx->send_buf_sending_range_end - ctx->send_buf_sending_cursor;
+    const struct ReportSize* const report_size   = find_optimal_report_size(ctx, send_buf_left);
+    const size_t                   take_size     = min((size_t)report_size->size - 1 /* link control */, send_buf_left);
+
+    struct IAPHIDReport* const report = (struct IAPHIDReport*)ctx->hid_send_staging_buf;
+
+    report->report_id = report_size->id;
+
+    const IAPBool is_first = ctx->send_buf_sending_cursor == ctx->send_buf_sending_range_begin;
+    const IAPBool is_last  = ctx->send_buf_sending_cursor + take_size == ctx->send_buf_sending_range_end;
+    report->link_control =
+        (!is_first ? IAPHIDReportLinkControlBits_Continue : 0) |
+        (!is_last ? IAPHIDReportLinkControlBits_MoreToFollow : 0);
+
+    memcpy(report->data, ctx->send_buf + ctx->send_buf_sending_cursor, take_size);
+    memset(report->data + take_size, 0, report_size->size - 1 - take_size); /* clear rest */
+
+    ctx->send_buf_sending_cursor += take_size;
+    ctx->send_busy = iap_true;
+
+    const size_t send_size = 1 + report_size->size;
+    check_ret(iap_platform_send_hid_report(ctx, report, send_size) == (int)send_size, iap_false);
+
+    return iap_true;
+}
diff --git a/firmware/usbstack/iap/libiap/iap.c b/firmware/usbstack/iap/libiap/iap.c
new file mode 100644
index 0000000000..b5f700b0a2
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/iap.c
@@ -0,0 +1,1361 @@
+#include <inttypes.h>
+#include <string.h>
+
+#include "constants.h"
+#include "endian.h"
+#include "iap.h"
+#include "macros.h"
+#include "pack-util.h"
+#include "platform.h"
+#include "span.h"
+#include "spec/iap.h"
+#include "unaligned.h"
+
+enum TransIDSupport {
+    TransIDUnknown,
+    TransIDSupported,
+    TransIDNotSupported,
+};
+
+static struct IAPContextButtons parse_context_button_bits(uint8_t bits[4], struct IAPContextButtons* current) {
+    struct IAPContextButtons new;
+    struct IAPContextButtons released;
+
+#define process(field, Mask, bits)                                    \
+    new.field      = !!(bits & IAPContextButtonStatusButtons_##Mask); \
+    released.field = current->field && !new.field;
+
+    process(play_pause, PlayPause, bits[0]);
+    process(volume_up, VolumeUp, bits[0]);
+    process(volume_down, VolumeDown, bits[0]);
+    process(next_track, NextTrack, bits[0]);
+    process(prev_track, PrevTrack, bits[0]);
+    process(next_album, NextAlbum, bits[0]);
+    process(prev_album, PrevAlbum, bits[0]);
+    process(stop, Stop, bits[0]);
+    process(play, PlayResume, bits[1]);
+    process(pause, Pause, bits[1]);
+    process(mute_toggle, MuteToggle, bits[1]);
+    process(next_chapter, NextChapter, bits[1]);
+    process(prev_chapter, PrevChapter, bits[1]);
+    process(next_playlist, NextPlaylist, bits[1]);
+    process(prev_playlist, PrevPlaylist, bits[1]);
+    process(shuffle_advance, ShuffleSettingAdvance, bits[1]);
+    process(repeat_advance, RepeatSettingAdvance, bits[2]);
+
+#undef process
+
+    *current = new;
+    return released;
+}
+
+IAPBool iap_init_ctx(struct IAPContext* ctx, struct IAPOpts opts, void* platform) {
+    const uint16_t max_input_hid_desc_size = opts.usb_highspeed ? 0x02FF : 0x3F;
+
+    memset(ctx, 0, sizeof(*ctx));
+    ctx->opts     = opts;
+    ctx->platform = platform;
+
+    ctx->hid_recv_buf = iap_platform_malloc(ctx, HID_BUFFER_SIZE, 0);
+    check_ret(ctx->hid_recv_buf != NULL, iap_false);
+    ctx->send_buf = iap_platform_malloc(ctx, SEND_BUFFER_SIZE, 0);
+    check_ret(ctx->send_buf != NULL, iap_false);
+    ctx->handling_trans_id    = -1;
+    ctx->trans_id_support     = TransIDUnknown;
+    ctx->hid_send_staging_buf = iap_platform_malloc(ctx, max_input_hid_desc_size + 1 /* report id */, IAPPlatformMallocFlags_Uncached);
+    check_ret(ctx->hid_send_staging_buf != NULL, iap_false);
+    ctx->phase = IAPPhase_Connected;
+    return iap_true;
+}
+
+IAPBool iap_deinit_ctx(struct IAPContext* ctx) {
+    if(ctx->artwork.valid) {
+        iap_platform_close_artwork(ctx, &ctx->artwork);
+    }
+    iap_platform_free(ctx, ctx->hid_send_staging_buf);
+    iap_platform_free(ctx, ctx->hid_recv_buf);
+    iap_platform_free(ctx, ctx->send_buf);
+    return iap_true;
+}
+
+#define read_request(Type)                                                      \
+    const struct Type* request = iap_span_read(request_span, sizeof(*request)); \
+    check_ret(request != NULL, -IAPAckStatus_EBadParameter);
+
+#define alloc_response_extra(Type, extra)                                             \
+    struct Type* response = iap_span_alloc(response_span, sizeof(*response) + extra); \
+    check_ret(response != NULL, -IAPAckStatus_EOutOfResource);
+
+#define alloc_response(Type) alloc_response_extra(Type, 0)
+
+static uint32_t play_stage_change_notification_set_mask_to_type_mask(uint32_t mask) {
+    uint32_t ret = 0;
+    if(mask & IAPStatusChangeNotificationBits_Basic) {
+        ret |= 1 << IAPStatusChangeNotificationType_PlaybackStopped |
+               1 << IAPStatusChangeNotificationType_PlaybackFEWSeekStop |
+               1 << IAPStatusChangeNotificationType_PlaybackREWSeekStop;
+    }
+    if(mask & IAPStatusChangeNotificationBits_Extended) {
+        ret |= 1 << IAPStatusChangeNotificationType_PlaybackStatusExtended;
+    }
+    if(mask & IAPStatusChangeNotificationBits_TrackIndex) {
+        ret |= 1 << IAPStatusChangeNotificationType_TrackIndex;
+    }
+    if(mask & IAPStatusChangeNotificationBits_TrackTimeOffsetMSec) {
+        ret |= 1 << IAPStatusChangeNotificationType_TrackTimeOffsetMSec;
+    }
+    if(mask & IAPStatusChangeNotificationBits_TrackTimeOffsetSec) {
+        ret |= 1 << IAPStatusChangeNotificationType_TrackTimeOffsetSec;
+    }
+    if(mask & IAPStatusChangeNotificationBits_PlaybackEngineContentsChanged) {
+        ret |= 1 << IAPStatusChangeNotificationType_PlaybackEngineContentsChanged;
+    }
+    return ret;
+}
+
+static IAPBool send_artwork_chunk_cb(struct IAPContext* ctx) {
+    struct IAPSpan request = _iap_get_buffer_for_send_payload(ctx);
+    if(ctx->artwork_chunk_index == 0) {
+        struct IAPRetTrackArtworkDataFirstPayload* payload = iap_span_alloc(&request, sizeof(*payload));
+
+        payload->index                = swap_16(ctx->artwork_chunk_index);
+        payload->pixel_format         = ctx->artwork.color ? IAPArtworkPixelFormats_RGB565LE : IAPArtworkPixelFormats_Mono;
+        payload->pixel_width          = swap_16(ctx->artwork.width);
+        payload->pixel_height         = swap_16(ctx->artwork.height);
+        payload->inset_top_left_x     = 0;
+        payload->inset_top_left_y     = 0;
+        payload->inset_bottom_right_x = payload->pixel_width;
+        payload->inset_bottom_right_y = payload->pixel_height;
+        payload->stride               = swap_32(ctx->artwork.width * 2); /* TODO: support stride */
+    } else {
+        struct IAPRetTrackArtworkDataSubsequenctPayload* payload = iap_span_alloc(&request, sizeof(*payload));
+
+        payload->index = swap_16(ctx->artwork_chunk_index);
+    }
+    struct IAPSpan artwork;
+    size_t         copy_size = 0;
+    if(!ctx->opts.artwork_single_report || ctx->artwork_chunk_index != 0) {
+        check_ret(iap_platform_get_artwork_ptr(ctx, &ctx->artwork, &artwork), iap_false);
+        check_ret(iap_span_read(&artwork, ctx->artwork_cursor) != NULL, iap_false); /* skip already read chunk */
+        copy_size = min((ctx->opts.artwork_single_report ? 48 : request.size), artwork.size);
+        memcpy(iap_span_alloc(&request, copy_size), iap_span_read(&artwork, copy_size), copy_size);
+    }
+    check_ret(_iap_send_packet(ctx, ctx->artwork_data_lingo, ctx->artwork_data_command, ctx->artwork_trans_id, request.ptr), iap_false);
+    if(artwork.size > 0) {
+        /* more to send, ask to call again */
+        ctx->artwork_cursor += copy_size;
+        ctx->artwork_chunk_index += 1;
+        ctx->on_send_complete = send_artwork_chunk_cb;
+        print("track artwork left %zu bytes", artwork.size);
+    } else {
+        /* finished, free artwork */
+        check_ret(iap_platform_close_artwork(ctx, &ctx->artwork), iap_false);
+        ctx->artwork.valid = iap_false;
+        print("track artwork done");
+    }
+    return iap_true;
+}
+
+static int32_t start_artwork_data(struct IAPContext* ctx, struct IAPSpan* request_span, IAPBool ext) {
+    read_request(IAPGetTrackArtworkDataPayload);
+    check_ret(request->format_id == 0, -IAPAckStatus_EBadParameter);
+    check_ret(request->offset_ms == 0, -IAPAckStatus_EBadParameter);
+    check_ret(!ctx->artwork.valid, -IAPAckStatus_EBadParameter);
+
+    check_ret(iap_platform_open_artwork(ctx, swap_32(request->track_index), &ctx->artwork), -IAPAckStatus_EBadParameter);
+    ctx->artwork.valid       = iap_true;
+    ctx->artwork_cursor      = 0;
+    ctx->artwork_chunk_index = 0;
+    ctx->artwork_trans_id    = ctx->handling_trans_id;
+    if(ext) {
+        ctx->artwork_data_lingo   = IAPLingoID_ExtendedInterface;
+        ctx->artwork_data_command = IAPExtendedInterfaceCommandID_RetTrackArtworkData;
+    } else {
+        ctx->artwork_data_lingo   = IAPLingoID_DisplayRemote;
+        ctx->artwork_data_command = IAPDisplayRemoteCommandID_RetTrackArtworkData;
+    }
+    check_ret(send_artwork_chunk_cb(ctx), -IAPAckStatus_ECommandFailed);
+    return 0;
+}
+
+static int32_t ipod_ack(uint16_t command, enum IAPAckStatus status, struct IAPSpan* response_span, uint16_t ret) {
+    alloc_response(IAPIPodAckPayload);
+    response->status = status;
+    response->id     = command;
+    return ret;
+}
+
+static int32_t ipod_ack_ext(uint16_t command, enum IAPAckStatus status, struct IAPSpan* response_span) {
+    alloc_response(IAPExtendedIPodAckPayload);
+    response->id     = swap_16(command);
+    response->status = status;
+    return IAPExtendedInterfaceCommandID_IPodAck;
+}
+
+static int32_t handle_command(struct IAPContext* ctx, uint8_t lingo, uint16_t command, struct IAPSpan* request_span, struct IAPSpan* response_span) {
+    switch(lingo) {
+    case IAPLingoID_General:
+        switch(command) {
+        case IAPGeneralCommandID_RequestExtendedInterfaceMode: {
+            alloc_response(IAPReturnExtendedInterfaceModePayload);
+            response->is_ext_mode = 1;
+            return IAPGeneralCommandID_ReturnExtendedInterfaceMode;
+        } break;
+        case IAPGeneralCommandID_RequestIPodSoftwareVersion: {
+            alloc_response(IAPReturnIPodSoftwareVersionPayload);
+            response->major    = 18;
+            response->minor    = 7;
+            response->revision = 2;
+            return IAPGeneralCommandID_ReturnIPodSoftwareVersion;
+        } break;
+        case IAPGeneralCommandID_RequestIPodSerialNum: {
+            check_ret(iap_platform_get_ipod_serial_num(ctx, response_span), -IAPAckStatus_ECommandFailed);
+            return IAPGeneralCommandID_ReturnIPodSerialNum;
+        } break;
+        case IAPGeneralCommandID_RequestIPodModelNum: {
+            static const char* model_num = "MTAY2J/A";
+            check_ret(iap_span_append(response_span, model_num, strlen(model_num) + 1), -IAPAckStatus_EOutOfResource);
+            return IAPGeneralCommandID_ReturnIPodModelNum;
+        } break;
+        case IAPGeneralCommandID_RequestTransportMaxPayloadSize: {
+            alloc_response(IAPReturnTransportMaxPayloadSizePayload);
+            response->max_payload_size = swap_16(HID_BUFFER_SIZE - 1 /*sync*/ - 1 /*sof*/ - 3 /*length*/ - 1 /*checksum*/);
+            return IAPGeneralCommandID_ReturnTransportMaxPayloadSize;
+        } break;
+        case IAPGeneralCommandID_RequestLingoProtocolVersion: {
+            static const struct {
+                uint8_t major;
+                uint8_t minor;
+            } table[] = {
+                [IAPLingoID_General]            = {1, 9},
+                [IAPLingoID_Microphone]         = {1, 1},
+                [IAPLingoID_SimpleRemote]       = {1, 4},
+                [IAPLingoID_DisplayRemote]      = {1, 5},
+                [IAPLingoID_ExtendedInterface]  = {1, 14},
+                [IAPLingoID_AccessoryPower]     = {1, 1},
+                [IAPLingoID_USBHostMode]        = {1, 0},
+                [IAPLingoID_RFTuner]            = {1, 1},
+                [IAPLingoID_AccessoryEqualizer] = {1, 0},
+                [IAPLingoID_Sports]             = {1, 1},
+                [IAPLingoID_DigitalAudio]       = {1, 3},
+                [IAPLingoID_Storage]            = {1, 2},
+                [IAPLingoID_IPodOut]            = {1, 0},
+                [IAPLingoID_Location]           = {1, 0},
+            };
+
+            read_request(IAPRequestLingoProtocolVersionPayload);
+            check_ret(request->lingo < array_size(table), -IAPAckStatus_EBadParameter);
+
+            alloc_response(IAPReturnLingoProtocolVersionPayload);
+            response->lingo = request->lingo;
+            response->major = table[request->lingo].major;
+            response->minor = table[request->lingo].minor;
+            return IAPGeneralCommandID_ReturnLingoProtocolVersion;
+        } break;
+        case IAPGeneralCommandID_GetIPodOptions: {
+            alloc_response(IAPRetIPodOptionsPayload);
+            response->state = 0;
+            return IAPGeneralCommandID_RetIPodOptions;
+        } break;
+        case IAPGeneralCommandID_GetIPodPreferences: {
+            read_request(IAPGetIPodPreferencesPayload);
+            alloc_response(IAPRetIPodPreferencesPayload);
+            response->class_id   = request->class_id;
+            response->setting_id = 0; /* TODO: return actual value */
+            return IAPGeneralCommandID_RetIPodPreferences;
+        } break;
+        case IAPGeneralCommandID_SetIPodPreferences: {
+            read_request(IAPSetIPodPreferencesPayload);
+            /* TODO: handle preferences */
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+        } break;
+        case IAPGeneralCommandID_SetUIMode: {
+            read_request(IAPSetUIModePayload);
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+        } break;
+        case IAPGeneralCommandID_GetIPodOptionsForLingo: {
+            read_request(IAPGetIPodOptionsForLingoPayload);
+            alloc_response(IAPRetIPodOptionsForLingoPayload);
+            response->lingo_id = request->lingo_id;
+            switch(request->lingo_id) {
+            case IAPLingoID_SimpleRemote:
+                response->bits = swap_64(IAPRetIPodOptionsForLingoSimpleRemoteBits_ContextSpecificControls);
+                break;
+            case IAPLingoID_General:
+            case IAPLingoID_DisplayRemote:
+            case IAPLingoID_ExtendedInterface:
+            case IAPLingoID_DigitalAudio:
+            case IAPLingoID_Storage: /* TODO: this is not supported */
+                response->bits = 0;
+                break;
+            case IAPLingoID_USBHostMode:
+            case IAPLingoID_RFTuner:
+            case IAPLingoID_Sports:
+            case IAPLingoID_IPodOut:
+            case IAPLingoID_Location: { /* not supported */
+                return -IAPAckStatus_EBadParameter;
+            }
+            }
+            return IAPGeneralCommandID_RetIPodOptionsForLingo;
+        } break;
+        case IAPGeneralCommandID_GetSupportedEventNotification: {
+            alloc_response(IAPRetSupportedEventNotificationPayload);
+            response->mask = swap_64(IAPSetEventNotificationEvents_FlowControl);
+            return IAPGeneralCommandID_RetSupportedEventNotification;
+        } break;
+        case IAPGeneralCommandID_SetAvailableCurrent: {
+            read_request(IAPSetAvailableCurrentPayload);
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+        } break;
+        case IAPGeneralCommandID_SetEventNotification: {
+            read_request(IAPSetEventNotificationPayload);
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+        } break;
+        }
+        break;
+    case IAPLingoID_SimpleRemote:
+        switch(command) {
+        case IAPSimpleRemoteCommandID_ContextButtonStatus: {
+            response_span->ptr = NULL;
+
+            uint8_t bits[4] = {0};
+            for(int i = 0; i < 4 && request_span->size > 0; i += 1) {
+                iap_span_read_8(request_span, &bits[i]);
+            }
+            const struct IAPContextButtons         released = parse_context_button_bits(bits, &ctx->context_button_state);
+            const struct IAPPlatformPendingControl pending  = {
+                 .req_command = command,
+                 .ack_command = -1,
+                 .trans_id    = ctx->handling_trans_id,
+                 .lingo       = lingo,
+            };
+            const struct {
+                IAPBool                 released;
+                enum IAPPlatformControl control;
+            } table[] = {
+                {released.play_pause, IAPPlatformControl_TogglePlayPause},
+                {released.volume_up, IAPPlatformControl_VolumeUp},
+                {released.volume_down, IAPPlatformControl_VolumeDown},
+                {released.next_track, IAPPlatformControl_Next},
+                {released.prev_track, IAPPlatformControl_Prev},
+                {released.next_album, IAPPlatformControl_Next},
+                {released.prev_album, IAPPlatformControl_Prev},
+                {released.stop, IAPPlatformControl_Stop},
+                {released.play, IAPPlatformControl_Play},
+                {released.pause, IAPPlatformControl_Pause},
+                {released.mute_toggle, IAPPlatformControl_ToggleMute},
+                {released.next_chapter, IAPPlatformControl_Next},
+                {released.prev_chapter, IAPPlatformControl_Prev},
+                {released.next_playlist, IAPPlatformControl_Next},
+                {released.prev_playlist, IAPPlatformControl_Prev},
+            };
+            for(size_t i = 0; i < array_size(table); i += 1) {
+                if(table[i].released) {
+                    iap_platform_control(ctx, table[i].control, pending);
+                }
+            }
+            if(released.shuffle_advance) {
+                uint8_t current;
+                check_act(iap_platform_get_shuffle_setting(ctx, &current), return 0);
+                current = (current + 1) % IAPIPodStateShuffleSettingState_Albums;
+                check_act(iap_platform_set_shuffle_setting(ctx, current), return 0);
+            }
+            if(released.repeat_advance) {
+                uint8_t current;
+                check_act(iap_platform_get_repeat_setting(ctx, &current), return 0);
+                current = (current + 1) % IAPIPodStateRepeatSettingState_All;
+                check_act(iap_platform_set_repeat_setting(ctx, current), return 0);
+            }
+            return 0;
+        } break;
+        }
+        break;
+    case IAPLingoID_DisplayRemote:
+        switch(command) {
+        case IAPDisplayRemoteCommandID_SetCurrentEQProfileIndex: {
+            read_request(IAPSetCurrentEQProfileIndexPayload);
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPDisplayRemoteCommandID_IPodAck);
+        } break;
+        case IAPDisplayRemoteCommandID_SetRemoteEventNotification: {
+            read_request(IAPSetRemoteEventNotificationPayload);
+            ctx->notifications_3         = 0;
+            ctx->enabled_notifications_3 = swap_32(request->mask);
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPDisplayRemoteCommandID_IPodAck);
+        } break;
+        case IAPDisplayRemoteCommandID_GetIPodStateInfo: {
+            read_request(IAPGetIPodStateInfoPayload);
+            check_ret(response_span->size >= sizeof(struct IAPIPodStatePayload), -IAPAckStatus_EOutOfResource);
+            ((struct IAPIPodStatePayload*)response_span->ptr)->type = request->type;
+            switch(request->type) {
+            case IAPIPodStateType_TrackTimePositionMSec: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateTrackTimePositionMSecPayload);
+                response->position_ms = swap_32(status.track_pos_ms);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_TrackPlaybackIndex: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateTrackPlaybackIndexPayload);
+                response->index = swap_32(status.track_index);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_ChapterIndex: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateChapterIndexPayload);
+                response->index = swap_32(status.track_index);
+                /* no chapters */
+                response->chapter_count = 0;
+                response->chapter_index = -1;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_PlayStatus: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStatePlayStatusPayload);
+                response->status = status.state; /* TODO: convert enum */
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_Volume: {
+                struct IAPPlatformVolumeStatus status;
+                check_ret(iap_platform_get_volume(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateVolumePayload);
+                response->mute_state = status.muted;
+                response->ui_volume  = status.volume;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_Power: {
+                struct IAPPlatformPowerStatus status;
+                check_ret(iap_platform_get_power_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStatePowerPayload);
+                response->power_state   = status.state; /* TODO: convert enum */
+                response->battery_level = status.battery_level;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_EQSetting: {
+                alloc_response(IAPIPodStateEQSettingPayload);
+                /* no eq setting support yet */
+                response->eq_index = 0;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_ShuffleSetting: {
+                alloc_response(IAPIPodStateShuffleSettingPayload);
+                check_ret(iap_platform_get_shuffle_setting(ctx, &response->shuffle_state), -IAPAckStatus_ECommandFailed);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_RepeatSetting: {
+                alloc_response(IAPIPodStateRepeatSettingPayload);
+                check_ret(iap_platform_get_repeat_setting(ctx, &response->repeat_state), -IAPAckStatus_ECommandFailed);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_DateTimeSetting: {
+                struct IAPDateTime time;
+                check_ret(iap_platform_get_date_time(ctx, &time), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateDateTimeSettingPayload);
+                response->year   = swap_16(time.year);
+                response->month  = time.month;
+                response->day    = time.day;
+                response->hour   = time.hour;
+                response->minute = time.minute;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_AlarmSetting: {
+                alloc_response(IAPIPodStateAlarmSettingPayload);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_BacklightLevel: {
+                alloc_response(IAPIPodStateBacklightLevelPayload);
+                check_ret(iap_platform_get_backlight_level(ctx, &response->level), -IAPAckStatus_ECommandFailed);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_HoldSwitchState: {
+                IAPBool state;
+                check_ret(iap_platform_get_hold_switch_state(ctx, &state), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateHoldSwitchStatePayload);
+                response->state = state;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_SoundCheckState: {
+                alloc_response(IAPIPodStateSoundCheckStatePayload);
+                response->state = 0; /* no sound check */
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_AudiobookSpeeed: {
+                alloc_response(IAPIPodStateAudiobookSpeeedPayload);
+                response->speed = IAPIPodStateAudiobookSpeeed_Normal;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_TrackTimePositionSec: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateTrackTimePositionSecPayload);
+                response->position_s = swap_16(status.track_pos_ms / 1000);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_AbsoluteVolume: {
+                struct IAPPlatformVolumeStatus status;
+                check_ret(iap_platform_get_volume(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateAbsoluteVolumePayload);
+                response->mute_state      = status.muted;
+                response->ui_volume       = status.volume;
+                response->absolute_volume = status.volume;
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_TrackCaps: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                alloc_response(IAPIPodStateTrackCapsPayload);
+                response->caps = swap_32(status.track_caps);
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            case IAPIPodStateType_PlaybackEngineContents: {
+                alloc_response(IAPIPodStatePlaybackEngineContentsPayload);
+                response->count = 0; /* TODO: shoud be supported? */
+                return IAPDisplayRemoteCommandID_RetIPodStateInfo;
+            } break;
+            default:
+                warn("invalid request type 0x%02" PRIX8, request->type);
+                return -IAPAckStatus_EBadParameter;
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_GetPlayStatus: {
+            struct IAPPlatformPlayStatus status;
+            check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+            alloc_response(IAPRetPlayStatusPayload);
+            response->state          = status.state; /* TODO: convert enum */
+            response->track_index    = swap_32(status.track_index);
+            response->track_pos_ms   = swap_32(status.track_pos_ms);
+            response->track_total_ms = swap_32(status.track_total_ms);
+            return IAPDisplayRemoteCommandID_RetPlayStatus;
+        } break;
+        case IAPDisplayRemoteCommandID_GetIndexedPlayingTrackInfo: {
+            read_request(IAPGetIndexedPlayingTrackInfoPayload);
+            check_ret(response_span->size > sizeof(struct IAPRetIndexedPlayingTrackInfoPayload), -IAPAckStatus_EBadParameter);
+            ((struct IAPRetIndexedPlayingTrackInfoPayload*)response_span->ptr)->type = request->type;
+            switch(request->type) {
+            case IAPIndexedPlayingTrackInfoType_TrackCapsInfo: {
+                uint32_t                    length;
+                uint32_t                    caps;
+                struct IAPPlatformTrackInfo info = {.total_ms = &length, .caps = &caps};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                alloc_response(IAPRetIndexedPlayingTrackInfoTrackCapsInfoPayload);
+                response->track_caps     = swap_32(caps);
+                response->track_total_ms = swap_32(length);
+                response->chapter_count  = 0;
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ChapterTimeName: {
+                return -IAPAckStatus_EBadParameter;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ArtistName: {
+                alloc_response(IAPRetIndexedPlayingTrackInfoArtistNamePayload);
+                struct IAPPlatformTrackInfo info = {.artist = response_span};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_AlbumName: {
+                alloc_response(IAPRetIndexedPlayingTrackInfoAlbumNamePayload);
+                struct IAPPlatformTrackInfo info = {.album = response_span};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_GenreName: {
+                alloc_response_extra(IAPRetIndexedPlayingTrackInfoGenreNamePayload, 1);
+                response->name[0] = '\0';
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_TrackTitle: {
+                alloc_response(IAPRetIndexedPlayingTrackInfoTrackTitlePayload);
+                struct IAPPlatformTrackInfo info = {.title = response_span};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ComposerName: {
+                alloc_response(IAPRetIndexedPlayingTrackInfoComposerNamePayload);
+                struct IAPPlatformTrackInfo info = {.composer = response_span};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_Lyrics: {
+                alloc_response_extra(IAPRetIndexedPlayingTrackInfoLyricsPayload, 1);
+                response->info_bits = 0;
+                response->index     = 0;
+                response->lyrics[0] = '\0';
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            case IAPIndexedPlayingTrackInfoType_ArtworkCount: {
+                alloc_response_extra(IAPRetIndexedPlayingTrackInfoArtworkCountPayload, sizeof(struct IAPArtworkCount));
+                response->data[0].format = 0;
+                response->data[0].count  = swap_16(1);
+                return IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo;
+            } break;
+            }
+        } break;
+        case IAPDisplayRemoteCommandID_GetArtworkFormats: {
+            alloc_response_extra(IAPArtworkFormat, sizeof(struct IAPArtworkFormat));
+            response->format_id    = 0;
+            response->pixel_format = IAP_COLOR_ARTWORK ? IAPArtworkPixelFormats_RGB565LE : IAPArtworkPixelFormats_Mono;
+            response->image_width  = swap_16(IAP_ARTWORK_WIDTH);
+            response->image_height = swap_16(IAP_ARTWORK_HEIGHT);
+            return IAPDisplayRemoteCommandID_RetArtworkFormats;
+        } break;
+        case IAPDisplayRemoteCommandID_GetNumPlayingTracks: {
+            struct IAPPlatformPlayStatus status;
+            check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+            alloc_response(IAPRetNumPlayingTracksPayload);
+            response->num_playing_tracks = swap_32(status.track_count);
+            return IAPDisplayRemoteCommandID_RetNumPlayingTracks;
+        } break;
+        case IAPDisplayRemoteCommandID_GetTrackArtworkData: {
+            const int32_t ret = start_artwork_data(ctx, request_span, iap_false);
+            check_ret(ret == 0, ret);
+            /* responded in send_artwork_chunk_cb, no need to do it here */
+            response_span->ptr = NULL;
+            return 0;
+        } break;
+        case IAPDisplayRemoteCommandID_GetPowerBatteryState: {
+            struct IAPPlatformPowerStatus status;
+            check_ret(iap_platform_get_power_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+            alloc_response(IAPRetPowerBatteryStatePayload);
+            response->power_state   = status.state; /* TODO: convert enum */
+            response->battery_level = status.battery_level;
+            return IAPDisplayRemoteCommandID_RetPowerBatteryState;
+        } break;
+        case IAPDisplayRemoteCommandID_GetTrackArtworkTimes: {
+            read_request(IAPGetTrackArtworkTimesPayload);
+            const uint16_t count = swap_16(request->artwork_count);
+            check_ret(count == 0 || count == 1, -IAPAckStatus_ECommandFailed, "not implemented");
+
+            void* payload = iap_span_alloc(response_span, sizeof(uint32_t) * count);
+            check_ret(payload != NULL, iap_false);
+            memset(payload, 0, sizeof(uint32_t) * count);
+            return IAPDisplayRemoteCommandID_RetTrackArtworkTimes;
+        } break;
+        }
+        break;
+    case IAPLingoID_ExtendedInterface:
+        switch(command) {
+        case IAPExtendedInterfaceCommandID_GetCurrentPlayingTrackChapterInfo: {
+            alloc_response(IAPReturnCurrentPlayingTrackChapterInfoPayload);
+            /* no chapters */
+            response->count = 0;
+            response->index = -1;
+            return IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackChapterInfo;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetAudiobookSpeed: {
+            alloc_response(IAPRetAudiobookSpeedPayload);
+            response->speed = IAPIPodStateAudiobookSpeeed_Normal;
+            return IAPExtendedInterfaceCommandID_RetAudiobookSpeed;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackInfo: {
+            read_request(IAPExtendedGetIndexedPlayingTrackInfoPayload);
+            check_ret(response_span->size > sizeof(struct IAPExtendedRetIndexedPlayingTrackInfoPayload), -IAPAckStatus_EBadParameter);
+            ((struct IAPExtendedRetIndexedPlayingTrackInfoPayload*)response_span->ptr)->type = request->type;
+            switch(request->type) {
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackCapsInfo: {
+                uint32_t                    length;
+                uint32_t                    caps;
+                struct IAPPlatformTrackInfo info = {.total_ms = &length, .caps = &caps};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                alloc_response(IAPExtendedRetIndexedPlayingTrackInfoTrackCapsInfoPayload);
+                response->track_caps     = swap_32(caps);
+                response->track_total_ms = swap_32(length);
+                response->chapter_count  = 0;
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_PodcastName: {
+                alloc_response_extra(IAPExtendedRetIndexedPlayingTrackInfoPodcastNamePayload, 1);
+                response->name[0] = '\0';
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackReleaseDate: {
+                struct IAPDateTime          time;
+                struct IAPPlatformTrackInfo info = {.release_date = &time};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                alloc_response(IAPExtendedRetIndexedPlayingTrackInfoTrackReleaseDatePayload);
+                response->seconds = time.seconds;
+                response->minutes = time.minute;
+                response->hours   = time.hour;
+                response->day     = time.day;
+                response->month   = time.month;
+                response->year    = swap_16(time.year);
+                response->weekday = 0; /* TODO: set weekday? */
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackDescription: {
+                alloc_response_extra(IAPExtendedRetIndexedPlayingTrackInfoTrackDescriptionPayload, 1);
+                response->info_bits      = 0;
+                response->index          = 0;
+                response->description[0] = '\0';
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackSongLyrics: {
+                alloc_response_extra(IAPExtendedRetIndexedPlayingTrackInfoTrackSongLyricsPayload, 1);
+                response->info_bits = 0;
+                response->index     = 0;
+                response->lyrics[0] = '\0';
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackGenre: {
+                alloc_response_extra(IAPExtendedRetIndexedPlayingTrackInfoTrackGenrePayload, 1);
+                response->genre[0] = '\0';
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackComposer: {
+                alloc_response(IAPExtendedRetIndexedPlayingTrackInfoTrackComposerPayload);
+                struct IAPPlatformTrackInfo info = {.composer = response_span};
+                check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->track_index), &info), -IAPAckStatus_EBadParameter);
+                return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo;
+            } break;
+            case IAPExtendedIndexedPlayingTrackInfoType_TrackArtworkCount: {
+                // IAPArtworkPixelFormats_RGB565LE;
+                // struct IAPExtendedRetIndexedPlayingTrackInfoTrackArtworkCountPayload payload = {
+                //     .};
+                warn("artwork not implemented");
+                return -IAPAckStatus_ECommandFailed;
+            } break;
+            default:
+                warn("invalid request type 0x%02" PRIX8, request->type);
+                return -IAPAckStatus_EBadParameter;
+            }
+        } break;
+        case IAPExtendedInterfaceCommandID_GetArtworkFormats: {
+            /* same as DisplayRemote::GetArtworkFormats */
+            const int32_t ret = handle_command(ctx, IAPLingoID_DisplayRemote, IAPDisplayRemoteCommandID_GetArtworkFormats, request_span, response_span);
+            check_ret(ret == IAPDisplayRemoteCommandID_RetArtworkFormats, ret);
+            return IAPExtendedInterfaceCommandID_RetArtworkFormats;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetTrackArtworkData: {
+            const int32_t ret = start_artwork_data(ctx, request_span, iap_true);
+            check_ret(ret == 0, ret);
+            /* responded in send_artwork_chunk_cb, no need to do it here */
+            response_span->ptr = NULL;
+            return 0;
+        } break;
+        case IAPExtendedInterfaceCommandID_ResetDBSelection: {
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_GetNumberCategorizedDBRecords: {
+            read_request(IAPGetNumberCategorizedDBRecordsPayload);
+            uint32_t count;
+            switch(request->type) {
+            case IAPDatabaseType_Playlist: {
+                /* TODO: implement platform callback */
+                count = 99;
+            } break;
+            case IAPDatabaseType_Track: {
+                struct IAPPlatformPlayStatus status;
+                check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+                /* track_count is invalid while stopped.
+                 * return non-zero dummy value in this case, because reporting zero tracks
+                 * may cause empty library error. */
+                /* TODO: maybe add dedicated platform callback? */
+                count = status.state == IAPIPodStatePlayStatus_PlaybackStopped ? 99 : status.track_count;
+            } break;
+            default: {
+                warn("unsupported type 0x%02" PRIX8, request->type);
+                count = 0;
+            } break;
+            }
+
+            alloc_response(IAPReturnNumberCategorizedDBRecordsPayload);
+            response->count = swap_32(count);
+            return IAPExtendedInterfaceCommandID_ReturnNumberCategorizedDBRecords;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetPlayStatus: {
+            struct IAPPlatformPlayStatus status;
+            check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+            alloc_response(IAPExtendedRetPlayStatusPayload);
+            response->state          = status.state; /* TODO: convert enum */
+            response->track_pos_ms   = swap_32(status.track_pos_ms);
+            response->track_total_ms = swap_32(status.track_total_ms);
+            return IAPExtendedInterfaceCommandID_ReturnPlayStatus;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetCurrentPlayingTrackIndex: {
+            struct IAPPlatformPlayStatus status;
+            check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+            alloc_response(IAPReturnCurrentPlayingTrackIndexPayload);
+            response->index = swap_32(status.state == IAPIPodStatePlayStatus_PlaybackStopped ? (uint32_t)-1 : status.track_index);
+            return IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackIndex;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackTitle: {
+            read_request(IAPGetIndexedPlayingTrackStringPayload);
+            struct IAPPlatformTrackInfo info = {.title = response_span};
+            check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->index), &info), -IAPAckStatus_ECommandFailed);
+            return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackTitle;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackArtistName: {
+            read_request(IAPGetIndexedPlayingTrackStringPayload);
+            struct IAPPlatformTrackInfo info = {.artist = response_span};
+            check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->index), &info), -IAPAckStatus_ECommandFailed);
+            return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackArtistName;
+        } break;
+        case IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackAlbumName: {
+            read_request(IAPGetIndexedPlayingTrackStringPayload);
+            struct IAPPlatformTrackInfo info = {.album = response_span};
+            check_ret(iap_platform_get_indexed_track_info(ctx, swap_32(request->index), &info), -IAPAckStatus_ECommandFailed);
+            return IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackAlbumName;
+        } break;
+        case IAPExtendedInterfaceCommandID_SetPlayStatusChangeNotification: {
+            if(request_span->size == sizeof(struct IAPSetPlayStatusChangeNotification1BytePayload)) {
+                read_request(IAPSetPlayStatusChangeNotification1BytePayload);
+                ctx->notifications_4 = 0;
+                if(request->enable) {
+                    ctx->enabled_notifications_4 = 1 << IAPStatusChangeNotificationType_PlaybackStopped |
+                                                   1 << IAPStatusChangeNotificationType_TrackIndex |
+                                                   1 << IAPStatusChangeNotificationType_PlaybackFEWSeekStop |
+                                                   1 << IAPStatusChangeNotificationType_PlaybackREWSeekStop |
+                                                   1 << IAPStatusChangeNotificationType_TrackTimeOffsetMSec |
+                                                   1 << IAPStatusChangeNotificationType_ChapterIndex;
+                } else {
+                    ctx->enabled_notifications_4 = 0;
+                }
+            } else if(request_span->size == sizeof(struct IAPSetPlayStatusChangeNotification4BytesPayload)) {
+                read_request(IAPSetPlayStatusChangeNotification4BytesPayload);
+                ctx->enabled_notifications_4 = play_stage_change_notification_set_mask_to_type_mask(swap_32(request->mask));
+            }
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_PlayCurrentSelection: {
+            read_request(IAPPlayCurrentSelectionPayload);
+            const struct IAPPlatformPendingControl pending = {
+                .req_command = command,
+                .ack_command = IAPExtendedInterfaceCommandID_IPodAck,
+                .trans_id    = ctx->handling_trans_id,
+                .lingo       = lingo,
+            };
+            iap_platform_control(ctx, IAPPlatformControl_Play, pending);
+            response_span->ptr = NULL;
+            return 0;
+        } break;
+        case IAPExtendedInterfaceCommandID_PlayControl: {
+            read_request(IAPPlayControlPayload);
+            static const int enum_table[][2] = {
+                {IAPPlayControlCode_TogglePlayPause, IAPPlatformControl_TogglePlayPause},
+                {IAPPlayControlCode_Stop, IAPPlatformControl_Stop},
+                {IAPPlayControlCode_NextTrack, IAPPlatformControl_Next},
+                {IAPPlayControlCode_PrevTrack, IAPPlatformControl_Prev},
+                {IAPPlayControlCode_StartFF, -1},
+                {IAPPlayControlCode_StartRew, -1},
+                {IAPPlayControlCode_EndFFRew, -1},
+                {IAPPlayControlCode_Next, IAPPlatformControl_Next},
+                {IAPPlayControlCode_Prev, IAPPlatformControl_Prev},
+                {IAPPlayControlCode_Play, IAPPlatformControl_Play},
+                {IAPPlayControlCode_Pause, IAPPlatformControl_Pause},
+                {IAPPlayControlCode_NextChapter, IAPPlatformControl_Next},
+                {IAPPlayControlCode_PrevChapter, IAPPlatformControl_Prev},
+                {IAPPlayControlCode_ResumeIPod, -1},
+            };
+            int control = -1;
+            for(size_t i = 0; i < array_size(enum_table); i += 1) {
+                if(enum_table[i][0] == request->code) {
+                    control = enum_table[i][1];
+                    break;
+                }
+            }
+            if(control >= 0) {
+                const struct IAPPlatformPendingControl pending = {
+                    .req_command = command,
+                    .ack_command = IAPExtendedInterfaceCommandID_IPodAck,
+                    .trans_id    = ctx->handling_trans_id,
+                    .lingo       = lingo,
+                };
+                iap_platform_control(ctx, control, pending);
+                response_span->ptr = NULL;
+                return 0;
+            }
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_GetShuffle: {
+            alloc_response(IAPReturnShufflePayload);
+            check_ret(iap_platform_get_shuffle_setting(ctx, &response->mode), -IAPAckStatus_ECommandFailed);
+            return IAPExtendedInterfaceCommandID_ReturnShuffle;
+        } break;
+        case IAPExtendedInterfaceCommandID_SetShuffle: {
+            read_request(IAPSetShufflePayload);
+            check_ret(iap_platform_set_shuffle_setting(ctx, request->mode), -IAPAckStatus_ECommandFailed);
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_GetRepeat: {
+            alloc_response(IAPReturnRepeatPayload);
+            check_ret(iap_platform_get_repeat_setting(ctx, &response->mode), -IAPAckStatus_ECommandFailed);
+            return IAPExtendedInterfaceCommandID_ReturnRepeat;
+        } break;
+        case IAPExtendedInterfaceCommandID_SetRepeat: {
+            read_request(IAPSetRepeatPayload);
+            check_ret(iap_platform_set_repeat_setting(ctx, request->mode), -IAPAckStatus_ECommandFailed);
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_SetDisplayImage: {
+            /* TODO: pass downloaded image to user */
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_GetNumPlayingTracks: {
+            struct IAPPlatformPlayStatus status;
+            check_ret(iap_platform_get_play_status(ctx, &status), -IAPAckStatus_ECommandFailed);
+            alloc_response(IAPRetNumPlayingTracksPayload);
+            response->num_playing_tracks = swap_32(status.track_count);
+            return IAPExtendedInterfaceCommandID_ReturnNumPlayingTracks;
+        } break;
+        case IAPExtendedInterfaceCommandID_SetCurrentPlayingTrack: {
+            read_request(IAPSetCurrentPlayingTrackPayload);
+            check_ret(iap_platform_set_playing_track(ctx, swap_32(request->index)), -IAPAckStatus_ECommandFailed);
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_SelectSortDBRecord: {
+            /* ignored */
+            return ipod_ack_ext(command, IAPAckStatus_Success, response_span);
+        } break;
+        case IAPExtendedInterfaceCommandID_GetColorDisplayImageLimits: {
+            alloc_response(IAPColorDisplayImageLimit);
+            response->max_width    = 0;
+            response->max_height   = 0;
+            response->pixel_format = IAPArtworkPixelFormats_RGB565LE;
+            return IAPExtendedInterfaceCommandID_ReturnColorDisplayImageLimits;
+        } break;
+        }
+        break;
+    case IAPLingoID_DigitalAudio:
+        switch(command) {
+        case IAPDigitalAudioCommandID_AccessoryAck: {
+            read_request(IAPAccAckPayload);
+            response_span->ptr = NULL;
+            if(request->id == IAPDigitalAudioCommandID_TrackNewAudioAttributes) {
+                check_ret(ctx->waiting_for_audio_attrs_ack, 0, "unexpected ack");
+                ctx->waiting_for_audio_attrs_ack = iap_false;
+            }
+            check_ret(request->status == IAPAckStatus_Success, 0);
+            return 0;
+        } break;
+        case IAPDigitalAudioCommandID_RetAccessorySampleRateCaps: {
+            check_ret(iap_platform_on_acc_samprs_received(ctx, request_span), -IAPAckStatus_ECommandFailed);
+            response_span->ptr = NULL; /* no response */
+            return 0;
+        } break;
+        }
+        break;
+    }
+
+    return -IAPAckStatus_EUnknownID;
+}
+
+static IAPBool transition_idps_to_auth_cb(struct IAPContext* ctx) {
+    print("starting accessory authentication");
+    ctx->phase = IAPPhase_Auth;
+    check_ret(_iap_send_packet(ctx, IAPLingoID_General, IAPGeneralCommandID_GetAccessoryAuthenticationInfo, _iap_next_trans_id(ctx), _iap_get_buffer_for_send_payload(ctx).ptr), iap_false);
+    return iap_true;
+}
+
+static int32_t handle_in_connected(struct IAPContext* ctx, uint8_t lingo, uint16_t command, struct IAPSpan* request_span, struct IAPSpan* response_span) {
+    switch(lingo) {
+    case IAPLingoID_General:
+        switch(command) {
+        case IAPGeneralCommandID_IdentifyDeviceLingoes: {
+            read_request(IAPIdentifyDeviceLingoesPayload);
+            switch(swap_32(request->options)) {
+            case IAPIdentifyDeviceLingoesOptions_NoAuth:
+                break;
+            case IAPIdentifyDeviceLingoesOptions_DeferAuth:
+                warn("unsupported option 0x%04" PRIX32, swap_32(request->options));
+                return -IAPAckStatus_EBadParameter;
+            case IAPIdentifyDeviceLingoesOptions_ImmediateAuth:
+                ctx->on_send_complete = transition_idps_to_auth_cb;
+                break;
+            }
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+        } break;
+        case IAPGeneralCommandID_StartIDPS: {
+            ctx->phase = IAPPhase_IDPS;
+            return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+        } break;
+        }
+        break;
+    }
+    return -IAPAckStatus_EUnknownID;
+}
+
+static int32_t handle_in_idps(struct IAPContext* ctx, uint8_t lingo, uint16_t command, struct IAPSpan* request_span, struct IAPSpan* response_span) {
+    switch(lingo) {
+    case IAPLingoID_General:
+        switch(command) {
+        case IAPGeneralCommandID_SetFIDTokenValues: {
+            const int ret = _iap_hanlde_set_fid_token_values(request_span, response_span);
+            check_ret(ret == 0, ret);
+            return IAPGeneralCommandID_AckFIDTokenValues;
+        } break;
+        case IAPGeneralCommandID_EndIDPS: {
+            read_request(IAPEndIDPSPayload);
+            check_ret(request->status == IAPEndIDPSStatus_Success, -IAPAckStatus_ECommandFailed);
+            ctx->on_send_complete = transition_idps_to_auth_cb;
+            alloc_response(IAPIDPSStatusPayload);
+            response->status = IAPIDPSStatus_Success;
+            return IAPGeneralCommandID_IDPSStatus;
+        } break;
+        }
+        break;
+    }
+    return iap_false;
+}
+
+static IAPBool send_auth_challenge_sig_cb(struct IAPContext* ctx) {
+    check_ret(ctx->phase == IAPPhase_Auth, iap_false);
+    struct IAPSpan                     request_span = _iap_get_buffer_for_send_payload(ctx);
+    struct IAPGetAccAuthSigPayload2p0* request      = iap_span_alloc(&request_span, sizeof(*request));
+    check_ret(request != NULL, iap_false);
+    request->retry = 1;
+    check_ret(_iap_send_packet(ctx, IAPLingoID_General, IAPGeneralCommandID_GetAccessoryAuthenticationSignature, _iap_next_trans_id(ctx), request_span.ptr), iap_false);
+    return iap_true;
+}
+
+static IAPBool send_sample_rate_caps_cb(struct IAPContext* ctx) {
+    struct IAPSpan request = _iap_get_buffer_for_send_payload(ctx);
+    check_ret(_iap_send_packet(ctx, IAPLingoID_DigitalAudio, IAPDigitalAudioCommandID_GetAccessorySampleRateCaps, _iap_next_trans_id(ctx), request.ptr), iap_false);
+    return iap_true;
+}
+
+static int32_t handle_in_auth(struct IAPContext* ctx, uint8_t lingo, uint16_t command, struct IAPSpan* request_span, struct IAPSpan* response_span) {
+    switch(lingo) {
+    case IAPLingoID_General:
+        switch(command) {
+        case IAPGeneralCommandID_RetAccessoryAuthenticationInfo: {
+            read_request(IAPRetAccAuthInfoPayload2p0);
+            print("accessory cert %" PRIu8 "/%" PRIu8, request->cert_current_section_index, request->cert_max_section_index);
+            /* iap_platform_dump_hex(request->ptr, request->size); */
+            if(request->cert_current_section_index < request->cert_max_section_index) {
+                return ipod_ack(command, IAPAckStatus_Success, response_span, IAPGeneralCommandID_IPodAck);
+            } else {
+                ctx->on_send_complete = send_auth_challenge_sig_cb;
+                alloc_response(IAPAckAccAuthInfoPayload);
+                response->status = IAPAckAccAuthInfoStatus_Supported;
+                return IAPGeneralCommandID_AckAccessoryAuthenticationInfo;
+            }
+        } break;
+        case IAPGeneralCommandID_RetAccessoryAuthenticationSignature: {
+            print("accessory signature");
+            /* iap_platform_dump_hex(request->ptr, request->size); */
+
+            alloc_response(IAPAckAccAuthSigPayload);
+            response->status = IAPAckStatus_Success;
+
+            ctx->phase            = IAPPhase_Authed;
+            ctx->on_send_complete = send_sample_rate_caps_cb;
+
+            return IAPGeneralCommandID_AckAccessoryAuthenticationStatus;
+        } break;
+        }
+        break;
+    }
+    return -IAPAckStatus_EUnknownID;
+}
+
+static int32_t build_ipod_ack_response(uint8_t lingo, uint16_t command, uint8_t status, struct IAPSpan* response_span) {
+    static int32_t ack_commmand_ids[] = {
+        IAPGeneralCommandID_IPodAck,
+        -1,
+        IAPSimpleRemoteCommandID_IPodAck,
+        IAPDisplayRemoteCommandID_IPodAck,
+        IAPExtendedInterfaceCommandID_IPodAck,
+        -1,
+        IAPUSBHostModeCommandID_IPodAck,
+        -1,
+        -1,
+        IAPSportsCommandID_IPodAck,
+        IAPDigitalAudioCommandID_IPodAck,
+        -1,
+        IAPStorageCommandID_IPodAck,
+        IAPIPodOutCommandID_IPodAck,
+        IAPLocationCommandID_IPodAck,
+    };
+    check_ret(lingo < array_size(ack_commmand_ids) && ack_commmand_ids[lingo] >= 0, -1);
+    switch(lingo) {
+    case IAPLingoID_General:
+    case IAPLingoID_SimpleRemote:
+    case IAPLingoID_DisplayRemote:
+    case IAPLingoID_USBHostMode:
+    case IAPLingoID_Sports:
+    case IAPLingoID_DigitalAudio:
+    case IAPLingoID_IPodOut:
+    case IAPLingoID_Location: {
+        alloc_response(IAPIPodAckPayload);
+        response->id     = command;
+        response->status = status;
+    } break;
+    case IAPLingoID_ExtendedInterface: {
+        alloc_response(IAPExtendedIPodAckPayload);
+        response->id     = swap_16(command);
+        response->status = status;
+    } break;
+    case IAPLingoID_Storage: {
+        alloc_response(IAPStorageIPodAckPayload);
+        response->id     = command;
+        response->status = status;
+        response->handle = -1; /* TODO: set proper handle */
+    } break;
+    }
+    return ack_commmand_ids[lingo];
+}
+
+IAPBool _iap_feed_packet(struct IAPContext* ctx, const uint8_t* const data, const size_t size) {
+    union {
+        uint8_t  u8;
+        uint16_t u16;
+    } buf;
+    struct IAPSpan span = {(uint8_t*)data, size};
+
+    /* read sof byte */
+    check_ret(iap_span_read_8(&span, &buf.u8), iap_false);
+    if(buf.u8 == IAP_SYNC_BYTE) {
+        /* skip sync byte */
+        check_ret(iap_span_read_8(&span, &buf.u8), iap_false);
+    }
+    check_ret(buf.u8 == IAP_SOF_BYTE, iap_false, "%x != %x", buf.u8, IAP_SOF_BYTE);
+    const uint8_t* const checksum_range_begin = span.ptr;
+    /* read size */
+    check_ret(iap_span_read_8(&span, &buf.u8), iap_false);
+    size_t length;
+    if(buf.u8 == 0) {
+        /* long packet */
+        check_ret(iap_span_read_16(&span, &buf.u16), iap_false);
+        length = buf.u16;
+    } else {
+        length = buf.u8;
+    }
+    /* we have length, strip span so that it contains lingo,command,payload */
+    check_ret(span.size >= length + 1 /* checksum */, iap_false);
+    span.size = length;
+    /* verify checksum */
+    const uint8_t* const checksum_range_end = span.ptr + span.size + 1 /* checksum */;
+    uint8_t              checksum           = 0;
+    for(const uint8_t* ptr = checksum_range_begin; ptr < checksum_range_end; ptr += 1) {
+        checksum += *ptr;
+    }
+    check_ret(checksum == 0, iap_false);
+    /* read lingo id */
+    check_ret(iap_span_read_8(&span, &buf.u8), iap_false);
+    uint8_t lingo = buf.u8;
+    /* read command id */
+    uint16_t command;
+    if(lingo == IAPLingoID_ExtendedInterface) {
+        check_ret(iap_span_read_16(&span, &buf.u16), iap_false);
+        command = buf.u16;
+    } else {
+        check_ret(iap_span_read_8(&span, &buf.u8), iap_false);
+        command = buf.u8;
+    }
+
+    /* now span contains only payload */
+
+    /* request handling */
+    if(ctx->trans_id_support == TransIDUnknown) {
+        check_ret(lingo == IAPLingoID_General, iap_false);
+        if(command == IAPGeneralCommandID_StartIDPS) {
+            ctx->trans_id_support = TransIDSupported;
+        } else if(command == IAPGeneralCommandID_IdentifyDeviceLingoes) {
+            ctx->trans_id_support  = TransIDNotSupported;
+            ctx->handling_trans_id = -1;
+        } else {
+            warn("the first command(%02X:%04X) must be StartIDPS or IdentifyDeviceLingoes", lingo, command);
+            return iap_false;
+        }
+    }
+    if(ctx->trans_id_support == TransIDSupported) {
+        check_ret(iap_span_read_16(&span, &buf.u16), iap_false);
+        ctx->handling_trans_id = buf.u16;
+    }
+
+    if(ctx->opts.enable_packet_dump) {
+        IAP_LOGF("==== acc ====");
+        _iap_dump_packet(lingo, command, ctx->handling_trans_id, span);
+    }
+
+    struct IAPSpan response = _iap_get_buffer_for_send_payload(ctx);
+    int32_t        ret      = handle_command(ctx, lingo, command, &span, &response);
+    if(response.ptr == NULL) {
+        /* handler disabled response */
+        return iap_true;
+    }
+    if(ret >= 0) {
+        /* handled successfully */
+        goto respond;
+    }
+    if(ret != -IAPAckStatus_EUnknownID) {
+        /* handled, but error */
+        goto error_ack;
+    }
+
+    /* not a standard request, try authentication handlers */
+    ret      = -IAPAckStatus_EBadParameter;
+    response = _iap_get_buffer_for_send_payload(ctx);
+    switch(ctx->phase) {
+    case IAPPhase_Connected:
+        ret = handle_in_connected(ctx, lingo, command, &span, &response);
+        break;
+    case IAPPhase_IDPS:
+        ret = handle_in_idps(ctx, lingo, command, &span, &response);
+        break;
+    case IAPPhase_Auth:
+        ret = handle_in_auth(ctx, lingo, command, &span, &response);
+        break;
+    }
+    if(response.ptr == NULL) {
+        /* handler disabled response */
+        return iap_true;
+    }
+    if(ret >= 0) {
+        /* handled successfully */
+        goto respond;
+    }
+error_ack:
+    /* handling failed, replace response with ipod ack */
+    warn("command handling failed 0x%02X(%s):0x%04X", lingo, _iap_lingo_str(lingo), command);
+    response = _iap_get_buffer_for_send_payload(ctx);
+    ret      = build_ipod_ack_response(lingo, command, -ret, &response);
+    check_ret(ret >= 0, iap_false);
+respond:
+    check_ret(_iap_send_packet(ctx, lingo, ret, ctx->handling_trans_id, response.ptr), iap_false);
+    return iap_true;
+}
+
+struct IAPSpan _iap_get_buffer_for_send_payload(struct IAPContext* ctx) {
+    const size_t header_size = 1 /* sof */ +
+                               3 /* long format length */ +
+                               1 /* lingo */ +
+                               2 /* largest command id */ +
+                               2 /* trans id */;
+    const size_t footer_size = 1 /* checksum */;
+
+    struct IAPSpan buf = {ctx->send_buf + header_size, SEND_BUFFER_SIZE - header_size - footer_size};
+    return buf;
+}
+
+int32_t _iap_next_trans_id(struct IAPContext* ctx) {
+    if(ctx->trans_id_support == TransIDSupported) {
+        return ctx->trans_id += 1;
+    } else {
+        return -1;
+    }
+}
+
+IAPBool _iap_send_packet(struct IAPContext* ctx, uint8_t lingo, uint16_t command, int32_t trans_id, uint8_t* final_ptr) {
+    uint8_t* ptr          = _iap_get_buffer_for_send_payload(ctx).ptr;
+    size_t   payload_size = final_ptr - ptr;
+
+    if(ctx->opts.enable_packet_dump) {
+        IAP_LOGF("==== dev ====");
+        struct IAPSpan payload_span = {
+            .ptr  = _iap_get_buffer_for_send_payload(ctx).ptr,
+            .size = payload_size,
+        };
+        _iap_dump_packet(lingo, command, trans_id, payload_span);
+    }
+
+#define pack_8(val) \
+    ptr -= 1;       \
+    *(uint8_t*)ptr = val;
+#define pack_16(val) \
+    ptr -= 2;        \
+    *(uu16*)ptr = swap_16(val);
+
+    /* fill header in reverse order */
+    /* trans id */
+    if(trans_id >= 0) {
+        pack_16(trans_id);
+    }
+    /* command id */
+    if(lingo == IAPLingoID_ExtendedInterface) {
+        pack_16(command);
+    } else {
+        pack_8(command);
+    }
+    /* lingo */
+    pack_8(lingo);
+    /* length */
+    const uint16_t length = 1 /*lingo*/ + (lingo == IAPLingoID_ExtendedInterface ? 2 : 1) /*command*/ + (trans_id >= 0 ? 2 : 0) + payload_size;
+    if(length <= 0xFC) {
+        pack_8(length);
+    } else {
+        pack_16(length);
+        pack_8(0);
+    }
+    /* sof */
+    pack_8(IAP_SOF_BYTE);
+
+    /* set checksum */
+    uint8_t checksum = 0;
+    for(uint8_t* p = ptr + 1 /* exclude sof byte */; p < final_ptr; p += 1) {
+        checksum += *p;
+    }
+    checksum *= -1;
+    *final_ptr = checksum;
+
+    check_ret(_iap_send_hid_reports(ctx, ptr - ctx->send_buf, final_ptr - ctx->send_buf + 1 /* include checksum */), iap_false);
+    return iap_true;
+}
+
+static IAPBool push_active_event(struct IAPContext* ctx, struct IAPActiveEvent event) {
+    if(!ctx->send_busy) {
+        check_ret(event.callback(ctx, &event), iap_false);
+        return iap_true;
+    }
+
+    for(size_t i = 0; i < array_size(ctx->active_events); i += 1) {
+        if(ctx->active_events[i].callback == NULL) {
+            ctx->active_events[i] = event;
+            return iap_true;
+        }
+    }
+    return iap_false;
+}
+
+static IAPBool process_select_sampr(struct IAPContext* ctx, struct IAPActiveEvent* event) {
+    if(ctx->waiting_for_audio_attrs_ack) {
+        IAP_ERRORF("another sampr request pending");
+    }
+    ctx->waiting_for_audio_attrs_ack = iap_true;
+
+    struct IAPSpan                            request_span = _iap_get_buffer_for_send_payload(ctx);
+    struct IAPTrackNewAudioAttributesPayload* request      = iap_span_alloc(&request_span, sizeof(*request));
+    request->sample_rate                                   = swap_32(event->sampr);
+    request->sound_check                                   = 0;
+    request->volume_adjustment                             = 0;
+    check_ret(_iap_send_packet(ctx, IAPLingoID_DigitalAudio, IAPDigitalAudioCommandID_TrackNewAudioAttributes, _iap_next_trans_id(ctx), request_span.ptr), iap_false);
+    return iap_true;
+}
+
+IAPBool iap_select_sampr(struct IAPContext* ctx, uint32_t sampr) {
+    struct IAPActiveEvent event = {
+        .callback = process_select_sampr,
+        .sampr    = sampr,
+    };
+    check_ret(push_active_event(ctx, event), iap_false);
+    return iap_true;
+}
+
+static IAPBool process_control_response(struct IAPContext* ctx, struct IAPActiveEvent* event) {
+    struct IAPSpan                          request_span = _iap_get_buffer_for_send_payload(ctx);
+    const struct IAPPlatformPendingControl* ctrl         = &event->control_response.control;
+    const uint8_t                           status       = event->control_response.result ? IAPAckStatus_Success : IAPAckStatus_ECommandFailed;
+    if(ctrl->lingo == IAPLingoID_ExtendedInterface) {
+        ipod_ack_ext(ctrl->req_command, status, &request_span);
+    } else {
+        ipod_ack(ctrl->req_command, status, &request_span, ctrl->ack_command);
+    }
+    check_ret(_iap_send_packet(ctx, ctrl->lingo, ctrl->ack_command, ctrl->trans_id, request_span.ptr), iap_false);
+    return iap_true;
+}
+
+IAPBool iap_control_response(struct IAPContext* ctx, struct IAPPlatformPendingControl pending, IAPBool result) {
+    struct IAPActiveEvent event = {
+        .callback         = process_control_response,
+        .control_response = {pending, result},
+    };
+    if(pending.lingo == IAPLingoID_SimpleRemote && pending.req_command == IAPSimpleRemoteCommandID_ContextButtonStatus) {
+        /* no response */
+        return iap_true;
+    }
+    check_ret(push_active_event(ctx, event), iap_false);
+
+    return iap_true;
+}
diff --git a/firmware/usbstack/iap/libiap/iap.h b/firmware/usbstack/iap/libiap/iap.h
new file mode 100644
index 0000000000..86ad0412e1
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/iap.h
@@ -0,0 +1,55 @@
+#pragma once
+#include "context.h"
+#include "span.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* iap.c */
+IAPBool        iap_init_ctx(struct IAPContext* ctx, struct IAPOpts opts, void* platform);
+IAPBool        iap_deinit_ctx(struct IAPContext* ctx);
+IAPBool        _iap_feed_packet(struct IAPContext* ctx, const uint8_t* data, size_t size);
+struct IAPSpan _iap_get_buffer_for_send_payload(struct IAPContext* ctx);
+int32_t        _iap_next_trans_id(struct IAPContext* ctx);
+IAPBool        _iap_send_packet(struct IAPContext* ctx, uint8_t lingo, uint16_t command, int32_t trans_id, uint8_t* final_ptr);
+/* must be called after iap_platform_on_acc_samprs_received */
+IAPBool iap_select_sampr(struct IAPContext* ctx, uint32_t sampr);
+IAPBool iap_control_response(struct IAPContext* ctx, struct IAPPlatformPendingControl pending, IAPBool result);
+
+/* hid.c */
+IAPBool iap_feed_hid_report(struct IAPContext* ctx, const uint8_t* data, size_t size);
+IAPBool iap_notify_send_complete(struct IAPContext* ctx);
+IAPBool _iap_send_hid_reports(struct IAPContext* ctx, size_t begin, size_t end); /* data is passed by ctx->send_buf */
+IAPBool _iap_send_next_report(struct IAPContext* ctx);
+
+/* fid-token-values.c */
+int _iap_hanlde_set_fid_token_values(struct IAPSpan* request, struct IAPSpan* response);
+
+/* notification.c */
+void iap_notify_track_time_position(struct IAPContext* ctx, uint32_t pos_ms);
+void iap_notify_track_playback_index(struct IAPContext* ctx, uint32_t index);
+void iap_notify_track_caps(struct IAPContext* ctx, uint32_t caps);
+void iap_notify_tracks_count(struct IAPContext* ctx, uint32_t count);
+void iap_notify_play_status(struct IAPContext* ctx, uint8_t status /* IAPIPodStatePlayStatus */);
+void iap_notify_volume(struct IAPContext* ctx, uint8_t volume, IAPBool muted);
+void iap_notify_power_state(struct IAPContext* ctx, uint8_t state /* IAPIPodStatePowerState */, uint8_t battery_level);
+void iap_notify_shuffle_state(struct IAPContext* ctx, uint8_t state /* IAPIPodStateShuffleSettingState */);
+void iap_notify_repeat_state(struct IAPContext* ctx, uint8_t state /* IAPIPodStateRepeatSettingState */);
+void iap_notify_time_setting(struct IAPContext* ctx, const struct IAPDateTime* time);
+void iap_notify_hold_switch_state(struct IAPContext* ctx, uint8_t state);
+
+IAPBool iap_periodic_tick(struct IAPContext* ctx); /* call every 100ms */
+
+IAPBool _iap_flush_notification(struct IAPContext* ctx);
+
+/* debug.c */
+const char* _iap_lingo_str(uint8_t lingo);
+const char* _iap_command_str(uint8_t lingo, uint16_t command);
+IAPBool     _iap_span_is_str(const struct IAPSpan* span);
+const char* _iap_span_as_str(const struct IAPSpan* span);
+void        _iap_dump_packet(uint8_t lingo, uint16_t command, int32_t trans_id, struct IAPSpan span);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/firmware/usbstack/iap/libiap/macros.h b/firmware/usbstack/iap/libiap/macros.h
new file mode 100644
index 0000000000..f2cab9bb67
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/macros.h
@@ -0,0 +1,27 @@
+#pragma once
+#include "../platform-macros.h"
+
+#if !defined(IAP_LOGF)
+#define IAP_LOGF_MUTED
+#define IAP_LOGF(...)
+#endif
+#define print(fmt, ...) IAP_LOGF("%s:%d: " fmt, __func__, __LINE__ __VA_OPT__(, __VA_ARGS__));
+
+#if !defined(IAP_ERRORF)
+#define IAP_ERRORF_MUTED
+#define IAP_ERRORF(...)
+#endif
+#define warn(fmt, ...)  IAP_ERRORF("%s:%d: " fmt, __func__, __LINE__ __VA_OPT__(, __VA_ARGS__));
+
+#define check_act(cond, act, ...)                              \
+    if(!(cond)) {                                              \
+        warn("assertion failed" __VA_OPT__(": " __VA_ARGS__)); \
+        act;                                                   \
+    }
+
+#define check_ret(cond, ret, ...) check_act(cond, return ret, __VA_ARGS__)
+
+#define array_size(arr) (sizeof(arr) / sizeof(arr[0]))
+
+#define max(a, b) (a > b ? a : b)
+#define min(a, b) (a < b ? a : b)
diff --git a/firmware/usbstack/iap/libiap/notification.c b/firmware/usbstack/iap/libiap/notification.c
new file mode 100644
index 0000000000..2d12f4ca35
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/notification.c
@@ -0,0 +1,200 @@
+#include "context.h"
+#include "endian.h"
+#include "iap.h"
+#include "macros.h"
+#include "spec/iap.h"
+
+void iap_notify_track_time_position(struct IAPContext* ctx, uint32_t pos_ms) {
+    ctx->notification_data.track_time_position_ms = pos_ms;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_TrackTimePositionMSec;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_TrackTimePositionSec;
+    ctx->notifications_4 |= 1 << IAPStatusChangeNotificationType_TrackTimeOffsetMSec;
+    ctx->notifications_4 |= 1 << IAPStatusChangeNotificationType_TrackTimeOffsetSec;
+}
+
+void iap_notify_track_playback_index(struct IAPContext* ctx, uint32_t index) {
+    ctx->notification_data.track_playback_index = index;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_TrackPlaybackIndex;
+    ctx->notifications_4 |= 1 << IAPStatusChangeNotificationType_TrackIndex;
+}
+
+void iap_notify_track_caps(struct IAPContext* ctx, uint32_t caps) {
+    ctx->notification_data.track_caps = caps;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_TrackCaps;
+}
+
+void iap_notify_tracks_count(struct IAPContext* ctx, uint32_t count) {
+    ctx->notification_data.tracks_count = count;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_PlaybackEngineContents;
+    ctx->notifications_4 |= 1 << IAPStatusChangeNotificationType_PlaybackEngineContentsChanged;
+}
+
+void iap_notify_play_status(struct IAPContext* ctx, uint8_t status) {
+    ctx->notification_data.play_status = status;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_PlayStatus;
+    ctx->notifications_4 |= 1 << IAPStatusChangeNotificationType_PlaybackStatusExtended;
+    if(status == IAPIPodStatePlayStatus_PlaybackStopped) {
+        ctx->notifications_4 |= 1 << IAPStatusChangeNotificationType_PlaybackStopped;
+    }
+    /* should set Playback{FEW,REW}SeekStop, but no way to find which from EndFastForwardRewind */
+}
+
+void iap_notify_volume(struct IAPContext* ctx, uint8_t volume, IAPBool muted) {
+    ctx->notification_data.volume     = volume;
+    ctx->notification_data.mute_state = muted;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_Volume;
+}
+
+void iap_notify_power_state(struct IAPContext* ctx, uint8_t state, uint8_t battery_level) {
+    ctx->notification_data.power_state   = state;
+    ctx->notification_data.battery_level = battery_level;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_Power;
+}
+
+void iap_notify_shuffle_state(struct IAPContext* ctx, uint8_t state) {
+    ctx->notification_data.shuffle_state = state;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_ShuffleSetting;
+}
+
+void iap_notify_repeat_state(struct IAPContext* ctx, uint8_t state) {
+    ctx->notification_data.repeat_state = state;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_RepeatSetting;
+}
+
+void iap_notify_time_setting(struct IAPContext* ctx, const struct IAPDateTime* time) {
+    ctx->notification_data.time_setting = *time;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_DateTimeSetting;
+}
+
+void iap_notify_hold_switch_state(struct IAPContext* ctx, uint8_t state) {
+    ctx->notification_data.hold_switch_state = state;
+    ctx->notifications_3 |= 1 << IAPIPodStateType_HoldSwitchState;
+}
+
+IAPBool iap_periodic_tick(struct IAPContext* ctx) {
+    if(ctx->waiting_for_audio_attrs_ack) {
+        return iap_true;
+    }
+
+    ctx->flushing_notifications = iap_true;
+    ctx->notification_tick += 1;
+    if(!ctx->send_busy) {
+        check_ret(_iap_flush_notification(ctx), iap_false);
+    }
+    return iap_true;
+}
+
+static uint8_t play_status_to_extended(uint8_t status) {
+    switch(status) {
+    case IAPIPodStatePlayStatus_PlaybackStopped:
+        return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Stopped;
+    case IAPIPodStatePlayStatus_Playing:
+        return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Playing;
+    case IAPIPodStatePlayStatus_PlaybackPaused:
+        return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Paused;
+    case IAPIPodStatePlayStatus_FastForward:
+        return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_FFWSeekStarted;
+    case IAPIPodStatePlayStatus_FastRewind:
+        return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_REWSeekStarted;
+    case IAPIPodStatePlayStatus_EndFastForwardRewind:
+        return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_FFWREWSeekStopped;
+    }
+    /* unreachable */
+    return IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Stopped;
+}
+
+#define send_notify_3(PayloadType, StateType, set)                                                                                                                      \
+    if(ctx->enabled_notifications_3 & ctx->notifications_3 & (1 << StateType)) {                                                                                        \
+        struct PayloadType* payload = iap_span_alloc(&request, sizeof(*payload));                                                                                       \
+        check_ret(payload != NULL, iap_false);                                                                                                                          \
+        payload->type = StateType;                                                                                                                                      \
+        set;                                                                                                                                                            \
+        check_ret(_iap_send_packet(ctx, IAPLingoID_DisplayRemote, IAPDisplayRemoteCommandID_RemoteEventNotification, _iap_next_trans_id(ctx), request.ptr), iap_false); \
+        ctx->notifications_3 &= ~(1 << StateType);                                                                                                                      \
+        return iap_true;                                                                                                                                                \
+    }
+
+#define send_notify_4(PayloadType, StateType, set)                                                                                                                                   \
+    if(ctx->enabled_notifications_4 & ctx->notifications_4 & (1 << StateType)) {                                                                                                     \
+        print("notification " #StateType);                                                                                                                                           \
+        struct PayloadType* payload = iap_span_alloc(&request, sizeof(*payload));                                                                                                    \
+        check_ret(payload != NULL, iap_false);                                                                                                                                       \
+        payload->type = StateType;                                                                                                                                                   \
+        set;                                                                                                                                                                         \
+        check_ret(_iap_send_packet(ctx, IAPLingoID_ExtendedInterface, IAPExtendedInterfaceCommandID_PlayStatusChangeNotification, _iap_next_trans_id(ctx), request.ptr), iap_false); \
+        ctx->notifications_4 &= ~(1 << StateType);                                                                                                                                   \
+        return iap_true;                                                                                                                                                             \
+    }
+
+IAPBool _iap_flush_notification(struct IAPContext* ctx) {
+    struct IAPSpan request = _iap_get_buffer_for_send_payload(ctx);
+
+    /* [1] P.257:
+     *  Notifications for enabled events are sent every 500 ms,
+     *  with the exception of volume change notifications, which are sent every 100 ms.
+     */
+    if(ctx->notification_tick % 5 != 0) {
+        goto freq_events;
+    }
+
+    send_notify_3(IAPIPodStateTrackTimePositionMSecPayload,
+                  IAPIPodStateType_TrackTimePositionMSec,
+                  payload->position_ms = swap_32(ctx->notification_data.track_time_position_ms));
+    send_notify_3(IAPIPodStateTrackPlaybackIndexPayload,
+                  IAPIPodStateType_TrackPlaybackIndex,
+                  payload->index = swap_32(ctx->notification_data.track_playback_index));
+    send_notify_3(IAPIPodStateTrackCapsPayload,
+                  IAPIPodStateType_TrackCaps,
+                  payload->caps = swap_32(ctx->notification_data.track_caps));
+    send_notify_3(IAPIPodStateTrackTimePositionSecPayload,
+                  IAPIPodStateType_TrackTimePositionSec,
+                  payload->position_s = swap_16(ctx->notification_data.track_time_position_ms / 1000));
+    send_notify_3(IAPIPodStatePlaybackEngineContentsPayload,
+                  IAPIPodStateType_PlaybackEngineContents,
+                  payload->count = swap_32(ctx->notification_data.tracks_count));
+    send_notify_3(IAPIPodStatePlayStatusPayload,
+                  IAPIPodStateType_PlayStatus,
+                  payload->status = ctx->notification_data.play_status);
+    send_notify_3(IAPIPodStatePowerPayload,
+                  IAPIPodStateType_Power,
+                  payload->power_state   = ctx->notification_data.power_state;
+                  payload->battery_level = ctx->notification_data.battery_level);
+    send_notify_3(IAPIPodStateShuffleSettingPayload,
+                  IAPIPodStateType_ShuffleSetting,
+                  payload->shuffle_state = ctx->notification_data.shuffle_state);
+    send_notify_3(IAPIPodStateRepeatSettingPayload,
+                  IAPIPodStateType_RepeatSetting,
+                  payload->repeat_state = ctx->notification_data.repeat_state);
+    send_notify_3(IAPIPodStateDateTimeSettingPayload,
+                  IAPIPodStateType_DateTimeSetting,
+                  payload->year   = swap_16(ctx->notification_data.time_setting.year);
+                  payload->month  = ctx->notification_data.time_setting.month;
+                  payload->day    = ctx->notification_data.time_setting.day;
+                  payload->hour   = ctx->notification_data.time_setting.hour;
+                  payload->minute = ctx->notification_data.time_setting.minute);
+    send_notify_4(IAPPlayStatusChangeNotificationPlaybackStoppedPayload,
+                  IAPStatusChangeNotificationType_PlaybackStopped, );
+    send_notify_4(IAPPlayStatusChangeNotificationTrackIndexPayload,
+                  IAPStatusChangeNotificationType_TrackIndex,
+                  payload->index = swap_32(ctx->notification_data.track_playback_index));
+    send_notify_4(IAPPlayStatusChangeNotificationTrackTimeOffsetMSecPayload,
+                  IAPStatusChangeNotificationType_TrackTimeOffsetMSec,
+                  payload->offset_ms = swap_32(ctx->notification_data.track_time_position_ms));
+    send_notify_4(IAPPlayStatusChangeNotificationPlaybackStatusExtendedPayload,
+                  IAPStatusChangeNotificationType_PlaybackStatusExtended,
+                  payload->state = play_status_to_extended(ctx->notification_data.play_status));
+    send_notify_4(IAPPlayStatusChangeNotificationTrackTimeOffsetSecPayload,
+                  IAPStatusChangeNotificationType_TrackTimeOffsetSec,
+                  payload->offset_s = swap_32(ctx->notification_data.track_time_position_ms / 1000));
+    send_notify_4(IAPPlayStatusChangeNotificationPlaybackEngineContentsChangedPayload,
+                  IAPStatusChangeNotificationType_PlaybackEngineContentsChanged,
+                  payload->count = swap_32(ctx->notification_data.tracks_count));
+freq_events:
+    send_notify_3(IAPIPodStateVolumePayload,
+                  IAPIPodStateType_Volume,
+                  payload->mute_state = ctx->notification_data.mute_state;
+                  payload->ui_volume  = ctx->notification_data.volume);
+
+    ctx->flushing_notifications = iap_false;
+    return iap_true;
+}
diff --git a/firmware/usbstack/iap/libiap/notification.h b/firmware/usbstack/iap/libiap/notification.h
new file mode 100644
index 0000000000..977f380b09
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/notification.h
@@ -0,0 +1,20 @@
+#pragma once
+#include <stdint.h>
+
+#include "datetime.h"
+
+struct _IAPNotifyState {
+    uint32_t           track_time_position_ms;
+    uint32_t           track_playback_index;
+    uint32_t           track_caps; /* IAPIPodStateTrackCapBits */
+    uint32_t           tracks_count;
+    uint8_t            play_status; /* IAPIPodStatePlayStatus */
+    uint8_t            mute_state;
+    uint8_t            volume;
+    uint8_t            power_state; /* IAPIPodStatePowerState */
+    uint8_t            battery_level;
+    uint8_t            shuffle_state; /* IAPIPodStateShuffleSettingState */
+    uint8_t            repeat_state;  /* IAPIPodStateRepeatSettingState */
+    struct IAPDateTime time_setting;
+    uint8_t            hold_switch_state;
+};
diff --git a/firmware/usbstack/iap/libiap/pack-util.h b/firmware/usbstack/iap/libiap/pack-util.h
new file mode 100644
index 0000000000..bc5dae8fc8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/pack-util.h
@@ -0,0 +1,41 @@
+#include <stdint.h>
+#include <string.h>
+
+#include "bool.h"
+#include "macros.h"
+
+__attribute__((unused)) static IAPBool pack_u8(uint8_t** data, size_t* size, uint8_t value) {
+    check_ret(*size >= 1, iap_false);
+    (*data)[0] = value;
+    *data += 1;
+    *size -= 1;
+    return iap_true;
+}
+
+__attribute__((unused)) static IAPBool pack_u16(uint8_t** data, size_t* size, uint16_t value) {
+    check_ret(*size >= 2, iap_false);
+    (*data)[0] = value >> 8;
+    (*data)[1] = value;
+    *data += 2;
+    *size -= 2;
+    return iap_true;
+}
+
+__attribute__((unused)) static IAPBool pack_u32(uint8_t** data, size_t* size, uint32_t value) {
+    check_ret(*size >= 2, iap_false);
+    (*data)[0] = value >> 24;
+    (*data)[1] = value >> 16;
+    (*data)[2] = value >> 8;
+    (*data)[3] = value;
+    *data += 4;
+    *size -= 4;
+    return iap_true;
+}
+
+__attribute__((unused)) static IAPBool pack_data(uint8_t** data, size_t* size, const void* payload, size_t payload_size) {
+    check_ret(*size >= payload_size, iap_false);
+    memcpy(*data, payload, payload_size);
+    *data += payload_size;
+    *size -= payload_size;
+    return iap_true;
+}
diff --git a/firmware/usbstack/iap/libiap/platform.h b/firmware/usbstack/iap/libiap/platform.h
new file mode 100644
index 0000000000..880f6f4c58
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/platform.h
@@ -0,0 +1,116 @@
+#pragma once
+#include <stddef.h>
+#include <stdint.h>
+
+#include "datetime.h"
+#include "span.h"
+#include "spec/iap.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct IAPContext;
+
+/* iap_platform_malloc */
+enum IAPPlatformMallocFlags {
+    IAPPlatformMallocFlags_Uncached = 1 << 0,
+};
+
+/* iap_platform_get_play_status */
+struct IAPPlatformPlayStatus {
+    uint32_t track_total_ms;
+    uint32_t track_pos_ms;
+    uint32_t track_index;
+    uint32_t track_count;
+    uint32_t track_caps; /* IAPIPodStateTrackCapBits */
+    uint8_t  state;      /* IAPIPodStatePlayStatus */
+};
+
+/* iap_platform_control */
+enum IAPPlatformControl {
+    IAPPlatformControl_TogglePlayPause,
+    IAPPlatformControl_Play,
+    IAPPlatformControl_Pause,
+    IAPPlatformControl_Stop,
+    IAPPlatformControl_Next,
+    IAPPlatformControl_Prev,
+    IAPPlatformControl_VolumeUp,
+    IAPPlatformControl_VolumeDown,
+    IAPPlatformControl_ToggleMute,
+};
+
+struct IAPPlatformPendingControl {
+    uint16_t req_command;
+    uint16_t ack_command;
+    int32_t  trans_id;
+    uint8_t  lingo;
+};
+
+/* iap_platform_get_volume */
+struct IAPPlatformVolumeStatus {
+    uint8_t volume;
+    IAPBool muted;
+};
+
+/* iap_platform_get_power_status */
+struct IAPPlatformPowerStatus {
+    uint8_t state; /* IAPIPodStatePowerState */
+    uint8_t battery_level;
+};
+
+/* iap_platform_get_indexed_track_info */
+struct IAPPlatformTrackInfo {
+    uint32_t*           total_ms;
+    uint32_t*           caps; /* IAPIPodStateTrackCapBits */
+    struct IAPDateTime* release_date;
+    struct IAPSpan*     artist;
+    struct IAPSpan*     composer;
+    struct IAPSpan*     album;
+    struct IAPSpan*     title;
+};
+
+/* iap_platform_open_artwork */
+struct IAPPlatformArtwork {
+    uint16_t  width;  /* user */
+    uint16_t  height; /* user */
+    IAPBool   color;  /* user */
+    IAPBool   valid;  /* library */
+    uintptr_t opaque; /* user */
+};
+
+/* library routines */
+void* iap_platform_malloc(struct IAPContext* iap_ctx, size_t size, int flags);
+void  iap_platform_free(struct IAPContext* iap_ctx, void* ptr);
+int   iap_platform_send_hid_report(struct IAPContext* iap_ctx, const void* ptr, size_t size);
+
+/* system info */
+IAPBool iap_platform_get_ipod_serial_num(struct IAPContext* iap_ctx, struct IAPSpan* serial);
+
+/* audio controls */
+IAPBool iap_platform_get_play_status(struct IAPContext* iap_ctx, struct IAPPlatformPlayStatus* status);
+void    iap_platform_control(struct IAPContext* iap_ctx, enum IAPPlatformControl control, struct IAPPlatformPendingControl pending);
+IAPBool iap_platform_get_volume(struct IAPContext* iap_ctx, struct IAPPlatformVolumeStatus* status);
+IAPBool iap_platform_get_power_status(struct IAPContext* iap_ctx, struct IAPPlatformPowerStatus* status);
+IAPBool iap_platform_get_shuffle_setting(struct IAPContext* iap_ctx, uint8_t* status /* IAPIPodStateShuffleSettingState */);
+IAPBool iap_platform_set_shuffle_setting(struct IAPContext* iap_ctx, uint8_t status /* IAPIPodStateShuffleSettingState */);
+IAPBool iap_platform_get_repeat_setting(struct IAPContext* iap_ctx, uint8_t* status /* IAPIPodStateRepeatSettingState */);
+IAPBool iap_platform_set_repeat_setting(struct IAPContext* iap_ctx, uint8_t status /* IAPIPodStateRepeatSettingState */);
+IAPBool iap_platform_get_date_time(struct IAPContext* iap_ctx, struct IAPDateTime* time);
+IAPBool iap_platform_get_backlight_level(struct IAPContext* iap_ctx, uint8_t* level);
+IAPBool iap_platform_get_hold_switch_state(struct IAPContext* iap_ctx, IAPBool* state);
+IAPBool iap_platform_get_indexed_track_info(struct IAPContext* iap_ctx, uint32_t index, struct IAPPlatformTrackInfo* info);
+IAPBool iap_platform_set_playing_track(struct IAPContext* iap_ctx, uint32_t index);
+IAPBool iap_platform_open_artwork(struct IAPContext* iap_ctx, uint32_t index, struct IAPPlatformArtwork* artwork);
+IAPBool iap_platform_get_artwork_ptr(struct IAPContext* iap_ctx, struct IAPPlatformArtwork* artwork, struct IAPSpan* span);
+IAPBool iap_platform_close_artwork(struct IAPContext* iap_ctx, struct IAPPlatformArtwork* artwork);
+
+/* other callbacks */
+IAPBool iap_platform_on_acc_samprs_received(struct IAPContext* iap_ctx, struct IAPSpan* samprs);
+
+/* debugging */
+void iap_platform_dump_hex(const void* ptr, size_t size);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/firmware/usbstack/iap/libiap/span.c b/firmware/usbstack/iap/libiap/span.c
new file mode 100644
index 0000000000..98c16989ac
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/span.c
@@ -0,0 +1,46 @@
+#include <string.h>
+
+#include "endian.h"
+#include "macros.h"
+#include "span.h"
+#include "unaligned.h"
+
+const void* iap_span_read(struct IAPSpan* span, size_t count) {
+    check_ret(span->size >= count, NULL);
+    span->ptr += count;
+    span->size -= count;
+    return span->ptr - count;
+}
+
+void* iap_span_alloc(struct IAPSpan* span, size_t count) __attribute__((alias("iap_span_read")));
+
+IAPBool iap_span_append(struct IAPSpan* span, const void* ptr, size_t size) {
+    void* dest = iap_span_alloc(span, size);
+    check_ret(dest != NULL, iap_false);
+    memcpy(dest, ptr, size);
+    return iap_true;
+}
+
+#define iap_span_template(width)                                                  \
+    IAPBool iap_span_peek_##width(struct IAPSpan* span, uint##width##_t* value) { \
+        check_ret(span->size >= width / 8, iap_false);                            \
+        *value = swap_##width(*(uu##width*)span->ptr);                            \
+        return iap_true;                                                          \
+    }                                                                             \
+    IAPBool iap_span_read_##width(struct IAPSpan* span, uint##width##_t* value) { \
+        const uu##width* ptr = iap_span_read(span, width / 8);                    \
+        check_ret(ptr != NULL, iap_false);                                        \
+        *value = swap_##width(*ptr);                                              \
+        return iap_true;                                                          \
+    }                                                                             \
+    IAPBool iap_span_write_##width(struct IAPSpan* span, uint##width##_t value) { \
+        uint##width##_t* ptr = iap_span_alloc(span, width / 8);                   \
+        check_ret(ptr != NULL, iap_false);                                        \
+        *ptr = swap_##width(value);                                               \
+        return iap_true;                                                          \
+    }
+iap_span_template(8);
+iap_span_template(16);
+iap_span_template(32);
+iap_span_template(64);
+#undef iap_span_template
diff --git a/firmware/usbstack/iap/libiap/span.h b/firmware/usbstack/iap/libiap/span.h
new file mode 100644
index 0000000000..ddb5b084f0
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/span.h
@@ -0,0 +1,32 @@
+#pragma once
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "bool.h"
+
+struct IAPSpan {
+    uint8_t* ptr;
+    size_t   size;
+};
+
+const void* iap_span_read(struct IAPSpan* span, size_t count);
+void*       iap_span_alloc(struct IAPSpan* span, size_t count);
+IAPBool     iap_span_append(struct IAPSpan* span, const void* ptr, size_t size);
+
+#define iap_span_template(width)                                                 \
+    IAPBool iap_span_peek_##width(struct IAPSpan* span, uint##width##_t* value); \
+    IAPBool iap_span_read_##width(struct IAPSpan* span, uint##width##_t* value); \
+    IAPBool iap_span_write_##width(struct IAPSpan* span, uint##width##_t value)
+iap_span_template(8);
+iap_span_template(16);
+iap_span_template(32);
+iap_span_template(64);
+#undef iap_span_template
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/firmware/usbstack/iap/libiap/spec/hid.h b/firmware/usbstack/iap/libiap/spec/hid.h
new file mode 100644
index 0000000000..a2c79b0fba
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/hid.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <stdint.h>
+
+/* [2] P.56 Table 3-2 Link control byte usage */
+enum IAPHIDReportLinkControlBits {
+    IAPHIDReportLinkControlBits_Continue     = 1 << 0,
+    IAPHIDReportLinkControlBits_MoreToFollow = 1 << 1,
+};
+
+struct IAPHIDReport {
+    uint8_t report_id;
+    uint8_t link_control; /* IAPHIDReportLinkControlBits */
+    uint8_t data[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/iap.h b/firmware/usbstack/iap/libiap/spec/iap.h
new file mode 100644
index 0000000000..fe1d51ebb8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/iap.h
@@ -0,0 +1,54 @@
+#pragma once
+/* References:
+ * [1]: MFi Accessory Firmware Specification R46
+ * [2]: MFI Accessory Hardware Specification R9
+ * [3]: MFi Accessory Interface Specification For Apple Devices R2
+ */
+
+#define IAP_SYNC_BYTE 0xFF
+#define IAP_SOF_BYTE  0x55
+
+/* [1] P.109 Table 2-10 iAP command packet format
+ * | name | size | description |
+ * | sync       | 0 or 1 | IAP_SYNC_BYTE, exists if UART transport is used |
+ * | sof        | 1      | IAP_SOF_BYTE |
+ * | length     | 1 or 3 | 0xNN or 0x00NNNN. sum of length of {lingo,command,trans}_id,payload |
+ * | lingo_id   | 1      | IAP_LINGO_ID, lingo identifier |
+ * | command_id | 1 or 2 | 2 bytes long if lingo == 4 |
+ * | trans_id   | 0 or 2 | exists for some commands |
+ * | payload    | N      | data |
+ * | checksum   | 1      | crc checksum |
+ */
+
+/* [1] P.211 Table 4-1 Additional iAP lingoes */
+enum IAPLingoID {
+    IAPLingoID_General            = 0x00,
+    IAPLingoID_Microphone         = 0x01,
+    IAPLingoID_SimpleRemote       = 0x02,
+    IAPLingoID_DisplayRemote      = 0x03,
+    IAPLingoID_ExtendedInterface  = 0x04,
+    IAPLingoID_AccessoryPower     = 0x05,
+    IAPLingoID_USBHostMode        = 0x06,
+    IAPLingoID_RFTuner            = 0x07,
+    IAPLingoID_AccessoryEqualizer = 0x08,
+    IAPLingoID_Sports             = 0x09,
+    IAPLingoID_DigitalAudio       = 0x0A,
+    IAPLingoID_Storage            = 0x0C,
+    IAPLingoID_IPodOut            = 0x0D,
+    IAPLingoID_Location           = 0x0E,
+};
+
+#include "lingoes/accessory-equalizer.h"
+#include "lingoes/accessory-power.h"
+#include "lingoes/digital-audio.h"
+#include "lingoes/display-remote.h"
+#include "lingoes/extended-interface.h"
+#include "lingoes/general.h"
+#include "lingoes/ipod-out.h"
+#include "lingoes/location.h"
+#include "lingoes/microphone.h"
+#include "lingoes/rf-tuner.h"
+#include "lingoes/simple-remote.h"
+#include "lingoes/sports.h"
+#include "lingoes/storage.h"
+#include "lingoes/usb-host-mode.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer.h b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer.h
new file mode 100644
index 0000000000..f34386a5ae
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.331 Table 4-200 Accessory Equalizer lingo command summary */
+enum IAPAccessoryEqualizerCommandID {
+    IAPAccessoryEqualizerCommandID_AccessoryAck      = 0x00, /* from acc, general/acc-ack.h */
+    IAPAccessoryEqualizerCommandID_GetCurrentEQIndex = 0x01, /* from dev, no payload */
+    IAPAccessoryEqualizerCommandID_RetCurrentEQIndex = 0x02, /* from acc, current-eq-index.h */
+    IAPAccessoryEqualizerCommandID_SetCurrentEQIndex = 0x03, /* from dev, current-eq-index.h */
+    IAPAccessoryEqualizerCommandID_GetEQSettingCount = 0x04, /* from dev, no payload */
+    IAPAccessoryEqualizerCommandID_RetEQSettingCount = 0x05, /* from acc, eq-setting-count.h */
+    IAPAccessoryEqualizerCommandID_GetEQIndexName    = 0x06, /* from dev, */
+    IAPAccessoryEqualizerCommandID_RetEQIndexName    = 0x07, /* from acc, */
+};
+
+#include "accessory-equalizer/current-eq-index.h"
+#include "accessory-equalizer/eq-index-name.h"
+#include "accessory-equalizer/eq-setting-count.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/current-eq-index.h b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/current-eq-index.h
new file mode 100644
index 0000000000..4cd2fc0845
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/current-eq-index.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetCurrentEQIndexPayload {
+    uint8_t index;
+} __attribute__((packed));
+
+struct IAPSetCurrentEQIndexPayload {
+    uint8_t index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/eq-index-name.h b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/eq-index-name.h
new file mode 100644
index 0000000000..4d7f0a4cc5
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/eq-index-name.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAGetEQIndexNamePayload {
+    uint8_t index;
+} __attribute__((packed));
+
+struct IARetEQIndexNamePayload {
+    uint8_t index;
+    char    name[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/eq-setting-count.h b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/eq-setting-count.h
new file mode 100644
index 0000000000..b798803c6e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-equalizer/eq-setting-count.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetEQSettingCountPayload {
+    uint8_t count;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/accessory-power.h b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-power.h
new file mode 100644
index 0000000000..040040a9ee
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/accessory-power.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.548 Table C-37 Accessory Power lingo command summary */
+enum IAPAccessoryPowerCommandID {
+    IAPAccessoryPowerCommandID_BeginHighPower = 0x02,
+    IAPAccessoryPowerCommandID_EndHighPower   = 0x03,
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio.h b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio.h
new file mode 100644
index 0000000000..804c7db444
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.346 Table 4-232 Digital Audio lingo command summary */
+enum IAPDigitalAudioCommandID {
+    IAPDigitalAudioCommandID_AccessoryAck               = 0x00, /* from acc, general/acc-ack.h */
+    IAPDigitalAudioCommandID_IPodAck                    = 0x01, /* from dev, general/ipod-ack.h */
+    IAPDigitalAudioCommandID_GetAccessorySampleRateCaps = 0x02, /* from dev, no payload */
+    IAPDigitalAudioCommandID_RetAccessorySampleRateCaps = 0x03, /* from acc, accessory-sample-rate-caps.h */
+    IAPDigitalAudioCommandID_TrackNewAudioAttributes    = 0x04, /* from acc, track-new-audio-attributes.h */
+    IAPDigitalAudioCommandID_SetVideoDelay              = 0x05, /* from acc, set-video-delay.h */
+};
+
+#include "digital-audio/accessory-sample-rate-caps.h"
+#include "digital-audio/set-video-delay.h"
+#include "digital-audio/track-new-audio-attributes.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/accessory-sample-rate-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/accessory-sample-rate-caps.h
new file mode 100644
index 0000000000..4cda0456b2
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/accessory-sample-rate-caps.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/*
+struct IAPRetAccessorySampleRateCapsPayload {
+    uint32_t sample_rates[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/set-video-delay.h b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/set-video-delay.h
new file mode 100644
index 0000000000..851040d3cd
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/set-video-delay.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPSetVideoDelayPayload {
+    uint32_t delay_ms;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/track-new-audio-attributes.h b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/track-new-audio-attributes.h
new file mode 100644
index 0000000000..e43638c016
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/digital-audio/track-new-audio-attributes.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPTrackNewAudioAttributesPayload {
+    uint32_t sample_rate;
+    uint32_t sound_check;
+    uint32_t volume_adjustment;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote.h
new file mode 100644
index 0000000000..62185f3b8f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote.h
@@ -0,0 +1,56 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.249 Table 4-48 Display Remote lingo command summary */
+enum IAPDisplayRemoteCommandID {
+    IAPDisplayRemoteCommandID_IPodAck                    = 0x00, /* from acc, general/ipod-ack.h */
+    IAPDisplayRemoteCommandID_GetCurrentEQProfileIndex   = 0x01, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetCurrentEQProfileIndex   = 0x02, /* from dev, current-eq-profile-index.h */
+    IAPDisplayRemoteCommandID_SetCurrentEQProfileIndex   = 0x03, /* from acc, current-eq-profile-index.h */
+    IAPDisplayRemoteCommandID_GetNumEQProfiles           = 0x04, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetNumEQProfiles           = 0x05, /* from dev, num-eq-profiles.h */
+    IAPDisplayRemoteCommandID_GetIndexedEQProfileName    = 0x06, /* from acc, indexed-eq-profile-name.h */
+    IAPDisplayRemoteCommandID_RetIndexedEQProfileName    = 0x07, /* from dev, indexed-eq-profile-name.h */
+    IAPDisplayRemoteCommandID_SetRemoteEventNotification = 0x08, /* from acc, ipod-state.h */
+    IAPDisplayRemoteCommandID_RemoteEventNotification    = 0x09, /* from dev, ipod-state.h */
+    IAPDisplayRemoteCommandID_GetRemoteEventStatus       = 0x0A, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetRemoteEventStatus       = 0x0B, /* from dev, remote-event-status.h */
+    IAPDisplayRemoteCommandID_GetIPodStateInfo           = 0x0C, /* from acc, ipod-state.h */
+    IAPDisplayRemoteCommandID_RetIPodStateInfo           = 0x0D, /* from dev, ipod-state.h */
+    IAPDisplayRemoteCommandID_SetIPodStateInfo           = 0x0E, /* from acc, ipod-state.h */
+    IAPDisplayRemoteCommandID_GetPlayStatus              = 0x0F, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetPlayStatus              = 0x10, /* from dev, play-status.h */
+    IAPDisplayRemoteCommandID_SetCurrentPlayingTrack     = 0x11, /* from acc, set-current-playing-track.h */
+    IAPDisplayRemoteCommandID_GetIndexedPlayingTrackInfo = 0x12, /* from acc, indexed-playing-track-info.h */
+    IAPDisplayRemoteCommandID_RetIndexedPlayingTrackInfo = 0x13, /* from dev, indexed-playing-track-info.h */
+    IAPDisplayRemoteCommandID_GetNumPlayingTracks        = 0x14, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetNumPlayingTracks        = 0x15, /* from dev, num-playing-tracks.h */
+    IAPDisplayRemoteCommandID_GetArtworkFormats          = 0x16, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetArtworkFormats          = 0x17, /* from dev, artwork-formats.h */
+    IAPDisplayRemoteCommandID_GetTrackArtworkData        = 0x18, /* from acc, track-artwork-data.h */
+    IAPDisplayRemoteCommandID_RetTrackArtworkData        = 0x19, /* from dev, track-artwork-data.h */
+    IAPDisplayRemoteCommandID_GetPowerBatteryState       = 0x1A, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetPowerBatteryState       = 0x1B, /* from dev, power-battery-state.h */
+    IAPDisplayRemoteCommandID_GetSoundCheckState         = 0x1C, /* from acc, no payload */
+    IAPDisplayRemoteCommandID_RetSoundCheckState         = 0x1D, /* from dev, sound-check-status.h */
+    IAPDisplayRemoteCommandID_SetSoundCheckState         = 0x1E, /* from acc, sound-check-status.h */
+    IAPDisplayRemoteCommandID_GetTrackArtworkTimes       = 0x1F, /* from acc, track-artwork-times.h */
+    IAPDisplayRemoteCommandID_RetTrackArtworkTimes       = 0x20, /* from dev, track-artwork-times.h */
+    IAPDisplayRemoteCommandID_CreateGeniusPlaylist       = 0x21, /* from acc, genius_playlist.h */
+    IAPDisplayRemoteCommandID_IsGeniusAvailableForTrack  = 0x22, /* from acc, genius_playlist.h */
+};
+
+#include "display-remote/artwork-formats.h"
+#include "display-remote/current-eq-profile-index.h"
+#include "display-remote/genius-playlist.h"
+#include "display-remote/indexed-eq-profile-name.h"
+#include "display-remote/indexed-playing-track-info.h"
+#include "display-remote/ipod-state.h"
+#include "display-remote/num-eq-profiles.h"
+#include "display-remote/num-playing-tracks.h"
+#include "display-remote/play-status.h"
+#include "display-remote/power-battery-state.h"
+#include "display-remote/remote-event-status.h"
+#include "display-remote/set-current-playing-track.h"
+#include "display-remote/track-artwork-data.h"
+#include "display-remote/track-artwork-times.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/artwork-formats.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/artwork-formats.h
new file mode 100644
index 0000000000..5c4b072532
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/artwork-formats.h
@@ -0,0 +1,21 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPArtworkPixelFormats {
+    IAPArtworkPixelFormats_Mono     = 0x01,
+    IAPArtworkPixelFormats_RGB565LE = 0x02,
+    IAPArtworkPixelFormats_RGB565BE = 0x03,
+};
+
+struct IAPArtworkFormat {
+    uint16_t format_id;
+    uint8_t  pixel_format; /* IAPArtworkPixelFormats */
+    uint16_t image_width;
+    uint16_t image_height;
+} __attribute__((packed));
+
+/*
+struct IAPRetArtworkFormatsPayload {
+    struct IAPArtworkFormat formats[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/current-eq-profile-index.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/current-eq-profile-index.h
new file mode 100644
index 0000000000..3c00f06f28
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/current-eq-profile-index.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetCurrentEQProfileIndexPayload {
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPSetCurrentEQProfileIndexPayload {
+    uint32_t index;
+    uint8_t  restore_on_exit;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/genius-playlist.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/genius-playlist.h
new file mode 100644
index 0000000000..b1da0f9f2f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/genius-playlist.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPCreateGeniusPlaylistPayload {
+    uint32_t playback_index;
+} __attribute__((packed));
+
+struct IAPIsGeniusAvailableForTrackPayload {
+    uint32_t playback_index;
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/indexed-eq-profile-name.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/indexed-eq-profile-name.h
new file mode 100644
index 0000000000..d526e1df10
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/indexed-eq-profile-name.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetIndexedEQProfileNamePayload {
+    uint32_t index;
+} __attribute__((packed));
+
+/*
+struct IAPRetIndexedEQProfileNamePayload {
+    char name[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/indexed-playing-track-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/indexed-playing-track-info.h
new file mode 100644
index 0000000000..4baeeed66c
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/indexed-playing-track-info.h
@@ -0,0 +1,85 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPIndexedPlayingTrackInfoType {
+    IAPIndexedPlayingTrackInfoType_TrackCapsInfo   = 0x00,
+    IAPIndexedPlayingTrackInfoType_ChapterTimeName = 0x01,
+    IAPIndexedPlayingTrackInfoType_ArtistName      = 0x02,
+    IAPIndexedPlayingTrackInfoType_AlbumName       = 0x03,
+    IAPIndexedPlayingTrackInfoType_GenreName       = 0x04,
+    IAPIndexedPlayingTrackInfoType_TrackTitle      = 0x05,
+    IAPIndexedPlayingTrackInfoType_ComposerName    = 0x06,
+    IAPIndexedPlayingTrackInfoType_Lyrics          = 0x07,
+    IAPIndexedPlayingTrackInfoType_ArtworkCount    = 0x08,
+};
+
+struct IAPGetIndexedPlayingTrackInfoPayload {
+    uint8_t  type; /* IAPIndexedPlayingTrackInfoType */
+    uint32_t track_index;
+    uint16_t chapter_index;
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoPayload {
+    uint8_t type; /* IAPIndexedPlayingTrackInfoType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoTrackCapsInfoPayload {
+    uint8_t  type;       /* = IAPIndexedPlayingTrackInfoType_TrackCapsInfo */
+    uint32_t track_caps; /* IAPIPodStateTrackCapBits */
+    uint32_t track_total_ms;
+    uint16_t chapter_count;
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoChapterTimeNamePayload {
+    uint8_t  type; /* = IAPIndexedPlayingTrackInfoType_ChapterTimeName */
+    uint32_t offset_ms;
+    char     name[];
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoArtistNamePayload {
+    uint8_t type; /* = IAPIndexedPlayingTrackInfoType_ArtistName */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoAlbumNamePayload {
+    uint8_t type; /* = IAPIndexedPlayingTrackInfoType_AlbumName */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoGenreNamePayload {
+    uint8_t type; /* = IAPIndexedPlayingTrackInfoType_GenreName */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoTrackTitlePayload {
+    uint8_t type; /* = IAPIndexedPlayingTrackInfoType_TrackTitle */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoComposerNamePayload {
+    uint8_t type; /* = IAPIndexedPlayingTrackInfoType_ComposerName */
+    char    name[];
+} __attribute__((packed));
+
+enum IAPIndexedPlayingTrackInfoLyricsInfoBits {
+    IAPIndexedPlayingTrackInfoLyricsInfoBits_Series = 1 << 0,
+    IAPIndexedPlayingTrackInfoLyricsInfoBits_Last   = 1 << 1,
+};
+
+struct IAPRetIndexedPlayingTrackInfoLyricsPayload {
+    uint8_t  type;      /* = IAPIndexedPlayingTrackInfoType_Lyrics */
+    uint8_t  info_bits; /* IAPIndexedPlayingTrackInfoLyricsInfoBits */
+    uint16_t index;
+    char     lyrics[];
+} __attribute__((packed));
+
+struct IAPArtworkCount {
+    uint16_t format; /* IAPArtworkPixelFormats */
+    uint16_t count;
+} __attribute__((packed));
+
+struct IAPRetIndexedPlayingTrackInfoArtworkCountPayload {
+    uint8_t                type;   /* = IAPIndexedPlayingTrackInfoType_ArtworkCount */
+    struct IAPArtworkCount data[]; /* or 0x08 to indicate no counts */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/ipod-state.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/ipod-state.h
new file mode 100644
index 0000000000..a861ec3667
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/ipod-state.h
@@ -0,0 +1,204 @@
+#pragma once
+#include <stdint.h>
+
+/* common state types */
+
+/* [1] P.266 Table 4-74 Apple device state data */
+enum IAPIPodStateType {
+    IAPIPodStateType_TrackTimePositionMSec  = 0x00,
+    IAPIPodStateType_TrackPlaybackIndex     = 0x01,
+    IAPIPodStateType_ChapterIndex           = 0x02,
+    IAPIPodStateType_PlayStatus             = 0x03,
+    IAPIPodStateType_Volume                 = 0x04,
+    IAPIPodStateType_Power                  = 0x05,
+    IAPIPodStateType_EQSetting              = 0x06,
+    IAPIPodStateType_ShuffleSetting         = 0x07,
+    IAPIPodStateType_RepeatSetting          = 0x08,
+    IAPIPodStateType_DateTimeSetting        = 0x09,
+    IAPIPodStateType_AlarmSetting           = 0x0A,
+    IAPIPodStateType_BacklightLevel         = 0x0B,
+    IAPIPodStateType_HoldSwitchState        = 0x0C,
+    IAPIPodStateType_SoundCheckState        = 0x0D,
+    IAPIPodStateType_AudiobookSpeeed        = 0x0E,
+    IAPIPodStateType_TrackTimePositionSec   = 0x0F,
+    IAPIPodStateType_AbsoluteVolume         = 0x10,
+    IAPIPodStateType_TrackCaps              = 0x11,
+    IAPIPodStateType_PlaybackEngineContents = 0x12,
+};
+
+struct IAPIPodStatePayload {
+    uint8_t type; /* IAPIPodStateType */
+    uint8_t data[];
+} __attribute__((packed));
+
+/* [1] P.257 Table 4-61 Event notification data */
+
+struct IAPIPodStateTrackTimePositionMSecPayload {
+    uint8_t  type; /* = IAPIPodStateType_TrackTimePositionMSec */
+    uint32_t position_ms;
+} __attribute__((packed));
+
+struct IAPIPodStateTrackPlaybackIndexPayload {
+    uint8_t  type; /* = IAPIPodStateType_TrackPlaybackIndex */
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPIPodStateChapterIndexPayload {
+    uint8_t  type; /* = IAPIPodStateType_ChapterIndex */
+    uint32_t index;
+    uint16_t chapter_count;
+    uint16_t chapter_index;
+} __attribute__((packed));
+
+enum IAPIPodStatePlayStatus {
+    IAPIPodStatePlayStatus_PlaybackStopped      = 0x00,
+    IAPIPodStatePlayStatus_Playing              = 0x01,
+    IAPIPodStatePlayStatus_PlaybackPaused       = 0x02,
+    IAPIPodStatePlayStatus_FastForward          = 0x03,
+    IAPIPodStatePlayStatus_FastRewind           = 0x04,
+    IAPIPodStatePlayStatus_EndFastForwardRewind = 0x05,
+};
+
+struct IAPIPodStatePlayStatusPayload {
+    uint8_t type;   /* = IAPIPodStateType_PlayStatus */
+    uint8_t status; /* IAPIPodStatePlayStatus */
+} __attribute__((packed));
+
+struct IAPIPodStateVolumePayload {
+    uint8_t type; /* = IAPIPodStateType_Volume */
+    uint8_t mute_state;
+    uint8_t ui_volume;
+} __attribute__((packed));
+
+enum IAPIPodStatePowerState {
+    IAPIPodStatePowerState_InternalLow      = 0x00,
+    IAPIPodStatePowerState_Internal         = 0x01,
+    IAPIPodStatePowerState_ExternalBattery  = 0x02,
+    IAPIPodStatePowerState_External         = 0x03,
+    IAPIPodStatePowerState_ExternalCharging = 0x04,
+    IAPIPodStatePowerState_ExternalCharged  = 0x05,
+};
+
+struct IAPIPodStatePowerPayload {
+    uint8_t type;        /* = IAPIPodStateType_Power */
+    uint8_t power_state; /* IAPIPodStatePowerState */
+    uint8_t battery_level;
+} __attribute__((packed));
+
+struct IAPIPodStateEQSettingPayload {
+    uint8_t  type; /* = IAPIPodStateType_EQSetting */
+    uint32_t eq_index;
+} __attribute__((packed));
+
+enum IAPIPodStateShuffleSettingState {
+    IAPIPodStateShuffleSettingState_Off    = 0x00,
+    IAPIPodStateShuffleSettingState_Tracks = 0x01,
+    IAPIPodStateShuffleSettingState_Albums = 0x02,
+};
+
+struct IAPIPodStateShuffleSettingPayload {
+    uint8_t type;          /* = IAPIPodStateType_ShuffleSetting */
+    uint8_t shuffle_state; /* IAPIPodStateShuffleSettingState */
+} __attribute__((packed));
+
+enum IAPIPodStateRepeatSettingState {
+    IAPIPodStateRepeatSettingState_Off = 0x00,
+    IAPIPodStateRepeatSettingState_One = 0x01,
+    IAPIPodStateRepeatSettingState_All = 0x02,
+};
+
+struct IAPIPodStateRepeatSettingPayload {
+    uint8_t type;         /* = IAPIPodStateType_RepeatSetting */
+    uint8_t repeat_state; /* IAPIPodStateRepeatSettingState */
+} __attribute__((packed));
+
+struct IAPIPodStateDateTimeSettingPayload {
+    uint8_t  type; /* = IAPIPodStateType_DateTimeSetting */
+    uint16_t year;
+    uint8_t  month;
+    uint8_t  day;
+    uint8_t  hour;
+    uint8_t  minute;
+} __attribute__((packed));
+
+struct IAPIPodStateAlarmSettingPayload {
+    uint8_t type; /* = IAPIPodStateType_AlarmSetting */
+    uint8_t deprecated[3];
+} __attribute__((packed));
+
+struct IAPIPodStateBacklightLevelPayload {
+    uint8_t type; /* = IAPIPodStateType_BacklightLevel */
+    uint8_t level;
+} __attribute__((packed));
+
+struct IAPIPodStateHoldSwitchStatePayload {
+    uint8_t type; /* = IAPIPodStateType_HoldSwitchState */
+    uint8_t state;
+} __attribute__((packed));
+
+struct IAPIPodStateSoundCheckStatePayload {
+    uint8_t type; /* = IAPIPodStateType_SoundCheckState */
+    uint8_t state;
+} __attribute__((packed));
+
+enum IAPIPodStateAudiobookSpeeed {
+    IAPIPodStateAudiobookSpeeed_Slower = 0xFF,
+    IAPIPodStateAudiobookSpeeed_Normal = 0x00,
+    IAPIPodStateAudiobookSpeeed_Faster = 0x01,
+};
+
+struct IAPIPodStateAudiobookSpeeedPayload {
+    uint8_t type;  /* = IAPIPodStateType_AudiobookSpeeed */
+    uint8_t speed; /* IAPIPodStateAudiobookSpeeed */
+} __attribute__((packed));
+
+struct IAPIPodStateTrackTimePositionSecPayload {
+    uint8_t  type; /* = IAPIPodStateType_TrackTimePositionSec */
+    uint16_t position_s;
+} __attribute__((packed));
+
+struct IAPIPodStateAbsoluteVolumePayload {
+    uint8_t type; /* = IAPIPodStateType_AbsoluteVolume */
+    uint8_t mute_state;
+    uint8_t ui_volume;
+    uint8_t absolute_volume;
+} __attribute__((packed));
+
+enum IAPIPodStateTrackCapBits {
+    IAPIPodStateTrackCapBits_IsAudiobook            = 1 << 0,
+    IAPIPodStateTrackCapBits_HasChapters            = 1 << 1,
+    IAPIPodStateTrackCapBits_HasAlbumArts           = 1 << 2,
+    IAPIPodStateTrackCapBits_HasLyrics              = 1 << 3,
+    IAPIPodStateTrackCapBits_IsPodcast              = 1 << 4,
+    IAPIPodStateTrackCapBits_HasReleaseDate         = 1 << 5,
+    IAPIPodStateTrackCapBits_HasDescription         = 1 << 6,
+    IAPIPodStateTrackCapBits_HasVideo               = 1 << 7,
+    IAPIPodStateTrackCapBits_IsQueued               = 1 << 8,
+    IAPIPodStateTrackCapBits_GenerateGeniusPlaylist = 1 << 13,
+    IAPIPodStateTrackCapBits_IsITunesUEpisode       = 1 << 14,
+};
+
+struct IAPIPodStateTrackCapsPayload {
+    uint8_t  type; /* = IAPIPodStateType_TrackCaps */
+    uint32_t caps; /* IAPIPodStateTrackCapBits */
+} __attribute__((packed));
+
+struct IAPIPodStatePlaybackEngineContentsPayload {
+    uint8_t  type; /* = IAPIPodStateType_PlaybackEngineContents */
+    uint32_t count;
+} __attribute__((packed));
+
+/* event notification */
+struct IAPSetRemoteEventNotificationPayload {
+    uint32_t mask; /* (1 << IAPIPodStateType) | ... */
+} __attribute__((packed));
+
+/* IAPRemoteEventNotificationPayload = IAPIPodStatePayload */
+
+/* get/ret state info */
+struct IAPGetIPodStateInfoPayload {
+    uint8_t type; /* = IAPIPodStateType */
+} __attribute__((packed));
+
+/* RetIPodStateInfoPayload = IAPIPodStatePayload */
+/* SetIPodStateInfoPayload = IAPIPodStatePayload */
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/num-eq-profiles.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/num-eq-profiles.h
new file mode 100644
index 0000000000..bb40c6dacf
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/num-eq-profiles.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetNumEQProfilesPayload {
+    uint32_t count;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/num-playing-tracks.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/num-playing-tracks.h
new file mode 100644
index 0000000000..b8d7a59d6a
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/num-playing-tracks.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetNumPlayingTracksPayload {
+    uint32_t num_playing_tracks;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/play-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/play-status.h
new file mode 100644
index 0000000000..6ddfd38d74
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/play-status.h
@@ -0,0 +1,9 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetPlayStatusPayload {
+    uint8_t  state; /* IAPIPodStatePlayStatus */
+    uint32_t track_index;
+    uint32_t track_total_ms;
+    uint32_t track_pos_ms;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/power-battery-state.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/power-battery-state.h
new file mode 100644
index 0000000000..43f03f5e9f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/power-battery-state.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetPowerBatteryStatePayload {
+    uint8_t power_state; /* IAPIPodStatePowerState */
+    uint8_t battery_level;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/remote-event-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/remote-event-status.h
new file mode 100644
index 0000000000..7b7e6a4a39
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/remote-event-status.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetRemoteEventStatusPayload {
+    uint32_t mask; /* (1 << IAPRemoteEventNotifications) | ... */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/set-current-playing-track.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/set-current-playing-track.h
new file mode 100644
index 0000000000..b2522186a8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/set-current-playing-track.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPSetCurrentPlayingTrackPayload {
+    uint32_t index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/track-artwork-data.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/track-artwork-data.h
new file mode 100644
index 0000000000..f540c63f26
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/track-artwork-data.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetTrackArtworkDataPayload {
+    uint32_t track_index;
+    uint16_t format_id;
+    uint32_t offset_ms;
+} __attribute__((packed));
+
+struct IAPRetTrackArtworkDataFirstPayload {
+    uint16_t index;        /* = 0x0000 */
+    uint8_t  pixel_format; /* IAPArtworkPixelFormats */
+    uint16_t pixel_width;
+    uint16_t pixel_height;
+    uint16_t inset_top_left_x;
+    uint16_t inset_top_left_y;
+    uint16_t inset_bottom_right_x;
+    uint16_t inset_bottom_right_y;
+    uint32_t stride;
+    uint8_t  data[];
+} __attribute__((packed));
+
+struct IAPRetTrackArtworkDataSubsequenctPayload {
+    uint16_t index; /* > 0x0000 */
+    uint8_t  data[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/track-artwork-times.h b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/track-artwork-times.h
new file mode 100644
index 0000000000..670b4b5732
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/display-remote/track-artwork-times.h
@@ -0,0 +1,15 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetTrackArtworkTimesPayload {
+    uint32_t track_index;
+    uint16_t format_id;
+    uint16_t artwork_index;
+    uint16_t artwork_count;
+} __attribute__((packed));
+
+/*
+struct IAPRetTrackArtworkTimesPayload {
+    uint32_t offsets_ms[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface.h
new file mode 100644
index 0000000000..b40c4342ea
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface.h
@@ -0,0 +1,109 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.398 Table 5-1 Extended Interface lingo command summary */
+enum IAPExtendedInterfaceCommandID {
+    IAPExtendedInterfaceCommandID_IPodAck                                    = 0x0001, /* 1.00, NoAuth, from acc, ipod-ack.h */
+    IAPExtendedInterfaceCommandID_GetCurrentPlayingTrackChapterInfo          = 0x0002, /* 1.06, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackChapterInfo       = 0x0003, /* 1.06, NoAuth, from dev, current-playing-track-chapter.h */
+    IAPExtendedInterfaceCommandID_SetCurrentPlayingTrackChapter              = 0x0004, /* 1.06, NoAuth, from dev, current-playing-track-chapter.h */
+    IAPExtendedInterfaceCommandID_GetCurrentPlayingTrackChapterPlayStatus    = 0x0005, /* 1.06, NoAuth, from dev, current-playing-track-chapter.h */
+    IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackChapterPlayStatus = 0x0006, /* 1.06, NoAuth, from dev, current-playing-track-chapter.h */
+    IAPExtendedInterfaceCommandID_GetCurrentPlayingTrackChapterName          = 0x0007, /* 1.06, NoAuth, from dev, current-playing-track-chapter.h */
+    IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackChapterName       = 0x0008, /* 1.06, NoAuth, from dev, current-playing-track-chapter.h */
+    IAPExtendedInterfaceCommandID_GetAudiobookSpeed                          = 0x0009, /* 1.06, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_RetAudiobookSpeed                          = 0x000A, /* 1.06, NoAuth, from dev, audiobook-speed.h */
+    IAPExtendedInterfaceCommandID_SetAudiobookSpeed                          = 0x000B, /* 1.06, NoAuth, from acc, audiobook-speed.h */
+    IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackInfo                 = 0x000C, /* 1.08, NoAuth, from acc, indexed-playing-track-info.h */
+    IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackInfo              = 0x000D, /* 1.08, NoAuth, from dev, indexed-playing-track-info.h */
+    IAPExtendedInterfaceCommandID_GetArtworkFormats                          = 0x000E, /* 1.10, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_RetArtworkFormats                          = 0x000F, /* 1.10, NoAuth, from dev, display-remote/artwork-formats.h*/
+    IAPExtendedInterfaceCommandID_GetTrackArtworkData                        = 0x0010, /* 1.10, NoAuth, from acc, display-remote/track-artwork-data.h*/
+    IAPExtendedInterfaceCommandID_RetTrackArtworkData                        = 0x0011, /* 1.10, NoAuth, from dev, display-remote/track-artwork-data.h*/
+    IAPExtendedInterfaceCommandID_RequestProtocolVersion                     = 0x0012, /* deprecated */
+    IAPExtendedInterfaceCommandID_ReturnProtocolVersion                      = 0x0013, /* deprecated */
+    IAPExtendedInterfaceCommandID_RequestIPodName                            = 0x0014, /* deprecated */
+    IAPExtendedInterfaceCommandID_ReturnIPodName                             = 0x0015, /* deprecated */
+    IAPExtendedInterfaceCommandID_ResetDBSelection                           = 0x0016, /* 1.00, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_SelectDBRecord                             = 0x0017, /* 1.00, NoAuth, from dev, database.h*/
+    IAPExtendedInterfaceCommandID_GetNumberCategorizedDBRecords              = 0x0018, /* 1.00, NoAuth, from acc, database.h */
+    IAPExtendedInterfaceCommandID_ReturnNumberCategorizedDBRecords           = 0x0019, /* 1.00, NoAuth, from dev, database.h */
+    IAPExtendedInterfaceCommandID_RetrieveCategorizedDatabaseRecords         = 0x001A, /* 1.00, NoAuth, from acc, database.h */
+    IAPExtendedInterfaceCommandID_ReturnCategorizedDatabaseRecords           = 0x001B, /* 1.00, NoAuth, from acc, database.h */
+    IAPExtendedInterfaceCommandID_GetPlayStatus                              = 0x001C, /* 1.00, NoAuth, no payload */
+    IAPExtendedInterfaceCommandID_ReturnPlayStatus                           = 0x001D, /* 1.00, NoAuth, play-status.h */
+    IAPExtendedInterfaceCommandID_GetCurrentPlayingTrackIndex                = 0x001E, /* 1.00, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnCurrentPlayingTrackIndex             = 0x001F, /* 1.00, NoAuth, from dev, current-playing-index.h */
+    IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackTitle                = 0x0020, /* 1.00, NoAuth, from acc, indexed-playing-track-string.h */
+    IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackTitle             = 0x0021, /* 1.00, NoAuth, from dev, indexed-playing-track-string.h */
+    IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackArtistName           = 0x0022, /* 1.00, NoAuth, from acc, indexed-playing-track-string.h */
+    IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackArtistName        = 0x0023, /* 1.00, NoAuth, from dev, indexed-playing-track-string.h */
+    IAPExtendedInterfaceCommandID_GetIndexedPlayingTrackAlbumName            = 0x0024, /* 1.00, NoAuth, from acc, indexed-playing-track-string.h */
+    IAPExtendedInterfaceCommandID_ReturnIndexedPlayingTrackAlbumName         = 0x0025, /* 1.00, NoAuth, from dev, indexed-playing-track-string.h */
+    IAPExtendedInterfaceCommandID_SetPlayStatusChangeNotification            = 0x0026, /* 1.00, NoAuth, from acc, play-status-change-notification.h */
+    IAPExtendedInterfaceCommandID_PlayStatusChangeNotification               = 0x0027, /* 1.00, NoAuth, from dev, play-status-change-notification.h */
+    IAPExtendedInterfaceCommandID_PlayCurrentSelection                       = 0x0028, /* 1.00, NoAuth, from acc, play-current-selection.h */
+    IAPExtendedInterfaceCommandID_PlayControl                                = 0x0029, /* 1.00, NoAuth, from acc, play-control.h */
+    IAPExtendedInterfaceCommandID_GetTrackArtworkTimes                       = 0x002A, /* 1.10, NoAuth, from acc, display-remote/track-artwork-times.h */
+    IAPExtendedInterfaceCommandID_RetTrackArtworkTimes                       = 0x002B, /* 1.10, NoAuth, from dev, display-remote/track-artwork-times.h */
+    IAPExtendedInterfaceCommandID_GetShuffle                                 = 0x002C, /* 1.00, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnShuffle                              = 0x002D, /* 1.00, NoAuth, from dev, shuffle.h */
+    IAPExtendedInterfaceCommandID_SetShuffle                                 = 0x002E, /* 1.00, NoAuth, from acc, shuffle.h */
+    IAPExtendedInterfaceCommandID_GetRepeat                                  = 0x002F, /* 1.00, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnRepeat                               = 0x0030, /* 1.00, NoAuth, from dev, repeat.h */
+    IAPExtendedInterfaceCommandID_SetRepeat                                  = 0x0031, /* 1.00, NoAuth, from acc, repeat.h */
+    IAPExtendedInterfaceCommandID_SetDisplayImage                            = 0x0032, /* 1.01, NoAuth, from acc, set-display-image.h */
+    IAPExtendedInterfaceCommandID_GetMonoDisplayImageLimits                  = 0x0033, /* 1.01, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnMonoDisplayImageLimits               = 0x0034, /* 1.01, NoAuth, from dev, mono-display-image-limits.h */
+    IAPExtendedInterfaceCommandID_GetNumPlayingTracks                        = 0x0035, /* 1.01, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnNumPlayingTracks                     = 0x0036, /* 1.01, NoAuth, from dev, display-remote/num-playing-tracks.h */
+    IAPExtendedInterfaceCommandID_SetCurrentPlayingTrack                     = 0x0037, /* 1.01, NoAuth, from acc, display-remote/set-current-playing-track.h */
+    IAPExtendedInterfaceCommandID_SelectSortDBRecord                         = 0x0038, /* deprecated */
+    IAPExtendedInterfaceCommandID_GetColorDisplayImageLimits                 = 0x0039, /* 1.09, NoAuth, from acc, no payload */
+    IAPExtendedInterfaceCommandID_ReturnColorDisplayImageLimits              = 0x003A, /* 1.09, NoAuth, from dev, color-display-image-limits.h */
+    IAPExtendedInterfaceCommandID_ResetDBSelectionHierarchy                  = 0x003B, /* 1.11, Auth, from acc, db-selection-hierarchy.h */
+    IAPExtendedInterfaceCommandID_GetDBITunesInfo                            = 0x003C, /* 1.13, Auth, from acc, db-itunes-info.h */
+    IAPExtendedInterfaceCommandID_RetDBITunesInfo                            = 0x003D, /* 1.13, Auth, from dev, db-itunes-info.h */
+    IAPExtendedInterfaceCommandID_GetUIDTrackInfo                            = 0x003E, /* 1.13, Auth, from acc, track-info.h */
+    IAPExtendedInterfaceCommandID_RetUIDTrackInfo                            = 0x003F, /* 1.13, Auth, from dev, track-info.h */
+    IAPExtendedInterfaceCommandID_GetDBTrackInfo                             = 0x0040, /* 1.13, Auth, from acc, track-info.h */
+    IAPExtendedInterfaceCommandID_RetDBTrackInfo                             = 0x0041, /* 1.13, Auth, from dev, track-info.h */
+    IAPExtendedInterfaceCommandID_GetPBTrackInfo                             = 0x0042, /* 1.13, Auth, from acc, track-info.h */
+    IAPExtendedInterfaceCommandID_RetPBTrackInfo                             = 0x0043, /* 1.13, Auth, from dev, track-info.h */
+    IAPExtendedInterfaceCommandID_CreateGeniusPlaylist                       = 0x0044, /* 1.13, Auth, from acc, genius-playlist.h */
+    IAPExtendedInterfaceCommandID_RefreshGeniusPlaylist                      = 0x0045, /* 1.13, Auth, from acc, genius-playlist.h */
+    IAPExtendedInterfaceCommandID_IsGeniusAvailableForTrack                  = 0x0047, /* 1.13, Auth, from acc, genius-playlist.h */
+    IAPExtendedInterfaceCommandID_GetPlaylistInfo                            = 0x0048, /* 1.13, Auth, from acc, playlist-info.h */
+    IAPExtendedInterfaceCommandID_RetPlaylistInfo                            = 0x0049, /* 1.13, Auth, from dev, playlist-info.h */
+    IAPExtendedInterfaceCommandID_PrepareUIDList                             = 0x004A, /* 1.14, Auth, from dev, uid-list.h */
+    IAPExtendedInterfaceCommandID_PlayPreparedUIDList                        = 0x004B, /* 1.14, Auth, from dev, uid-list.h */
+    IAPExtendedInterfaceCommandID_GetArtworkTimes                            = 0x004C, /* 1.14, Auth, from acc, artwork-times.h */
+    IAPExtendedInterfaceCommandID_RetArtworkTimes                            = 0x004D, /* 1.14, Auth, from dev, artwork-times.h */
+    IAPExtendedInterfaceCommandID_GetArtworkData                             = 0x004E, /* 1.14, Auth, from acc, artwork-data.h */
+    IAPExtendedInterfaceCommandID_RetArtworkData                             = 0x004F, /* 1.14, Auth, from dev, artwork-data.h */
+};
+
+#include "extended-interface/artwork-data.h"
+#include "extended-interface/artwork-times.h"
+#include "extended-interface/audiobook-speed.h"
+#include "extended-interface/color-display-image-limits.h"
+#include "extended-interface/current-playing-track-chapter.h"
+#include "extended-interface/current-playing-track-index.h"
+#include "extended-interface/database.h"
+#include "extended-interface/db-itunes-info.h"
+#include "extended-interface/db-selection-hierarchy.h"
+#include "extended-interface/genius-playlist.h"
+#include "extended-interface/indexed-playing-track-info.h"
+#include "extended-interface/indexed-playing-track-string.h"
+#include "extended-interface/ipod-ack.h"
+#include "extended-interface/mono-display-image-limits.h"
+#include "extended-interface/play-control.h"
+#include "extended-interface/play-current-selection.h"
+#include "extended-interface/play-status-change-notification.h"
+#include "extended-interface/play-status.h"
+#include "extended-interface/playlist-info.h"
+#include "extended-interface/repeat.h"
+#include "extended-interface/set-display-image.h"
+#include "extended-interface/shuffle.h"
+#include "extended-interface/track-info.h"
+#include "extended-interface/uid-list.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/artwork-data.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/artwork-data.h
new file mode 100644
index 0000000000..67c0569750
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/artwork-data.h
@@ -0,0 +1,58 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.459 5.1.72 Command 0x004E: GetArtworkData */
+
+struct IAPGetArtworkDataPayload {
+    uint8_t id_type; /* IAPGetArtworkTrackIDType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPGetArtworkDataUIDPayload {
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType_UID */
+    uint8_t  uid[8];
+    uint16_t format_id;
+    uint32_t offset_ms;
+} __attribute__((packed));
+
+struct IAPGetArtworkDataIndexPayload {
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType_{PlaybackListIndex,DatabaseIndex} */
+    uint32_t index;
+    uint16_t format_id;
+    uint32_t offset_ms;
+} __attribute__((packed));
+
+struct IAPRetArtworkDataPayload {
+    uint16_t current_sector;
+    uint16_t max_sectors;
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType */
+    uint8_t  data[];
+} __attribute__((packed));
+
+struct IAPRetArtworkDataBody {
+    uint8_t  pixel_format; /* IAPArtworkPixelFormats */
+    uint16_t pixel_width;
+    uint16_t pixel_height; /* [1] P.461 "Table 5-104 imageDescriptionAndData format" misses this field, but should be here */
+    uint16_t inset_top_left_x;
+    uint16_t inset_top_left_y;
+    uint16_t inset_bottom_right_x;
+    uint16_t inset_bottom_right_y;
+    uint32_t stride;
+    uint8_t  data[];
+} __attribute__((packed));
+
+struct IAPRetArtworkDataUIDPayload {
+    uint16_t                     current_sector;
+    uint16_t                     max_sectors;
+    uint8_t                      id_type; /* = IAPGetArtworkTrackIDType_UID */
+    uint8_t                      uid[8];
+    struct IAPRetArtworkDataBody body;
+} __attribute__((packed));
+
+struct IAPRetArtworkDataIndexPayload {
+    uint16_t                     current_sector;
+    uint16_t                     max_sectors;
+    uint8_t                      id_type; /* = IAPGetArtworkTrackIDType_{PlaybackListIndex,DatabaseIndex} */
+    uint32_t                     index;
+    struct IAPRetArtworkDataBody body;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/artwork-times.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/artwork-times.h
new file mode 100644
index 0000000000..41207efe5e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/artwork-times.h
@@ -0,0 +1,48 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.458 5.1.70 Command 0x004C: GetArtworkTimes */
+
+enum IAPArtworkTrackIDType {
+    IAPGetArtworkTrackIDType_UID               = 0x00,
+    IAPGetArtworkTrackIDType_PlaybackListIndex = 0x01,
+    IAPGetArtworkTrackIDType_DatabaseIndex     = 0x02,
+};
+
+struct IAPGetArtworkTimesPayload {
+    uint8_t id_type; /* IAPGetArtworkTrackIDType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPGetArtworkTimesUIDPayload {
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType_UID */
+    uint8_t  uid[8];
+    uint16_t format_id;
+    uint16_t artwork_index;
+    uint16_t artwork_count;
+} __attribute__((packed));
+
+struct IAPGetArtworkTimesIndexPayload {
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType_{PlaybackListIndex,DatabaseIndex} */
+    uint32_t index;
+    uint16_t format_id;
+    uint16_t artwork_index;
+    uint16_t artwork_count;
+} __attribute__((packed));
+
+struct IAPRetArtworkTimesPayload {
+    uint8_t id_type; /* IAPGetArtworkTrackIDType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPRetArtworkTimesUIDPayload {
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType_UID */
+    uint8_t  uid[8];
+    uint32_t offsets_ms[];
+} __attribute__((packed));
+
+struct IAPRetArtworkTimesIndexPayload {
+    uint8_t  id_type; /* = IAPGetArtworkTrackIDType_{PlaybackListIndex,DatabaseIndex} */
+    uint32_t index;
+    uint32_t offsets_ms[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/audiobook-speed.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/audiobook-speed.h
new file mode 100644
index 0000000000..770cf06039
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/audiobook-speed.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetAudiobookSpeedPayload {
+    uint8_t speed; /* IAPIPodStateAudiobookSpeeed */
+} __attribute__((packed));
+
+struct IAPSetAudiobookSpeedPayload {
+    uint8_t speed;           /* IAPIPodStateAudiobookSpeeed */
+    uint8_t restore_on_exit; /* optional */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/color-display-image-limits.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/color-display-image-limits.h
new file mode 100644
index 0000000000..c5e75fd3b7
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/color-display-image-limits.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.443 5.1.53 Command 0x003A: ReturnColorDisplayImageLimits */
+
+struct IAPColorDisplayImageLimit {
+    uint16_t max_width;
+    uint16_t max_height;
+    uint8_t  pixel_format; /* IAPArtworkPixelFormats */
+} __attribute__((packed));
+
+/*
+struct IAPReturnColorDisplayImageLimitsPayload {
+    struct IAPColorDisplayImageLimit limits[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/current-playing-track-chapter.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/current-playing-track-chapter.h
new file mode 100644
index 0000000000..f059b53c49
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/current-playing-track-chapter.h
@@ -0,0 +1,30 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPReturnCurrentPlayingTrackChapterInfoPayload {
+    uint32_t index;
+    uint32_t count;
+} __attribute__((packed));
+
+struct IAPSetCurrentPlayingTrackChapterPayload {
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPGetCurrentPlayingTrackChapterPlayStatusPayload {
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPReturnCurrentPlayingTrackChapterPlayStatusPayload {
+    uint32_t length_ms;
+    uint32_t elapsed_ms;
+} __attribute__((packed));
+
+struct IAPGetCurrentPlayingTrackChapterNamePayload {
+    uint32_t index;
+} __attribute__((packed));
+
+/*
+struct IAPReturnCurrentPlayingTrackChapterNamePayload {
+    char name[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/current-playing-track-index.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/current-playing-track-index.h
new file mode 100644
index 0000000000..1f86a18e83
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/current-playing-track-index.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPReturnCurrentPlayingTrackIndexPayload {
+    uint32_t index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/database.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/database.h
new file mode 100644
index 0000000000..55a548ba7e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/database.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPDatabaseType {
+    IAPDatabaseType_TopLevel       = 0x00, /* 1.14 */
+    IAPDatabaseType_Playlist       = 0x01, /* 1.00 */
+    IAPDatabaseType_Artist         = 0x02, /* 1.00 */
+    IAPDatabaseType_Album          = 0x03, /* 1.00 */
+    IAPDatabaseType_Genre          = 0x04, /* 1.00 */
+    IAPDatabaseType_Track          = 0x05, /* 1.00 */
+    IAPDatabaseType_Composer       = 0x06, /* 1.00 */
+    IAPDatabaseType_Audiobook      = 0x07, /* 1.06 */
+    IAPDatabaseType_Podcast        = 0x08, /* 1.08 */
+    IAPDatabaseType_NestedPlaylist = 0x09, /* 1.13 */
+    IAPDatabaseType_GeniusMixes    = 0x0A, /* 1.14 */
+    IAPDatabaseType_ITunesU        = 0x0B, /* 1.14 */
+
+};
+
+struct IAPSelectDBRecord {
+    uint8_t  type; /* IAPDatabaseType */
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPGetNumberCategorizedDBRecordsPayload {
+    uint8_t type; /* IAPDatabaseType */
+} __attribute__((packed));
+
+struct IAPReturnNumberCategorizedDBRecordsPayload {
+    uint32_t count;
+} __attribute__((packed));
+
+struct IAPRetrieveCategorizedDatabaseRecordsPayload {
+    uint8_t  type; /* IAPDatabaseType */
+    uint32_t index;
+    uint32_t count;
+} __attribute__((packed));
+
+struct IAPReturnCategorizedDatabaseRecordsPayload {
+    uint32_t index;
+    char     record[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/db-itunes-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/db-itunes-info.h
new file mode 100644
index 0000000000..11618be6eb
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/db-itunes-info.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.444 5.1.55 Command 0x003C: GetDBiTunesInfo */
+
+enum IAPITunesMetadataType {
+    IAPITunesMetadataType_UID             = 0x00,
+    IAPITunesMetadataType_LastSyncDate    = 0x01,
+    IAPITunesMetadataType_AudioTrackCount = 0x02,
+    IAPITunesMetadataType_VideoTrackCount = 0x03,
+    IAPITunesMetadataType_AudiobookCount  = 0x04,
+    IAPITunesMetadataType_PhotoCount      = 0x05,
+};
+
+struct IAPGetDBITunesInfoPayload {
+    uint8_t metadata_type; /* IAPITunesMetadataType */
+} __attribute__((packed));
+
+struct IAPRetDBITunesInfoPayload {
+    uint8_t metadata_type; /* = IAPITunesMetadataType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPRetDBITunesInfoUIDPayload {
+    uint8_t metadata_type; /* = IAPITunesMetadataType_UID */
+    uint8_t uid[8];
+} __attribute__((packed));
+
+struct IAPRetDBITunesInfoLastSyncDatePayload {
+    uint8_t  metadata_type; /* = IAPITunesMetadataType_LastSyncDate */
+    uint8_t  seconds;
+    uint8_t  minute;
+    uint8_t  hour;
+    uint8_t  day;
+    uint8_t  month;
+    uint16_t year;
+} __attribute__((packed));
+
+struct IAPRetDBITunesInfoCountPayload {
+    uint8_t  metadata_type; /* = IAPITunesMetadataType_{AudioTrack,VideoTrack,Audiobook,Photo}Count */
+    uint32_t count;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/db-selection-hierarchy.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/db-selection-hierarchy.h
new file mode 100644
index 0000000000..434b21bab3
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/db-selection-hierarchy.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.443 5.1.54 Command 0x003B: ResetDBSelectionHierarchy */
+
+enum IAPResetDBSelectionHierarchySelection {
+    IAPResetDBSelectionHierarchySelection_Audio = 0x01,
+    IAPResetDBSelectionHierarchySelection_Video = 0x02,
+};
+
+struct IAPResetDBSelectionHierarchyPayload {
+    uint8_t selection; /* IAPResetDBSelectionHierarchySelection */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/genius-playlist.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/genius-playlist.h
new file mode 100644
index 0000000000..81601886e4
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/genius-playlist.h
@@ -0,0 +1,23 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.454 5.1.63 Command 0x0044: CreateGeniusPlaylist */
+
+enum IAPCreateGeniusPlaylistIndexType {
+    IAPCreateGeniusPlaylistIndexType_DatabaseEngine = 0x00,
+    IAPCreateGeniusPlaylistIndexType_PlaybackEngine = 0x01,
+};
+
+struct IAPExtendedCreateGeniusPlaylistPayload {
+    uint8_t  index_type; /* IAPCreateGeniusPlaylistIndexType */
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPExtendedRefreshGeniusPlaylistPayload {
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPExtendedIsGeniusAvailableForTrackPayload {
+    uint8_t  index_type; /* IAPCreateGeniusPlaylistIndexType */
+    uint32_t index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/indexed-playing-track-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/indexed-playing-track-info.h
new file mode 100644
index 0000000000..ed7615d1f0
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/indexed-playing-track-info.h
@@ -0,0 +1,81 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.408 5.1.13 Command 0x000C: GetIndexedPlayingTrackInfo */
+
+enum IAPExtendedIndexedPlayingTrackInfoType {
+    IAPExtendedIndexedPlayingTrackInfoType_TrackCapsInfo     = 0x00,
+    IAPExtendedIndexedPlayingTrackInfoType_PodcastName       = 0x01,
+    IAPExtendedIndexedPlayingTrackInfoType_TrackReleaseDate  = 0x02,
+    IAPExtendedIndexedPlayingTrackInfoType_TrackDescription  = 0x03,
+    IAPExtendedIndexedPlayingTrackInfoType_TrackSongLyrics   = 0x04,
+    IAPExtendedIndexedPlayingTrackInfoType_TrackGenre        = 0x05,
+    IAPExtendedIndexedPlayingTrackInfoType_TrackComposer     = 0x06,
+    IAPExtendedIndexedPlayingTrackInfoType_TrackArtworkCount = 0x07,
+};
+
+struct IAPExtendedGetIndexedPlayingTrackInfoPayload {
+    uint8_t  type; /* IAPExtendedIndexedPlayingTrackInfoType */
+    uint32_t track_index;
+    uint16_t chapter_index;
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoPayload {
+    uint8_t type; /* IAPExtendedIndexedPlayingTrackInfoType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackCapsInfoPayload {
+    uint8_t  type;       /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackCapsInfo */
+    uint32_t track_caps; /* IAPIPodStateTrackCapBits */
+    uint32_t track_total_ms;
+    uint16_t chapter_count;
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoPodcastNamePayload {
+    uint8_t type; /* = IAPExtendedRetIndexedPlayingTrackInfo_PodcastName */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackReleaseDatePayload {
+    uint8_t  type; /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackReleaseDate */
+    uint8_t  seconds;
+    uint8_t  minutes;
+    uint8_t  hours;
+    uint8_t  day;
+    uint8_t  month;
+    uint16_t year;
+    uint8_t  weekday;
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackDescriptionPayload {
+    uint8_t  type;      /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackDescription */
+    uint8_t  info_bits; /* IAPIndexedPlayingTrackInfoLyricsInfoBits */
+    uint16_t index;
+    char     description[];
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackSongLyricsPayload {
+    uint8_t  type;      /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackSongLyrics */
+    uint8_t  info_bits; /* IAPIndexedPlayingTrackInfoLyricsInfoBits */
+    uint16_t index;
+    char     lyrics[];
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackGenrePayload {
+    uint8_t type; /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackGenre */
+    char    genre[];
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackComposerPayload {
+    uint8_t type; /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackComposer */
+    char    composer[];
+} __attribute__((packed));
+
+struct IAPExtendedRetIndexedPlayingTrackInfoTrackArtworkCountPayload {
+    uint8_t type; /* = IAPExtendedRetIndexedPlayingTrackInfo_TrackArtworkCount */
+    struct {
+        uint16_t format; /* IAPArtworkPixelFormats */
+        uint16_t count;
+    } __attribute__((packed)) data[]; /* or 0x08 to indicate no counts */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/indexed-playing-track-string.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/indexed-playing-track-string.h
new file mode 100644
index 0000000000..895fa2eba3
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/indexed-playing-track-string.h
@@ -0,0 +1,20 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetIndexedPlayingTrackStringPayload {
+    uint32_t index;
+} __attribute__((packed));
+
+/* IAPGetIndexedPlayingTrackTitlePayload = IAPGetIndexedPlayingTrackStringPayload */
+/* IAPGetIndexedPlayingTrackArtistNamePayload = IAPGetIndexedPlayingTrackStringPayload */
+/* IAPGetIndexedPlayingTrackAlbumNamePayload = IAPGetIndexedPlayingTrackStringPayload */
+
+/*
+struct IAPReturnIndexedPlayingTrackStringPayload {
+    char string[];
+} __attribute__((packed));
+*/
+
+/* IAPReturnIndexedPlayingTrackTitlePayload = IAPReturnIndexedPlayingTrackStringPayload */
+/* IAPReturnIndexedPlayingTrackArtistNamePayload = IAPReturnIndexedPlayingTrackStringPayload */
+/* IAPReturnIndexedPlayingTrackAlbumNamePayload = IAPReturnIndexedPlayingTrackStringPayload */
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/ipod-ack.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/ipod-ack.h
new file mode 100644
index 0000000000..bcaf1e2203
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/ipod-ack.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPExtendedIPodAckPayload {
+    uint8_t  status; /* IAPAckStatus */
+    uint16_t id;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/mono-display-image-limits.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/mono-display-image-limits.h
new file mode 100644
index 0000000000..6f8d8ac375
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/mono-display-image-limits.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.440 5.1.48 Command 0x0034: ReturnMonoDisplayImageLimits */
+
+struct IAPReturnMonoDisplayImageLimitsPayload {
+    uint16_t max_width;
+    uint16_t max_height;
+    uint8_t  pixel_format; /* IAPArtworkPixelFormats */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-control.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-control.h
new file mode 100644
index 0000000000..0360026daa
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-control.h
@@ -0,0 +1,25 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.428 5.1.37 Command 0x0029: PlayControl */
+
+enum IAPPlayControlCode {
+    IAPPlayControlCode_TogglePlayPause = 0x01, /* 1.00 */
+    IAPPlayControlCode_Stop            = 0x02, /* 1.00 */
+    IAPPlayControlCode_NextTrack       = 0x03, /* 1.00 */
+    IAPPlayControlCode_PrevTrack       = 0x04, /* 1.00 */
+    IAPPlayControlCode_StartFF         = 0x05, /* 1.00 */
+    IAPPlayControlCode_StartRew        = 0x06, /* 1.00 */
+    IAPPlayControlCode_EndFFRew        = 0x07, /* 1.00 */
+    IAPPlayControlCode_Next            = 0x08, /* 1.06 */
+    IAPPlayControlCode_Prev            = 0x09, /* 1.06 */
+    IAPPlayControlCode_Play            = 0x0A, /* 1.13 */
+    IAPPlayControlCode_Pause           = 0x0B, /* 1.13 */
+    IAPPlayControlCode_NextChapter     = 0x0C, /* 1.14 */
+    IAPPlayControlCode_PrevChapter     = 0x0D, /* 1.14 */
+    IAPPlayControlCode_ResumeIPod      = 0x0E, /* 1.14 */
+};
+
+struct IAPPlayControlPayload {
+    uint8_t code; /* IAPPlayControlCode */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-current-selection.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-current-selection.h
new file mode 100644
index 0000000000..a543ae7b86
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-current-selection.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.546 Table C-34 PlayCurrentSelection packet */
+
+struct IAPPlayCurrentSelectionPayload {
+    uint32_t track_index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-status-change-notification.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-status-change-notification.h
new file mode 100644
index 0000000000..a7ce5095f5
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-status-change-notification.h
@@ -0,0 +1,137 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPSetPlayStatusChangeNotification1BytePayload {
+    uint8_t enable;
+} __attribute__((packed));
+
+enum IAPStatusChangeNotificationBits {
+    IAPStatusChangeNotificationBits_Basic                         = 1 << 0,
+    IAPStatusChangeNotificationBits_Extended                      = 1 << 1,
+    IAPStatusChangeNotificationBits_TrackIndex                    = 1 << 2,
+    IAPStatusChangeNotificationBits_TrackTimeOffsetMSec           = 1 << 3,
+    IAPStatusChangeNotificationBits_TrackTimeOffsetSec            = 1 << 4,
+    IAPStatusChangeNotificationBits_ChapterIndex                  = 1 << 5,
+    IAPStatusChangeNotificationBits_ChapterTimeOffsetMSec         = 1 << 6,
+    IAPStatusChangeNotificationBits_ChapterTimeOffsetSec          = 1 << 7,
+    IAPStatusChangeNotificationBits_TrackUniqueID                 = 1 << 8,
+    IAPStatusChangeNotificationBits_TrackMediaType                = 1 << 9,
+    IAPStatusChangeNotificationBits_TrackLyricsReady              = 1 << 10,
+    IAPStatusChangeNotificationBits_TrackCapsChanged              = 1 << 11,
+    IAPStatusChangeNotificationBits_PlaybackEngineContentsChanged = 1 << 12,
+
+};
+
+struct IAPSetPlayStatusChangeNotification4BytesPayload {
+    uint32_t mask; /* IAPStatusChangeNotificationBits */
+} __attribute__((packed));
+
+/* [1] P.426 5.1.36 Command 0x0027: PlayStatusChangeNotification */
+
+enum IAPStatusChangeNotificationType {
+    IAPStatusChangeNotificationType_PlaybackStopped               = 0x00,
+    IAPStatusChangeNotificationType_TrackIndex                    = 0x01,
+    IAPStatusChangeNotificationType_PlaybackFEWSeekStop           = 0x02,
+    IAPStatusChangeNotificationType_PlaybackREWSeekStop           = 0x03,
+    IAPStatusChangeNotificationType_TrackTimeOffsetMSec           = 0x04,
+    IAPStatusChangeNotificationType_ChapterIndex                  = 0x05,
+    IAPStatusChangeNotificationType_PlaybackStatusExtended        = 0x06,
+    IAPStatusChangeNotificationType_TrackTimeOffsetSec            = 0x07,
+    IAPStatusChangeNotificationType_ChapterTimeOffsetMSec         = 0x08,
+    IAPStatusChangeNotificationType_ChapterTimeOffsetSec          = 0x09,
+    IAPStatusChangeNotificationType_TrackUniqueID                 = 0x0A,
+    IAPStatusChangeNotificationType_TrackPlaybackMode             = 0x0B,
+    IAPStatusChangeNotificationType_TrackLyricsReady              = 0x0C,
+    IAPStatusChangeNotificationType_TrackCapsChanged              = 0x0D,
+    IAPStatusChangeNotificationType_PlaybackEngineContentsChanged = 0x0E,
+};
+
+struct IAPPlayStatusChangeNotificationPayload {
+    uint8_t type;
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationPlaybackStoppedPayload {
+    uint8_t type; /* = IAPStatusChangeNotificationType_PlaybackStopped */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationTrackIndexPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_TrackIndex */
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationPlaybackFEWSeekStopPayload {
+    uint8_t type; /* = IAPStatusChangeNotificationType_PlaybackFEWSeekStop */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationPlaybackREWSeekStopPayload {
+    uint8_t type; /* = IAPStatusChangeNotificationType_PlaybackREWSeekStop */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationTrackTimeOffsetMSecPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_TrackTimeOffsetMSec */
+    uint32_t offset_ms;
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationChapterIndexPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_ChapterIndex */
+    uint32_t index;
+} __attribute__((packed));
+
+enum IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates {
+    IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Stopped           = 0x02,
+    IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_FFWSeekStarted    = 0x05,
+    IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_REWSeekStarted    = 0x06,
+    IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_FFWREWSeekStopped = 0x07,
+    IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Playing           = 0x0A,
+    IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates_Paused            = 0x0B,
+};
+
+struct IAPPlayStatusChangeNotificationPlaybackStatusExtendedPayload {
+    uint8_t type;  /* = IAPStatusChangeNotificationType_PlaybackStatusExtended */
+    uint8_t state; /* IAPPlayStatusChangeNotificationPlaybackStatusExtendedStates */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationTrackTimeOffsetSecPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_TrackTimeOffsetSec */
+    uint32_t offset_s;
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationChapterTimeOffsetMSecPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_ChapterTimeOffsetMSec */
+    uint32_t offset_ms;
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationChapterTimeOffsetSecPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_ChapterTimeOffsetSec */
+    uint32_t offset_s;
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationTrackUniqueIDPayload {
+    uint8_t type; /* = IAPStatusChangeNotificationType_TrackUniqueID */
+    uint8_t id[8];
+} __attribute__((packed));
+
+enum IAPPlayStatusChangeNotificationTrackMediaTypePlayMode {
+    IAPPlayStatusChangeNotificationTrackMediaTypePlayMode_Audio = 0x00,
+    IAPPlayStatusChangeNotificationTrackMediaTypePlayMode_Video = 0x01,
+};
+
+struct IAPPlayStatusChangeNotificationTrackPlaybackModePayload {
+    uint8_t type;      /* = IAPStatusChangeNotificationType_TrackPlaybackMode */
+    uint8_t play_mode; /* IAPPlayStatusChangeNotificationTrackMediaTypePlayMode */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationTrackLyricsReadyPayload {
+    uint8_t type; /* = IAPStatusChangeNotificationType_TrackLyricsReady */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationTrackCapsChangedPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_TrackCapsChanged */
+    uint32_t caps; /* IAPIPodStateTrackCapBits */
+} __attribute__((packed));
+
+struct IAPPlayStatusChangeNotificationPlaybackEngineContentsChangedPayload {
+    uint8_t  type; /* = IAPStatusChangeNotificationType_PlaybackEngineContentsChanged */
+    uint32_t count;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-status.h
new file mode 100644
index 0000000000..66d4e00a79
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/play-status.h
@@ -0,0 +1,15 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPPlayStatus {
+    IAPPlayStatus_Stopped = 0x00,
+    IAPPlayStatus_Playing = 0x01,
+    IAPPlayStatus_Paused  = 0x02,
+    IAPPlayStatus_Error   = 0xFF,
+};
+
+struct IAPExtendedRetPlayStatusPayload {
+    uint32_t track_total_ms;
+    uint32_t track_pos_ms;
+    uint8_t  state; /* IAPPlayStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/playlist-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/playlist-info.h
new file mode 100644
index 0000000000..7693e5a002
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/playlist-info.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.456 5.1.66 Command 0x0048: GetPlaylistInfo */
+
+enum IAPPlaylistInfoType {
+    PlaylistInfo = 0x00,
+};
+
+struct IAPGetPlaylistInfoPayload {
+    uint8_t  info_type; /* IAPPlaylistInfoType */
+    uint32_t index;
+} __attribute__((packed));
+
+struct IAPRetPlaylistInfoPayload {
+    uint8_t info_type; /* IAPPlaylistInfoType */
+    uint8_t data;      /* TODO: add definitions */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/repeat.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/repeat.h
new file mode 100644
index 0000000000..619e5dacef
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/repeat.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.434 5.1.44 Command 0x0030: ReturnRepeat */
+
+struct IAPReturnRepeatPayload {
+    uint8_t mode; /* IAPIPodStateRepeaetSettingState */
+};
+
+struct IAPSetRepeatPayload {
+    uint8_t mode;            /* IAPIPodStateRepeatSettingState */
+    uint8_t restore_on_exit; /* optional */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/set-display-image.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/set-display-image.h
new file mode 100644
index 0000000000..4687fe8f35
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/set-display-image.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.435 5.1.46 Command 0x0032: SetDisplayImage */
+
+struct IAPSetDisplayImageFirstPayload {
+    uint16_t index;        /* = 0x0000 */
+    uint8_t  pixel_format; /* IAPArtworkPixelFormats */
+    uint16_t pixel_width;
+    uint16_t pixel_height;
+    uint32_t stride;
+    uint8_t  data[];
+} __attribute__((packed));
+
+struct IAPSetDisplayImageSubsequenctPayload {
+    uint16_t index; /* > 0x0000 */
+    uint8_t  data[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/shuffle.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/shuffle.h
new file mode 100644
index 0000000000..f94a0e6e1e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/shuffle.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.431 5.1.41 Command 0x002D: ReturnShuffle */
+
+struct IAPReturnShufflePayload {
+    uint8_t mode; /* IAPIPodStateShuffleSettingState */
+};
+
+struct IAPSetShufflePayload {
+    uint8_t mode;            /* IAPIPodStateShuffleSettingState */
+    uint8_t restore_on_exit; /* optional */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/track-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/track-info.h
new file mode 100644
index 0000000000..2eab33ed67
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/track-info.h
@@ -0,0 +1,59 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.446 5.1.57 Command 0x003E: GetUIDTrackInfo */
+
+enum IAPTrakcInfoTypeBits {
+    /* mask[0] */
+    IAPTrakcInfoTypeBits_Caps                   = 1 << 0,
+    IAPTrakcInfoTypeBits_TrackName              = 1 << 1,
+    IAPTrakcInfoTypeBits_ArtistName             = 1 << 2,
+    IAPTrakcInfoTypeBits_AlbumName              = 1 << 3,
+    IAPTrakcInfoTypeBits_GenreName              = 1 << 4,
+    IAPTrakcInfoTypeBits_ComposerName           = 1 << 5,
+    IAPTrakcInfoTypeBits_TotalTrackTimeDuration = 1 << 6,
+    IAPTrakcInfoTypeBits_UniqueTrackID          = 1 << 7,
+    /* mask[1] */
+    IAPTrakcInfoTypeBits_ChapterCount      = 1 << 0,
+    IAPTrakcInfoTypeBits_ChapterTimes      = 1 << 1,
+    IAPTrakcInfoTypeBits_ChapterNames      = 1 << 2,
+    IAPTrakcInfoTypeBits_Lyrics            = 1 << 3,
+    IAPTrakcInfoTypeBits_Description       = 1 << 4,
+    IAPTrakcInfoTypeBits_AlbumTrackIndex   = 1 << 5,
+    IAPTrakcInfoTypeBits_DiscSetAlbumIndex = 1 << 6,
+    IAPTrakcInfoTypeBits_PlayCount         = 1 << 7,
+    /* mask[2] */
+    IAPTrakcInfoTypeBits_SkipCount          = 1 << 0,
+    IAPTrakcInfoTypeBits_PodcastReleaseDate = 1 << 1,
+    IAPTrakcInfoTypeBits_LastPlayedDate     = 1 << 2,
+    IAPTrakcInfoTypeBits_Year               = 1 << 3,
+    IAPTrakcInfoTypeBits_StarRating         = 1 << 4,
+    IAPTrakcInfoTypeBits_SeriesName         = 1 << 5,
+    IAPTrakcInfoTypeBits_SeasonNumber       = 1 << 6,
+    IAPTrakcInfoTypeBits_TrackVolumeAdjust  = 1 << 7,
+    /* mask[3] */
+    IAPTrakcInfoTypeBits_TrackEQPreset       = 1 << 0,
+    IAPTrakcInfoTypeBits_TrackDataRate       = 1 << 1,
+    IAPTrakcInfoTypeBits_BookmarkOffset      = 1 << 2,
+    IAPTrakcInfoTypeBits_StartStopTimeOffset = 1 << 3,
+
+};
+
+struct IAPGetUIDTrackInfoPayload {
+    uint8_t  uid[8];
+    uint32_t mask[]; /* IAPTrakcInfoTypeBits */
+} __attribute__((packed));
+
+struct IAPGetDBTrackInfoPayload {
+    uint8_t  database_index;
+    uint8_t  track_count;
+    uint32_t mask[]; /* IAPTrakcInfoTypeBits */
+} __attribute__((packed));
+
+struct IAPRetDBTrackInfoPayload {
+    uint8_t  playing_index;
+    uint8_t  track_count;
+    uint32_t mask[]; /* IAPTrakcInfoTypeBits */
+} __attribute__((packed));
+
+/* TODO: define IAPRet{UID,DB,PB}TrackInfoPayload, but who really needs this? */
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/uid-list.h b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/uid-list.h
new file mode 100644
index 0000000000..4947e70f56
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/extended-interface/uid-list.h
@@ -0,0 +1,15 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.457 5.1.68 Command 0x004A: PrepareUIDList */
+
+struct IAPPrepareUIDListPayload {
+    uint16_t current_sector;
+    uint16_t max_sectors;
+    uint8_t  uids[][8];
+} __attribute__((packed));
+
+struct IAPPlayPreparedUIDListPayload {
+    uint8_t reserved;
+    uint8_t uid[8];
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general.h b/firmware/usbstack/iap/libiap/spec/lingoes/general.h
new file mode 100644
index 0000000000..5bcae5276d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general.h
@@ -0,0 +1,112 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.119 Table 3-1 General lingo commands */
+enum IAPGeneralCommandID {
+    IAPGeneralCommandID_RequestIdentify                     = 0x00, /* 0.00, from dev, no payload */
+    IAPGeneralCommandID_Identify                            = 0x01, /* 0.00, from acc, identify.h, deprecated */
+    IAPGeneralCommandID_IPodAck                             = 0x02, /* 1.00, from dev, ipod-ack.h */
+    IAPGeneralCommandID_RequestExtendedInterfaceMode        = 0x03, /* 1.00, from acc, no payload */
+    IAPGeneralCommandID_ReturnExtendedInterfaceMode         = 0x04, /* 1.00, from dev, extended-interface-mode.h */
+    IAPGeneralCommandID_EnterExtendedInterfaceMode          = 0x05, /* 1.00, from acc, no payload, deprecated */
+    IAPGeneralCommandID_ExitExtendedInterfaceMode           = 0x06, /* 1.00, from acc, no payload */
+    IAPGeneralCommandID_RequestIPodName                     = 0x07, /* 1.00, from acc, no payload */
+    IAPGeneralCommandID_ReturnIPodName                      = 0x08, /* 1.00, from dev, ipod-name.h */
+    IAPGeneralCommandID_RequestIPodSoftwareVersion          = 0x09, /* 1.00, from acc, no payload */
+    IAPGeneralCommandID_ReturnIPodSoftwareVersion           = 0x0A, /* 1.00, from dev, ipod-software-version.h */
+    IAPGeneralCommandID_RequestIPodSerialNum                = 0x0B, /* 1.00, from acc, no payload */
+    IAPGeneralCommandID_ReturnIPodSerialNum                 = 0x0C, /* 1.00, from dev, ipod-serial-num.h */
+    IAPGeneralCommandID_RequestIPodModelNum                 = 0x0D, /* 1.00, from acc, deprecated */
+    IAPGeneralCommandID_ReturnIPodModelNum                  = 0x0E, /* 1.00, from dev, deprecated */
+    IAPGeneralCommandID_RequestLingoProtocolVersion         = 0x0F, /* 1.00, from acc, lingo-protocol-version.h */
+    IAPGeneralCommandID_ReturnLingoProtocolVersion          = 0x10, /* 1.00, from dev, lingo-protocol-version.h */
+    IAPGeneralCommandID_RequestTransportMaxPayloadSize      = 0x11, /* 1.09, from acc, no payload  */
+    IAPGeneralCommandID_ReturnTransportMaxPayloadSize       = 0x12, /* 1.09, from dev, transport-max-payload-size.h */
+    IAPGeneralCommandID_IdentifyDeviceLingoes               = 0x13, /* 1.01, from acc, identify-device-lingoes.h */
+    IAPGeneralCommandID_GetAccessoryAuthenticationInfo      = 0x14, /* 1.01, from dev, no payload */
+    IAPGeneralCommandID_RetAccessoryAuthenticationInfo      = 0x15, /* 1.01, from acc, acc-auth-info.h */
+    IAPGeneralCommandID_AckAccessoryAuthenticationInfo      = 0x16, /* 1.01, from dev, acc-auth-info.h */
+    IAPGeneralCommandID_GetAccessoryAuthenticationSignature = 0x17, /* 1.01, from dev, acc-auth-sig.h */
+    IAPGeneralCommandID_RetAccessoryAuthenticationSignature = 0x18, /* 1.01, from acc, acc-auth-sig.h */
+    IAPGeneralCommandID_AckAccessoryAuthenticationStatus    = 0x19, /* 1.01, from dev, acc-auth-sig.h*/
+    IAPGeneralCommandID_GetIPodAuthenticationInfo           = 0x1A, /* 1.01, from acc, no payload */
+    IAPGeneralCommandID_RetIPodAuthenticationInfo           = 0x1B, /* 1.01, from dev, ipod-auth-info.h */
+    IAPGeneralCommandID_AckIPodAuthenticationInfo           = 0x1C, /* 1.01, from acc, ipod-auth-info.h */
+    IAPGeneralCommandID_GetIPodAuthenticationSignature      = 0x1D, /* 1.01, from acc, ipod-auth-sig.h */
+    IAPGeneralCommandID_RetIPodAuthenticationSignature      = 0x1E, /* 1.01, from dev, ipod-auth-sig.h */
+    IAPGeneralCommandID_AckIPodAuthenticationStatus         = 0x1F, /* 1.01, from acc, ipod-auth-sig.h */
+    IAPGeneralCommandID_NotifyIPodStateChange               = 0x23, /* 1.02, from dev, notify-ipod-state-change.h */
+    IAPGeneralCommandID_GetIPodOptions                      = 0x24, /* 1.05, from acc, no payload */
+    IAPGeneralCommandID_RetIPodOptions                      = 0x25, /* 1.05, from dev, ipod-options.h */
+    IAPGeneralCommandID_GetAccessoryInfo                    = 0x27, /* 1.04, from dev, acc-info.h */
+    IAPGeneralCommandID_RetAccessoryInfo                    = 0x28, /* 1.04, from acc, acc-info.h */
+    IAPGeneralCommandID_GetIPodPreferences                  = 0x29, /* 1.05, from acc, ipod-preferences.h */
+    IAPGeneralCommandID_RetIPodPreferences                  = 0x2A, /* 1.05, from dev, ipod-preferences.h */
+    IAPGeneralCommandID_SetIPodPreferences                  = 0x2B, /* 1.05, from acc, ipod-preferences.h */
+    IAPGeneralCommandID_GetUIMode                           = 0x35, /* 1.09, from acc, no payload */
+    IAPGeneralCommandID_RetUIMode                           = 0x36, /* 1.09, from dev, ui-mode.h */
+    IAPGeneralCommandID_SetUIMode                           = 0x37, /* 1.09, from acc, ui-mode.h */
+    IAPGeneralCommandID_StartIDPS                           = 0x38, /* 1.09, from acc, no payload */
+    IAPGeneralCommandID_SetFIDTokenValues                   = 0x39, /* 1.09, from acc, fid-token-values.h */
+    IAPGeneralCommandID_AckFIDTokenValues                   = 0x3A, /* 1.09, from dev, fid-token-values.h */
+    IAPGeneralCommandID_EndIDPS                             = 0x3B, /* 1.09, from acc, end-idps.h */
+    IAPGeneralCommandID_IDPSStatus                          = 0x3C, /* 1.09, from dev, end-idps.h */
+    IAPGeneralCommandID_OpenDataSessionForProtocol          = 0x3F, /* 1.09, from dev, data-session.h */
+    IAPGeneralCommandID_CloseDataSession                    = 0x40, /* 1.09, from dev, data-session.h */
+    IAPGeneralCommandID_AccessoryAck                        = 0x41, /* 1.09, from acc, acc-ack.h */
+    IAPGeneralCommandID_AccessoryDataTransfer               = 0x42, /* 1.09, from acc, data-transfer.h */
+    IAPGeneralCommandID_IPodDataTransfer                    = 0x43, /* 1.09, from dev, data-transfer.h */
+    IAPGeneralCommandID_SetAccessoryStatusNotification      = 0x46, /* 1.09, from dev, acc-status-notification.h */
+    IAPGeneralCommandID_RetAccessoryStatusNotification      = 0x47, /* 1.09, from acc, acc-status-notification.h */
+    IAPGeneralCommandID_AccessoryStatusNotification         = 0x48, /* 1.09, from acc, acc-status-notification.h */
+    IAPGeneralCommandID_SetEventNotification                = 0x49, /* 1.09, from acc, event-notification.h */
+    IAPGeneralCommandID_IPodNotification                    = 0x4A, /* 1.09, from dev, ipod-notifiction.h */
+    IAPGeneralCommandID_GetIPodOptionsForLingo              = 0x4B, /* 1.09, from acc, ipod-options-for-lingo.h */
+    IAPGeneralCommandID_RetIPodOptionsForLingo              = 0x4C, /* 1.09, from dev, ipod-options-for-lingo.h */
+    IAPGeneralCommandID_GetEventNotification                = 0x4D, /* 1.09, from acc, no payload */
+    IAPGeneralCommandID_RetEventNotification                = 0x4E, /* 1.09, from dev, event-notification.h */
+    IAPGeneralCommandID_GetSupportedEventNotification       = 0x4F, /* 1.09, from acc, no payload */
+    IAPGeneralCommandID_CancelCommand                       = 0x50, /* 1.09, from acc, cancel-command.h */
+    IAPGeneralCommandID_RetSupportedEventNotification       = 0x51, /* 1.09, from dev, event-notification.h */
+    IAPGeneralCommandID_SetAvailableCurrent                 = 0x54, /* 1.09, from acc, set-available-current.h */
+    IAPGeneralCommandID_SetInternalBatteryChargingState     = 0x56, /* 1.09, from acc, set-internal-battery-charging-state.h */
+    IAPGeneralCommandID_RequestApplicationLaunch            = 0x64, /* 1.09, from acc, request-application-launch.h */
+    IAPGeneralCommandID_GetNowPlayingApplicationBundleName  = 0x65, /* 1.09, from acc, no payload */
+    IAPGeneralCommandID_RetNowPlayingApplicationBundleName  = 0x66, /* 1.09, from dev, now-playing-app-bundle-name.h */
+    IAPGeneralCommandID_GetLocalizationInfo                 = 0x67, /* 1.09, from acc, localization-info.h */
+    IAPGeneralCommandID_RetLocalizationInfo                 = 0x68, /* 1.09, from dev, localization-info.h */
+    IAPGeneralCommandID_RequestWiFiConnectionInfo           = 0x69, /* 1.09, from acc, no payload */
+    IAPGeneralCommandID_WiFiConnectionInfo                  = 0x6A, /* 1.09, from dev, wifi-connection-info.h */
+};
+
+#include "general/acc-ack.h"
+#include "general/acc-auth-info.h"
+#include "general/acc-auth-sig.h"
+#include "general/acc-info.h"
+#include "general/acc-status-notification.h"
+#include "general/cancel-command.h"
+#include "general/data-session.h"
+#include "general/data-transfer.h"
+#include "general/end-idps.h"
+#include "general/event-notification.h"
+#include "general/extended-interface-mode.h"
+#include "general/fid-token-values.h"
+#include "general/identify-device-lingoes.h"
+#include "general/identify.h"
+#include "general/ipod-ack.h"
+#include "general/ipod-auth-info.h"
+#include "general/ipod-auth-sig.h"
+#include "general/ipod-name.h"
+#include "general/ipod-notification.h"
+#include "general/ipod-options-for-lingo.h"
+#include "general/ipod-options.h"
+#include "general/ipod-preferences.h"
+#include "general/ipod-software-version.h"
+#include "general/lingo-protocol-version.h"
+#include "general/localization-info.h"
+#include "general/now-playing-app-bundle-name.h"
+#include "general/request-application-launch.h"
+#include "general/set-available-current.h"
+#include "general/transport-max-payload-size.h"
+#include "general/ui-mode.h"
+#include "general/wifi-connection-info.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-ack.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-ack.h
new file mode 100644
index 0000000000..3d9a4d91ef
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-ack.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPAccAckPayload {
+    uint8_t status; /* = IAPAckStatus_{Success,EBadParameter} */
+    uint8_t id;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-auth-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-auth-info.h
new file mode 100644
index 0000000000..2dd764e93e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-auth-info.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetAccAuthInfoPayload {
+    uint8_t protocol_major;
+    uint8_t protocol_minor;
+} __attribute__((packed));
+
+struct IAPRetAccAuthInfoPayload2p0 {
+    uint8_t protocol_major; /* = 0x02 */
+    uint8_t protocol_minor; /* = 0x00 */
+    uint8_t cert_current_section_index;
+    uint8_t cert_max_section_index;
+    uint8_t cert_data[];
+} __attribute__((packed));
+
+enum IAPAckAccAuthInfoStatus {
+    IAPAckAccAuthInfoStatus_Supported       = 0x00,
+    IAPAckAccAuthInfoStatus_CertTooLong     = 0x04,
+    IAPAckAccAuthInfoStatus_Unsupported     = 0x08,
+    IAPAckAccAuthInfoStatus_InvalidCert     = 0x0A,
+    IAPAckAccAuthInfoStatus_InvalidCertPerm = 0x0B,
+};
+
+struct IAPAckAccAuthInfoPayload {
+    uint8_t status; /* IAPAckAccAuthInfoStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-auth-sig.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-auth-sig.h
new file mode 100644
index 0000000000..9fd22d2598
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-auth-sig.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetAccAuthSigPayload1p0 {
+    uint8_t challenge[16];
+    uint8_t retry;
+} __attribute__((packed));
+
+struct IAPGetAccAuthSigPayload2p0 {
+    uint8_t challenge[20];
+    uint8_t retry;
+} __attribute__((packed));
+
+/*
+struct IAPRetAccAuthSigPayload {
+    uint8_t sig[];
+} __attribute__((packed));
+*/
+
+struct IAPAckAccAuthSigPayload {
+    uint8_t status; /* IAPAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-info.h
new file mode 100644
index 0000000000..6f91bee72e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-info.h
@@ -0,0 +1,83 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPAccInfoType {
+    IAPAccInfoType_AccInfoCaps              = 0x00, /* G1 -> R2 */
+    IAPAccInfoType_AccName                  = 0x01, /* G1 -> R1 */
+    IAPAccInfoType_MinDeviceFirmwareVersion = 0x02, /* G2 -> R3 */
+    IAPAccInfoType_MinLingoVersion          = 0x03, /* G3 -> R4 */
+    IAPAccInfoType_FirmwareVersion          = 0x04, /* G1 -> R5 */
+    IAPAccInfoType_HardwareVersion          = 0x05, /* G1 -> R5 */
+    IAPAccInfoType_Manufacture              = 0x06, /* G1 -> R2 */
+    IAPAccInfoType_ModelNumber              = 0x07, /* G1 -> R2 */
+    IAPAccInfoType_SerialNumber             = 0x08, /* G1 -> R2 */
+    IAPAccInfoType_MaxPayloadSize           = 0x09, /* G1 -> R6 */
+    IAPAccInfoType_SupportedStatusTypes     = 0x0B, /* G1 -> R7 */
+};
+
+/* G1 */
+struct IAPGetAccInfoPayload {
+    uint8_t type; /* IAPAccInfoType */
+} __attribute__((packed));
+
+/* G2 */
+struct IAPGetAccInfoMinDeviceFirmwareVersionPayload {
+    uint8_t  type; /* = IAPAccInfoType_MinDeviceFirmwareVersion */
+    uint32_t model_id;
+} __attribute__((packed));
+
+/* G3 */
+struct IAPGetAccInfoMinLingoVersionPayload {
+    uint8_t type; /* = IAPAccInfoType_MinLingoVersion */
+    uint8_t lingo_id;
+};
+
+/* R1 */
+struct IAPRetAccInfoPayload {
+    uint8_t type; /* IAPAccInfoType */
+    char    value[];
+};
+
+/* R2 */
+struct IAPRetAccInfoCapsPayload {
+    uint8_t  type; /* = IAPAccInfoType_AccInfoCaps */
+    uint32_t caps;
+} __attribute__((packed));
+
+/* R3 */
+struct IAPRetAccInfoMinDeviceFirmwareVersionPayload {
+    uint8_t type; /* = IAPAccInfoType_MinDeviceFirmwareVersion */
+};
+
+/* R4 */
+struct IAPRetAccInfoMinLingoVersionPayload {
+    uint8_t type; /* = IAPAccInfoType_MinLingoVersion */
+    uint8_t lingo_id;
+    uint8_t major;
+    uint8_t minor;
+};
+
+/* R5 */
+struct IAPRetAccInfoFirmHardVersionPayload {
+    uint8_t type; /* = IAPAccInfoType_{Firmware,Hardware}Version */
+    uint8_t major;
+    uint8_t minor;
+    uint8_t revision;
+} __attribute__((packed));
+
+/* R6 */
+struct IAPRetAccInfoMaxPayloadSizePayload {
+    uint8_t  type; /* = IAPAccInfoType_MaxPayloadSize */
+    uint16_t max_payload_size;
+} __attribute__((packed));
+
+enum IAPAccInfoStatusTypes {
+    IAPAccInfoStatusTypes_Bluetooth      = 0b0010,
+    IAPAccInfoStatusTypes_FaultCondition = 0b0100,
+};
+
+/* R7 */
+struct IAPRetAccInfoSupportedStatusTypes {
+    uint8_t  type;         /* = IAPAccInfoType_SupportedStatusTypes */
+    uint32_t status_types; /* IAPAccInfoStatusTypes */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-status-notification.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-status-notification.h
new file mode 100644
index 0000000000..501d6d2ce6
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/acc-status-notification.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPSetAccStatusNotificationPayload {
+    uint32_t mask; /* IAPAccInfoStatusTypes */
+} __attribute__((packed));
+
+struct IAPRetAccStatusNotificationPayload {
+    uint32_t mask; /* IAPAccInfoStatusTypes */
+} __attribute__((packed));
+
+struct IAPAccStatusNotificationPayload {
+    uint32_t type; /* IAPAccInfoStatusTypes */
+    uint8_t  params[];
+} __attribute__((packed));
+
+/* [1] P.181 Table 3-108 AccessoryStatusNotification parameters */
+/* TODO: add definitions */
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/cancel-command.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/cancel-command.h
new file mode 100644
index 0000000000..af10443935
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/cancel-command.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPCancelCommandPayload {
+    uint8_t  lingo_id;
+    uint16_t command_id;
+    uint16_t transaction_id;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/data-session.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/data-session.h
new file mode 100644
index 0000000000..1367987cd6
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/data-session.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPOpenDataSessionForProtocolPayload {
+    uint16_t session_id;
+    uint8_t protocol_index;
+} __attribute__((packed));
+
+struct IAPCloseDataSessionPayload {
+    uint16_t session_id;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/data-transfer.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/data-transfer.h
new file mode 100644
index 0000000000..73cfa522a1
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/data-transfer.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPAccDataTransferPayload {
+    uint16_t session_id;
+    uint8_t  data[];
+} __attribute__((packed));
+
+struct IAPIPodDataTransferPayload {
+    uint16_t session_id;
+    uint8_t  data[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/end-idps.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/end-idps.h
new file mode 100644
index 0000000000..c9c7fe41e1
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/end-idps.h
@@ -0,0 +1,33 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPEndIDPSStatus {
+    IAPEndIDPSStatus_Success          = 0x00,
+    IAPEndIDPSStatus_Reset            = 0x01,
+    IAPEndIDPSStatus_Abort            = 0x02,
+    IAPEndIDPSStatus_AnotherTransport = 0x03,
+};
+
+struct IAPEndIDPSPayload {
+    uint8_t status; /* IAPEndIDPSStatus */
+};
+
+enum IAPIDPSStatus {
+    /* IAPEndIDPSStatus_Success */
+    IAPIDPSStatus_Success                      = 0x00,
+    IAPIDPSStatus_RequiredTokenRejected        = 0x01,
+    IAPIDPSStatus_RequiredTokenMissing         = 0x02,
+    IAPIDPSStatus_RequiredTokenRejectedMissing = 0x03,
+    /* IAPEndIDPSStatus_Reset */
+    IAPIDPSStatus_NotTimedOut = 0x04,
+    IAPIDPSStatus_TimedOut    = 0x05,
+    /* IAPEndIDPSStatus_Abort */
+    IAPIDPSStatus_Aborted = 0x06,
+    /* IAPEndIDPSStatus_AnotherTransport */
+    IAPIDPSStatus_Failed = 0x07,
+
+};
+
+struct IAPIDPSStatusPayload {
+    uint8_t status; /* IAPIDPSStatus */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/event-notification.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/event-notification.h
new file mode 100644
index 0000000000..d6ac7f1505
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/event-notification.h
@@ -0,0 +1,31 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.184 Table 3-112 Notification bitmask bits */
+
+enum IAPEventNotificationEvents {
+    IAPSetEventNotificationEvents_FlowControl         = 1 << 2,
+    IAPSetEventNotificationEvents_RadioTagging        = 1 << 3,
+    IAPSetEventNotificationEvents_Camera              = 1 << 4,
+    IAPSetEventNotificationEvents_ChargingInfo        = 1 << 5,
+    IAPSetEventNotificationEvents_DatabaseChanged     = 1 << 9,
+    IAPSetEventNotificationEvents_AppBundleName       = 1 << 10,
+    IAPSetEventNotificationEvents_SessionSpaceAvail   = 1 << 11,
+    IAPSetEventNotificationEvents_CommandComplete     = 1 << 13,
+    IAPSetEventNotificationEvents_IPodOutMode         = 1 << 15,
+    IAPSetEventNotificationEvents_BluetoothConnection = 1 << 17,
+    IAPSetEventNotificationEvents_AppDisplayName      = 1 << 19,
+    IAPSetEventNotificationEvents_AssistiveTouch      = 1 << 20,
+};
+
+struct IAPSetEventNotificationPayload {
+    uint64_t mask; /* IAPEventNotificationEvents */
+} __attribute__((packed));
+
+struct IAPRetEventNotificationPayload {
+    uint64_t mask; /* IAPEventNotificationEvents */
+} __attribute__((packed));
+
+struct IAPRetSupportedEventNotificationPayload {
+    uint64_t mask; /* IAPEventNotificationEvents */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/extended-interface-mode.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/extended-interface-mode.h
new file mode 100644
index 0000000000..5a19804e26
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/extended-interface-mode.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.127 3.3.4 Command 0x04: ReturnExtendedInterfaceMode */
+
+struct IAPReturnExtendedInterfaceModePayload {
+    uint8_t is_ext_mode;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values.h
new file mode 100644
index 0000000000..01bbf6ffbd
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values.h
@@ -0,0 +1,58 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.161 Table 3-67 FIDTokenValues tokens */
+/* = 0x{type}{subtype} */
+enum IAPFIDTokenTypes {
+    IAPFIDTokenTypes_Identify                   = 0x0000,
+    IAPFIDTokenTypes_AccCaps                    = 0x0001,
+    IAPFIDTokenTypes_AccInfo                    = 0x0002,
+    IAPFIDTokenTypes_IPodPreference             = 0x0003,
+    IAPFIDTokenTypes_EAProtocol                 = 0x0004,
+    IAPFIDTokenTypes_BundleSeedIDPref           = 0x0005,
+    IAPFIDTokenTypes_ScreenInfo                 = 0x0007,
+    IAPFIDTokenTypes_EAProtocolMetadata         = 0x0008,
+    IAPFIDTokenTypes_AccDigitalAudioSampleRates = 0x000E,
+    IAPFIDTokenTypes_AccDigitalAudioVideoDelay  = 0x000F,
+    IAPFIDTokenTypes_MicrophoneCaps             = 0x0100,
+};
+
+/* [1] P.161 Table 3-66 FIDTokenValues field format */
+struct IAPFIDTokenValuesToken {
+    uint8_t length;
+    uint8_t type;
+    uint8_t subtype;
+    uint8_t data[];
+} __attribute__((packed));
+
+/* [1] P.170 Table 3-86 Acknowledgment status codes */
+enum IAPFIDTokenValuesAckStatus {
+    IAPFIDTokenValuesIdentifyAckStatus_Accepted       = 0x00,
+    IAPFIDTokenValuesIdentifyAckStatus_RequiredFailed = 0x01,
+    IAPFIDTokenValuesIdentifyAckStatus_OptionalFailed = 0x02,
+    IAPFIDTokenValuesIdentifyAckStatus_NotSupported   = 0x03,
+    IAPFIDTokenValuesIdentifyAckStatus_LingoBusy      = 0x04,
+    IAPFIDTokenValuesIdentifyAckStatus_MaxConnections = 0x05,
+};
+
+#include "fid-token-values/acc-caps.h"
+#include "fid-token-values/acc-digital-audio-sample-rates.h"
+#include "fid-token-values/acc-digital-audio-video-delay.h"
+#include "fid-token-values/acc-info.h"
+#include "fid-token-values/bundle-seed-id-pref.h"
+#include "fid-token-values/ea-protocol-metadata.h"
+#include "fid-token-values/ea-protocol.h"
+#include "fid-token-values/identify.h"
+#include "fid-token-values/ipod-preference.h"
+#include "fid-token-values/microphone-caps.h"
+#include "fid-token-values/screen-info.h"
+
+struct IAPSetFIDTokenValuesPayload {
+    uint8_t num_token_values;
+    uint8_t data[]; /* packed token values */
+};
+
+struct IAPAckFIDTokenValuesPayload {
+    uint8_t num_token_value_acks;
+    uint8_t data[]; /* packed token value acks */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-caps.h
new file mode 100644
index 0000000000..3a86700594
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-caps.h
@@ -0,0 +1,31 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.164 Table 3-71 Accessory capabilities bit values */
+enum IAPFIDTokenValuesAccCapsBits {
+    IAPFIDTokenValuesAccCapsBits_LineOut             = 1 << 0,
+    IAPFIDTokenValuesAccCapsBits_LineIn              = 1 << 1,
+    IAPFIDTokenValuesAccCapsBits_VideoOut            = 1 << 2,
+    IAPFIDTokenValuesAccCapsBits_USBAudioOut         = 1 << 4,
+    IAPFIDTokenValuesAccCapsBits_AppsCommunication   = 1 << 9,
+    IAPFIDTokenValuesAccCapsBits_CheckVolume         = 1 << 11,
+    IAPFIDTokenValuesAccCapsBits_VoiceOver           = 1 << 17,
+    IAPFIDTokenValuesAccCapsBits_AsyncPlaybackChange = 1 << 18,
+    IAPFIDTokenValuesAccCapsBits_MixedResponse       = 1 << 19,
+    IAPFIDTokenValuesAccCapsBits_AudioRoutingSwitch  = 1 << 21,
+    IAPFIDTokenValuesAccCapsBits_AssistiveTouch      = 1 << 23,
+};
+
+struct IAPFIDTokenValuesAccCapsToken {
+    uint8_t  length;    /* = 0x0A */
+    uint8_t  type;      /* = 0x00 */
+    uint8_t  subtype;   /* = 0x01 */
+    uint64_t caps_bits; /* IAPFIDTokenValuesAccCapsBits */
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesAccCapsAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x01 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-digital-audio-sample-rates.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-digital-audio-sample-rates.h
new file mode 100644
index 0000000000..e9ee239aaa
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-digital-audio-sample-rates.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPFIDTokenValuesAccDigitalAudioSampleRatesToken {
+    uint8_t  length;
+    uint8_t  type;    /* = 0x00 */
+    uint8_t  subtype; /* = 0x0E */
+    uint32_t sample_rates[];
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesAccDigitalAudioSampleRatesAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x0E */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-digital-audio-video-delay.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-digital-audio-video-delay.h
new file mode 100644
index 0000000000..b658d55c5f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-digital-audio-video-delay.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPFIDTokenValuesAccDigitalAudioVideoDelayToken {
+    uint8_t  length;  /* = 0x06 */
+    uint8_t  type;    /* = 0x00 */
+    uint8_t  subtype; /* = 0x0F */
+    uint32_t delay;
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesAccDigitalAudioVideoDelayAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x0F */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-info.h
new file mode 100644
index 0000000000..94926ae7d9
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/acc-info.h
@@ -0,0 +1,33 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.165 Table 3-73 Accessory Info Type values */
+enum IAPFIDTokenValuesAccInfoTypes {
+    IAPFIDTokenValuesAccInfoTypes_AccName         = 0x01,
+    IAPFIDTokenValuesAccInfoTypes_FirmwareVersion = 0x04,
+    IAPFIDTokenValuesAccInfoTypes_HardwareVersion = 0x05,
+    IAPFIDTokenValuesAccInfoTypes_Manufacture     = 0x06,
+    IAPFIDTokenValuesAccInfoTypes_ModelNumber     = 0x07,
+    IAPFIDTokenValuesAccInfoTypes_SerialNumber    = 0x08,
+    IAPFIDTokenValuesAccInfoTypes_MaxPayloadSize  = 0x09,
+    IAPFIDTokenValuesAccInfoTypes_AccStatus       = 0x0B,
+    IAPFIDTokenValuesAccInfoTypes_RFCerts         = 0x0C,
+};
+
+struct IAPFIDTokenValuesAccInfoToken {
+    uint8_t length;
+    uint8_t type;      /* = 0x00 */
+    uint8_t subtype;   /* = 0x02 */
+    uint8_t info_type; /* IAPFIDTokenValuesAccInfoTypes */
+    uint8_t info[];
+} __attribute__((packed));
+
+/* TODO: add info definitions */
+
+struct IAPFIDTokenValuesAccInfoAck {
+    uint8_t length;    /* = 0x04 */
+    uint8_t type;      /* = 0x00 */
+    uint8_t subtype;   /* = 0x02 */
+    uint8_t status;    /* IAPFIDTokenValuesAckStatus */
+    uint8_t info_type; /* IAPFIDTokenValuesAccInfoTypes */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/bundle-seed-id-pref.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/bundle-seed-id-pref.h
new file mode 100644
index 0000000000..144ebc3ac8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/bundle-seed-id-pref.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPFIDTokenValuesBundleSeedIDPrefToken {
+    uint8_t length;  /* = 0x0D */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x05 */
+    char    bundle_seed_id_string[11];
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesBundleSeedIDPrefAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x05 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ea-protocol-metadata.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ea-protocol-metadata.h
new file mode 100644
index 0000000000..7ec88e97b7
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ea-protocol-metadata.h
@@ -0,0 +1,23 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPFIDTokenValuesEAProtocolMetadataTypes {
+    IAPFIDTokenValuesEAProtocolMetadataTypes_DontFindApp               = 0x00,
+    IAPFIDTokenValuesEAProtocolMetadataTypes_FindApp                   = 0x01,
+    IAPFIDTokenValuesEAProtocolMetadataTypes_DontFindButDisplayMessage = 0x03,
+};
+
+struct IAPFIDTokenValuesEAProtocolMetadataToken {
+    uint8_t length;  /* = 0x04 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x08 */
+    uint8_t protocol_index;
+    uint8_t metadata_type; /* IAPFIDTokenValuesEAProtocolMetadataTypes */
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesEAProtocolMetadataAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x08 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ea-protocol.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ea-protocol.h
new file mode 100644
index 0000000000..20558ea7d3
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ea-protocol.h
@@ -0,0 +1,20 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.167 Table 3-76 EAProtocolToken format */
+struct IAPFIDTokenValuesEAProtocolToken {
+    uint8_t length;
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x04 */
+    uint8_t protocol_index;
+    char    protocol_string[];
+} __attribute__((packed));
+
+/* [1] P.171 Table 3-90 Acknowledgment format for EAProtocolToken */
+struct IAPFIDTokenValuesEAProtocolAck {
+    uint8_t length;  /* = 0x04 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x04 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+    uint8_t protocol_index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/identify.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/identify.h
new file mode 100644
index 0000000000..8779774b28
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/identify.h
@@ -0,0 +1,28 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPFIDTokenValuesIdentifyTokenHead {
+    uint8_t length;
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x00 */
+    uint8_t num_lingoes;
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesIdentifyTokenTail {
+    uint32_t device_option; /* = 0b10 */
+    uint32_t device_id;
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesIdentifyToken {
+    struct IAPFIDTokenValuesIdentifyTokenHead head;
+    /*uint8_t lingoes[num_lingoes]; */
+    struct IAPFIDTokenValuesIdentifyTokenHead tail;
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesIdentifyAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x00 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+    uint8_t lingo_ids[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ipod-preference.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ipod-preference.h
new file mode 100644
index 0000000000..722cc1db11
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/ipod-preference.h
@@ -0,0 +1,21 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.166 Table 3-75 iPodPreferenceToken format */
+struct IAPFIDTokenValuesIPodPreferenceToken {
+    uint8_t length;   /* = 0x05 */
+    uint8_t type;     /* = 0x00 */
+    uint8_t subtype;  /* = 0x03 */
+    uint8_t class_id; /* IAPIPodPereferenceClassID */
+    uint8_t setting_id;
+    uint8_t restore_on_exit; /* = 0x01 */
+} __attribute__((packed));
+
+/* [1] P.171 Table 3-89 Acknowledgment format for ipodPreferenceToken */
+struct IAPFIDTokenValuesIPodPreferenceAck {
+    uint8_t length;   /* = 0x04 */
+    uint8_t type;     /* = 0x00 */
+    uint8_t subtype;  /* = 0x03 */
+    uint8_t status;   /* IAPFIDTokenValuesAckStatus */
+    uint8_t class_id; /* IAPIPodPereferenceClassID */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/microphone-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/microphone-caps.h
new file mode 100644
index 0000000000..947f7852b6
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/microphone-caps.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPFIDTokenValuesMicrophoneCaps {
+    IAPFIDTokenValuesMicrophoneCaps_StereoInput           = 1 << 0,
+    IAPFIDTokenValuesMicrophoneCaps_StereoMonoInputSwitch = 1 << 1,
+    IAPFIDTokenValuesMicrophoneCaps_VariableRecordLevel   = 1 << 2,
+    IAPFIDTokenValuesMicrophoneCaps_RecordLevelLimit      = 1 << 3,
+    IAPFIDTokenValuesMicrophoneCaps_DuplexAudio           = 1 << 4,
+};
+
+struct IAPFIDTokenValuesMicrophoneCapsToken {
+    uint8_t  length;    /* = 0x06 */
+    uint8_t  type;      /* = 0x01 */
+    uint8_t  subtype;   /* = 0x00 */
+    uint32_t caps_bits; /* IAPFIDTokenValuesMicrophoneCaps */
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesMicrophoneCapsAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x01 */
+    uint8_t subtype; /* = 0x00 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/screen-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/screen-info.h
new file mode 100644
index 0000000000..a05c8b0faf
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/fid-token-values/screen-info.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPFIDTokenValuesScreenInfoFeatureBits {
+    IAPFIDTokenValuesScreenInfoFeatureBits_ColorDisplay = 1 << 0,
+};
+
+struct IAPFIDTokenValuesScreenInfoToken {
+    uint8_t  length;  /* = 0x10 */
+    uint8_t  type;    /* = 0x00 */
+    uint8_t  subtype; /* = 0x07 */
+    uint16_t total_screen_width_inches;
+    uint16_t total_screen_height_inches;
+    uint16_t total_screen_width_pixels;
+    uint16_t total_screen_height_pixels;
+    uint16_t ipod_out_screen_width_pixels;
+    uint16_t ipod_out_screen_height_pixels;
+    uint8_t  screen_feature_mask; /* IAPFIDTokenValuesScreenInfoFeatureBits */
+    uint8_t  screen_gamma_value;
+} __attribute__((packed));
+
+struct IAPFIDTokenValuesScreenInfoAck {
+    uint8_t length;  /* = 0x03 */
+    uint8_t type;    /* = 0x00 */
+    uint8_t subtype; /* = 0x07 */
+    uint8_t status;  /* IAPFIDTokenValuesAckStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/identify-device-lingoes.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/identify-device-lingoes.h
new file mode 100644
index 0000000000..d2c36955e3
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/identify-device-lingoes.h
@@ -0,0 +1,30 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.134 Table 3-24 Device Lingoes Spoken bits */
+enum IAPIdentifyDeviceLingoesLingoBits {
+    IAPIdentifyDeviceLingoesLingoBits_General            = 1 << 0,
+    IAPIdentifyDeviceLingoesLingoBits_SimpleRemote       = 1 << 2,
+    IAPIdentifyDeviceLingoesLingoBits_DisplayRemote      = 1 << 3,
+    IAPIdentifyDeviceLingoesLingoBits_ExtendedInterface  = 1 << 4,
+    IAPIdentifyDeviceLingoesLingoBits_AccessoryPower     = 1 << 5,
+    IAPIdentifyDeviceLingoesLingoBits_USBHostMode        = 1 << 6,
+    IAPIdentifyDeviceLingoesLingoBits_RFTuner            = 1 << 7,
+    IAPIdentifyDeviceLingoesLingoBits_AccessoryEqualizer = 1 << 8,
+    IAPIdentifyDeviceLingoesLingoBits_Sports             = 1 << 9,
+    IAPIdentifyDeviceLingoesLingoBits_DigitalAudio       = 1 << 10,
+    IAPIdentifyDeviceLingoesLingoBits_Storage            = 1 << 12,
+};
+
+/* [1] P.135 Table 3-25 IdentifyDeviceLingoes Options bits */
+enum IAPIdentifyDeviceLingoesOptions {
+    IAPIdentifyDeviceLingoesOptions_NoAuth        = 0b00,
+    IAPIdentifyDeviceLingoesOptions_DeferAuth     = 0b01,
+    IAPIdentifyDeviceLingoesOptions_ImmediateAuth = 0b10,
+};
+
+struct IAPIdentifyDeviceLingoesPayload {
+    uint32_t lingoes_bits; /* IAPIdentifyDeviceLingoesLingoBits */
+    uint32_t options;      /* IAPIdentifyDeviceLingoesOptions */
+    uint32_t device_id;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/identify.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/identify.h
new file mode 100644
index 0000000000..ef469fffa6
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/identify.h
@@ -0,0 +1,15 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.531 Table C-8 Identify packet */
+struct IAPIdentifyPayload {
+    uint8_t supported_lingo;
+};
+
+/* [1] P.531 Table C-9 Identify packet for high-power accessories */
+struct IAPIdentifyHighPowerPayload {
+    uint8_t supported_lingo; /* = IAPLingoID_AccessoryPower */
+    uint8_t reserved;        /* = 0x00 */
+    uint8_t num_valid_bits;  /* = 0x02 */
+    uint8_t option_flags;    /* TODO: add bitfield definition */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-ack.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-ack.h
new file mode 100644
index 0000000000..3dfe657a64
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-ack.h
@@ -0,0 +1,57 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.124 3.3.2 Command 0x02: iPodAck */
+
+enum IAPAckStatus {
+    IAPAckStatus_Success                     = 0x00,
+    IAPAckStatus_EUnknownDatabaseOrSessionID = 0x01,
+    IAPAckStatus_ECommandFailed              = 0x02,
+    IAPAckStatus_EOutOfResource              = 0x03,
+    IAPAckStatus_EBadParameter               = 0x04,
+    IAPAckStatus_EUnknownID                  = 0x05,
+    IAPAckStatus_CommandPending              = 0x06,
+    IAPAckStatus_ENotAuthenticated           = 0x07,
+    IAPAckStatus_EBadAuthVersion             = 0x08,
+    IAPAckStatus_EAccPowerModeRequestFailed  = 0x09,
+    IAPAckStatus_EInvalidCert                = 0x0A,
+    IAPAckStatus_EInvalidCertPerm            = 0x0B,
+    IAPAckStatus_EFileInUse                  = 0x0C,
+    IAPAckStatus_EInvalidFileHandle          = 0x0D,
+    IAPAckStatus_EDirectoryNotEmpty          = 0x0E,
+    IAPAckStatus_EOperationTimedOut          = 0x0F,
+    IAPAckStatus_ECommandUnavailable         = 0x10,
+    IAPAckStatus_EBadWire                    = 0x11,
+    IAPAckStatus_ESelectionNotGenius         = 0x12,
+    IAPAckStatus_MultiDataRecvSuccess        = 0x13,
+    IAPAckStatus_ELingoBusy                  = 0x14,
+    IAPAckStatus_EMaxNumAcc                  = 0x15,
+    IAPAckStatus_HIDIndexInUse               = 0x16,
+    IAPAckStatus_EDropped                    = 0x17,
+    IAPAckStatus_EInvalidVideoSettings       = 0x18,
+};
+
+struct IAPIPodAckPayload {
+    uint8_t status; /* IAPAckStatus */
+    uint8_t id;
+} __attribute__((packed));
+
+struct IAPIPodAckCommandPendingPayload {
+    uint8_t  status; /* = IAPAckStatus_CommandPending */
+    uint8_t  id;
+    uint32_t time_ms;
+} __attribute__((packed));
+
+/* [1] P.389 Table 4-292 Multisection iPodAck packet */
+struct IAPIPodAckMultiSectPayload {
+    uint8_t  status; /* IAPAckStatus_IAPAckStatus_MultiDataRecvSuccess */
+    uint8_t  id;
+    uint16_t sect_cur;
+};
+
+struct IAPIPodAckEDroppedPayload {
+    uint8_t  status; /* = IAPAckStatus_EDropped */
+    uint8_t  id;     /* = IAPCommandIDGeneral_AccessoryDataTransfer */
+    uint16_t session_id;
+    uint32_t num_dropped_bytes;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-auth-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-auth-info.h
new file mode 100644
index 0000000000..de03274418
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-auth-info.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetIPodAuthInfoPayload2p0 {
+    uint8_t protocol_major; /* = 0x02 */
+    uint8_t protocol_minor; /* = 0x00 */
+    uint8_t cert_current_section_index;
+    uint8_t cert_max_section_index;
+    uint8_t cert_data[];
+} __attribute__((packed));
+
+struct IAPAckIPodAuthInfoPayload {
+    uint8_t status;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-auth-sig.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-auth-sig.h
new file mode 100644
index 0000000000..977b299790
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-auth-sig.h
@@ -0,0 +1,17 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetIPodAuthSigPayload2p0 {
+    uint8_t challenge[20];
+    uint8_t retry;
+} __attribute__((packed));
+
+/*
+struct IAPRetIPodAuthSigPayload {
+    uint8_t sig[];
+} __attribute__((packed));
+*/
+
+struct IAPAckIPodAuthSigPayload {
+    uint8_t status;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-name.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-name.h
new file mode 100644
index 0000000000..7cedcedeff
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-name.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/*
+struct IAPReturnIPodNamePayload {
+    char name[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-notification.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-notification.h
new file mode 100644
index 0000000000..2674c4867e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-notification.h
@@ -0,0 +1,120 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPIPodNotificationType {
+    IAPIPodNotificationType_FlowControl         = 2,
+    IAPIPodNotificationType_RadioTagging        = 3,
+    IAPIPodNotificationType_Camera              = 4,
+    IAPIPodNotificationType_ChargingInfo        = 5,
+    IAPIPodNotificationType_DatabaseChanged     = 9,
+    IAPIPodNotificationType_AppBundleName       = 10,
+    IAPIPodNotificationType_SessionSpaceAvail   = 11,
+    IAPIPodNotificationType_CommandComplete     = 13,
+    IAPIPodNotificationType_IPodOutMode         = 15,
+    IAPIPodNotificationType_BluetoothConnection = 17,
+    IAPIPodNotificationType_AppDisplayName      = 19,
+    IAPIPodNotificationType_AssistiveTouch      = 20,
+};
+
+struct IAPIPodNotificationPayload {
+    uint8_t type; /* IAPIPodNotificationType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPIPodNotificationFlowControlPayload {
+    uint8_t  type; /* = IAPIPodNotificationType_FlowControl */
+    uint32_t wait_time_ms;
+    uint16_t overflow_transaction_id;
+} __attribute__((packed));
+
+enum IAPIPodNotificationRadioTaggingStatus {
+    IAPIPodNotificationRadioTaggingStatus_Success      = 0x00,
+    IAPIPodNotificationRadioTaggingStatus_Failed       = 0x01,
+    IAPIPodNotificationRadioTaggingStatus_InfoAvail    = 0x02,
+    IAPIPodNotificationRadioTaggingStatus_InfoNotAvail = 0x03,
+};
+
+struct IAPIPodNotificationRadioTaggingPayload {
+    uint8_t type;   /* = IAPIPodNotificationType_RadioTagging */
+    uint8_t status; /* IAPIPodNotificationRadioTaggingStatus */
+} __attribute__((packed));
+
+enum IAPIPodNotificationCameraStatus {
+    IAPIPodNotificationCameraStatus_AppOff    = 0x00,
+    IAPIPodNotificationCameraStatus_Preview   = 0x03,
+    IAPIPodNotificationCameraStatus_Recording = 0x04,
+};
+
+struct IAPIPodNotificationCameraPayload {
+    uint8_t type;   /* = IAPIPodNotificationType_Camera */
+    uint8_t status; /* IAPIPodNotificationCameraStatus */
+} __attribute__((packed));
+
+enum IAPIPodNotificationChargingInfoType {
+    IAPIPodNotificationChargingInfoType_AvailCurrent = 0x00,
+};
+
+struct IAPIPodNotificationChargingInfoPayload {
+    uint8_t  type;      /* = IAPIPodNotificationType_ChargingInfo */
+    uint8_t  info_type; /* IAPIPodNotificationChargingInfoType */
+    uint16_t info_value;
+} __attribute__((packed));
+
+struct IAPIPodNotificationDatabaseChangedPayload {
+    uint8_t type; /* = IAPIPodNotificationType_DatabaseChanged */
+} __attribute__((packed));
+
+struct IAPIPodNotificationAppBundleNamePayload {
+    uint8_t type; /* = IAPIPodNotificationType_AppBundleName */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPIPodNotificationSessionSpaceAvailPayload {
+    uint8_t  type; /* = IAPIPodNotificationType_SessionSpaceAvail */
+    uint16_t session_id;
+} __attribute__((packed));
+
+enum IAPIPodNotificationCommandCompleteStatus {
+    IAPIPodNotificationCommandCompleteStatus_Success   = 0x00,
+    IAPIPodNotificationCommandCompleteStatus_Failed    = 0x01,
+    IAPIPodNotificationCommandCompleteStatus_Cancelled = 0x02,
+};
+
+struct IAPIPodNotificationCommandCompletePayload {
+    uint8_t  type; /* = IAPIPodNotificationType_CommandComplete */
+    uint8_t  lingo_id;
+    uint16_t command_id;
+    uint8_t  status; /* IAPIPodNotificationCommandCompleteStatus */
+} __attribute__((packed));
+
+struct IAPIPodNotificationIPodOutModePayload {
+    uint8_t type; /* = IAPIPodNotificationType_IPodOutMode */
+    uint8_t is_active;
+} __attribute__((packed));
+
+enum IAPIPodNotificationBluetoothConnectionStatusBits {
+    IAPIPodNotificationBluetoothConnectionStatusBits_HFP     = 1 << 0,
+    IAPIPodNotificationBluetoothConnectionStatusBits_PBAP    = 1 << 1,
+    IAPIPodNotificationBluetoothConnectionStatusBits_AVRCP   = 1 << 3,
+    IAPIPodNotificationBluetoothConnectionStatusBits_A2DP    = 1 << 4,
+    IAPIPodNotificationBluetoothConnectionStatusBits_HID     = 1 << 5,
+    IAPIPodNotificationBluetoothConnectionStatusBits_IAP     = 1 << 7,
+    IAPIPodNotificationBluetoothConnectionStatusBits_PAN_NAP = 1 << 8,
+    IAPIPodNotificationBluetoothConnectionStatusBits_PAN_U   = 1 << 12,
+};
+
+struct IAPIPodNotificationBluetoothConnectionPayload {
+    uint8_t  type; /* = IAPIPodNotificationType_BluetoothConnection */
+    uint8_t  mac_addr[6];
+    uint64_t status; /* IAPIPodNotificationBluetoothConnectionStatusBits */
+} __attribute__((packed));
+
+struct IAPIPodNotificationAppDisplayNamePayload {
+    uint8_t type; /* = IAPIPodNotificationType_AppDisplayName */
+    char    name[];
+} __attribute__((packed));
+
+struct IAPIPodNotificationAssistiveTouchPayload {
+    uint8_t type; /* = IAPIPodNotificationType_AssistiveTouch */
+    uint8_t is_on;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-options-for-lingo.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-options-for-lingo.h
new file mode 100644
index 0000000000..aeeca1a793
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-options-for-lingo.h
@@ -0,0 +1,114 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPGetIPodOptionsForLingoPayload {
+    uint8_t lingo_id;
+} __attribute__((packed));
+
+/* [1] P.192 Table 3-132 RetiPodOptionsForLingo option bits */
+
+enum IAPRetIPodOptionsForLingoGeneralBits {
+    IAPRetIPodOptionsForLingoGeneralBits_LineOutUsage              = 1 << 0,
+    IAPRetIPodOptionsForLingoGeneralBits_VideOutput                = 1 << 1,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoNTSCFormat           = 1 << 2,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoPALFormat            = 1 << 3,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoCompositeConnection  = 1 << 4,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoSVideoConnection     = 1 << 5,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoComponentConnection  = 1 << 6,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoClosedCaptioning     = 1 << 7,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoAspectRatio4x3       = 1 << 8,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoAspectRatio16x9      = 1 << 9,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoSubtitle             = 1 << 10,
+    IAPRetIPodOptionsForLingoGeneralBits_VideoAltAudioChannel      = 1 << 11,
+    IAPRetIPodOptionsForLingoGeneralBits_AppCommnunication         = 1 << 13,
+    IAPRetIPodOptionsForLingoGeneralBits_DeviceNotification        = 1 << 14,
+    IAPRetIPodOptionsForLingoGeneralBits_PauseOnRemovalControl     = 1 << 19,
+    IAPRetIPodOptionsForLingoGeneralBits_RestrictedIDPS            = 1 << 23,
+    IAPRetIPodOptionsForLingoGeneralBits_AppLaunchRequest          = 1 << 24,
+    IAPRetIPodOptionsForLingoGeneralBits_AudioRouteSwitch          = 1 << 26,
+    IAPRetIPodOptionsForLingoGeneralBits_USBHostModeHardwareInvoke = 1 << 27,
+    IAPRetIPodOptionsForLingoGeneralBits_USBHostModeAudioOutput    = 1 << 28,
+    IAPRetIPodOptionsForLingoGeneralBits_USBHostModeAudioInput     = 1 << 29,
+    IAPRetIPodOptionsForLingoGeneralBits_USBHostModeNoMaxCurrent   = 1 << 30,
+};
+
+enum IAPRetIPodOptionsForLingoSimpleRemoteBits {
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_ContextSpecificControls = 1 << 0,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_AudioMediaControls      = 1 << 1,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_VideoMediaControls      = 1 << 2,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_ImageMediaControls      = 1 << 3,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_SportsMediaControls     = 1 << 4,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_CameraMediaControls     = 1 << 8,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_USBHIDCommands          = 1 << 9,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_VoiceOverControls       = 1 << 10,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_VoiceOverPreferences    = 1 << 11,
+    IAPRetIPodOptionsForLingoSimpleRemoteBits_AssistiveTouchCursor    = 1 << 12,
+};
+
+enum IAPRetIPodOptionsForLingoDisplayRemoteBits {
+    IAPRetIPodOptionsForLingoDisplayRemoteBits_UIVolumeControl        = 1 << 0,
+    IAPRetIPodOptionsForLingoDisplayRemoteBits_AbsoluteVolumeControl  = 1 << 1,
+    IAPRetIPodOptionsForLingoDisplayRemoteBits_GeniusPlaylistCreation = 1 << 2,
+};
+
+enum IAPRetIPodOptionsForLingoExtendedInterfaceBits {
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_VideoBrowsing                    = 1 << 0,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_InfoCommands                     = 1 << 1,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_NestedPlaylists                  = 1 << 2,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_GeniusPlaylistCreationAndRefresh = 1 << 3,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_DisplayImages                    = 1 << 4,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_CategoryListAccess               = 1 << 5,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_PlayControl                      = 1 << 6,
+    IAPRetIPodOptionsForLingoExtendedInterfaceBits_UIDBasedCommands                 = 1 << 7,
+};
+
+/*
+enum IAPRetIPodOptionsForLingoAccessoryPowerBits {
+};
+*/
+
+enum IAPRetIPodOptionsForLingoUSBHostModeBits {
+    IAPRetIPodOptionsForLingoUSBHostModeBits_USBHostModeHardwareInvoke = 1 << 0, /* deprecated */
+    IAPRetIPodOptionsForLingoUSBHostModeBits_USBHostModeFirmwareInvoke = 1 << 1,
+};
+
+enum IAPRetIPodOptionsForLingoRFTunerBits {
+    IAPRetIPodOptionsForLingoRFTunerBits_RDSRawMode    = 1 << 0,
+    IAPRetIPodOptionsForLingoRFTunerBits_HDRadioTuning = 1 << 1,
+    IAPRetIPodOptionsForLingoRFTunerBits_AMRadioTuning = 1 << 2,
+};
+
+/*
+enum IAPRetIPodOptionsForLingoAccessoryEqualizerBits {
+};
+*/
+
+enum IAPRetIPodOptionsForLingoSportsBits {
+    IAPRetIPodOptionsForLingoSportsBits_NikePlusEquipment = 1 << 1,
+};
+
+enum IAPRetIPodOptionsForLingoDigitalAudioBits {
+    IAPRetIPodOptionsForLingoDigitalAudioBits_SetVideoDelayEnabled   = 1 << 0,
+    IAPRetIPodOptionsForLingoDigitalAudioBits_SampleRateCapsFIDToken = 1 << 2,
+    IAPRetIPodOptionsForLingoDigitalAudioBits_VideoDelayFIDToken     = 1 << 3,
+};
+
+enum IAPRetIPodOptionsForLingoStorageBits {
+    IAPRetIPodOptionsForLingoStorageBits_ITunesTagging     = 1 << 0,
+    IAPRetIPodOptionsForLingoStorageBits_NikePlusEquipment = 1 << 1,
+};
+
+enum IAPRetIPodOptionsForLingoIPodOutBits {
+    IAPRetIPodOptionsForLingoIPodOutBits_WheelUIModeAvail = 1 << 0,
+    IAPRetIPodOptionsForLingoIPodOutBits_PALVideoEnabled  = 1 << 2,
+};
+
+enum IAPRetIPodOptionsForLingoLocationBits {
+    IAPRetIPodOptionsForLingoLocationBits_AcceptNMEAData             = 1 << 0,
+    IAPRetIPodOptionsForLingoLocationBits_SendLocationAssistanceData = 1 << 1,
+};
+
+struct IAPRetIPodOptionsForLingoPayload {
+    uint8_t  lingo_id;
+    uint64_t bits;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-options.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-options.h
new file mode 100644
index 0000000000..14377c04d9
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-options.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPRetIPodOptionsState {
+    IAPRetIPodOptionsState_VideoOutputSupport        = 1 << 0,
+    IAPRetIPodOptionsState_SetiPodPreferencesSupport = 1 << 1,
+};
+
+struct IAPRetIPodOptionsPayload {
+    uint64_t state; /* IAPRetIPodOptionsState */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-preferences.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-preferences.h
new file mode 100644
index 0000000000..ec6d168440
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-preferences.h
@@ -0,0 +1,95 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.152 Table 3-57 Apple device preference class and setting IDs */
+enum IAPIPodPereferenceClassID {
+    IAPIPodPereferenceClassID_VideoOut             = 0x00,
+    IAPIPodPereferenceClassID_Screen               = 0x01,
+    IAPIPodPereferenceClassID_VideoFormat          = 0x02,
+    IAPIPodPereferenceClassID_LineOutUsage         = 0x03,
+    IAPIPodPereferenceClassID_VideoOutConnection   = 0x08,
+    IAPIPodPereferenceClassID_ClosedCaptioning     = 0x09,
+    IAPIPodPereferenceClassID_VideoAspectRatio     = 0x0A,
+    IAPIPodPereferenceClassID_Subtitle             = 0x0C,
+    IAPIPodPereferenceClassID_VideoAltAudioChannel = 0x0D,
+    IAPIPodPereferenceClassID_PauseOnPowerRemoval  = 0x0F,
+    IAPIPodPereferenceClassID_VoiceOver            = 0x14,
+    IAPIPodPereferenceClassID_AssistiveTouch       = 0x16,
+};
+
+enum IAPIPodPreferenceVideoOutSettingID {
+    IAPIPodPreferenceVideoOutSettingID_Off = 0x00,
+    IAPIPodPreferenceVideoOutSettingID_On  = 0x01,
+};
+
+enum IAPIPodPreferenceScreenSettingID {
+    IAPIPodPreferenceScreenSettingID_FillScreen = 0x00,
+    IAPIPodPreferenceScreenSettingID_FitToEdge  = 0x01,
+};
+
+enum IAPIPodPreferenceVideoFormatSettingID {
+    IAPIPodPreferenceVideoFormatSettingID_NTSC = 0x00,
+    IAPIPodPreferenceVideoFormatSettingID_PAL  = 0x01,
+};
+
+enum IAPIPodPreferenceLineOutUsageSettingID {
+    IAPIPodPreferenceLineOutUsageSettingID_NotUsed = 0x00,
+    IAPIPodPreferenceLineOutUsageSettingID_Used    = 0x01,
+};
+
+enum IAPIPodPreferenceVideoOutConnectionSettingID {
+    IAPIPodPreferenceVideoOutConnectionSettingID_Composite = 0x01,
+    IAPIPodPreferenceVideoOutConnectionSettingID_SVideo    = 0x02,
+    IAPIPodPreferenceVideoOutConnectionSettingID_Component = 0x03,
+};
+
+enum IAPIPodPreferenceClosedCaptioningSettingID {
+    IAPIPodPreferenceClosedCaptioningSettingID_Off = 0x00,
+    IAPIPodPreferenceClosedCaptioningSettingID_On  = 0x01,
+};
+
+enum IAPIPodPreferenceVideoAspectRatioSettingID {
+    IAPIPodPreferenceVideoAspectRatioSettingID_4x3  = 0x00,
+    IAPIPodPreferenceVideoAspectRatioSettingID_16x9 = 0x01,
+};
+
+enum IAPIPodPreferenceSubtitleSettingID {
+    IAPIPodPreferenceSubtitleSettingID_Off = 0x00,
+    IAPIPodPreferenceSubtitleSettingID_On  = 0x01,
+
+};
+
+enum IAPIPodPreferenceVideoAltAudioChannelSettingID {
+    IAPIPodPreferenceVideoAltAudioChannelSettingID_Off = 0x00,
+    IAPIPodPreferenceVideoAltAudioChannelSettingID_On  = 0x01,
+};
+
+enum IAPIPodPreferencePauseOnPowerRemovalSettingID {
+    IAPIPodPreferencePauseOnPowerRemovalSettingID_Off = 0x00,
+    IAPIPodPreferencePauseOnPowerRemovalSettingID_On  = 0x01,
+};
+
+enum IAPIPodPreferenceVoiceOverSettingID {
+    IAPIPodPreferenceVoiceOverSettingID_Off = 0x00,
+    IAPIPodPreferenceVoiceOverSettingID_On  = 0x01,
+};
+
+enum IAPIPodPreferenceAssistiveTouchSettingID {
+    IAPIPodPreferenceAssistiveTouchSettingID_Off = 0x00,
+    IAPIPodPreferenceAssistiveTouchSettingID_On  = 0x01,
+};
+
+struct IAPGetIPodPreferencesPayload {
+    uint8_t class_id; /* IAPIPodPereferenceClassID */
+} __attribute__((packed));
+
+struct IAPRetIPodPreferencesPayload {
+    uint8_t class_id; /* IAPIPodPereferenceClassID */
+    uint8_t setting_id;
+} __attribute__((packed));
+
+struct IAPSetIPodPreferencesPayload {
+    uint8_t class_id; /* IAPIPodPereferenceClassID */
+    uint8_t setting_id;
+    uint8_t restore_on_exit;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-serial-num.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-serial-num.h
new file mode 100644
index 0000000000..3285007403
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-serial-num.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPReturnIPodSerialNumPayload {
+    char serial[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-software-version.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-software-version.h
new file mode 100644
index 0000000000..bb036dbbc9
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ipod-software-version.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPReturnIPodSoftwareVersionPayload {
+    uint8_t major;
+    uint8_t minor;
+    uint8_t revision;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/lingo-protocol-version.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/lingo-protocol-version.h
new file mode 100644
index 0000000000..44569c7c12
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/lingo-protocol-version.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRequestLingoProtocolVersionPayload {
+    uint8_t lingo;
+} __attribute__((packed));
+
+struct IAPReturnLingoProtocolVersionPayload {
+    uint8_t lingo;
+    uint8_t major;
+    uint8_t minor;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/localization-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/localization-info.h
new file mode 100644
index 0000000000..f9a4235834
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/localization-info.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPLocalicationInfoType {
+    IAPLocalicationInfoType_Language = 0x00,
+    IAPLocalicationInfoType_Region   = 0x01,
+};
+
+struct IAPGetLocalizationInfoPayload {
+    uint8_t type; /* IAPLocalicationInfoType */
+} __attribute__((packed));
+
+struct IAPRetLocalizationInfoPayload {
+    uint8_t type; /* IAPLocalicationInfoType */
+    char    value[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/notify-ipod-state-change.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/notify-ipod-state-change.h
new file mode 100644
index 0000000000..22b187832a
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/notify-ipod-state-change.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPNotifyIPodStateChangeState {
+    IAPNotifyIPodStateChangeState_LostContextHibernate = 0x01,
+    IAPNotifyIPodStateChangeState_SaveContextHibernate = 0x02,
+    IAPNotifyIPodStateChangeState_Sleep                = 0x03,
+    IAPNotifyIPodStateChangeState_PowerOn              = 0x04,
+};
+
+struct IAPNotifyIPodStateChangePayload {
+    uint8_t state; /* IAPNotifyIPodStateChangeState */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/now-playing-app-bundle-name.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/now-playing-app-bundle-name.h
new file mode 100644
index 0000000000..b6401d555d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/now-playing-app-bundle-name.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/*
+struct IAPRetNowPlayingApplicationBundleNamePayload {
+    char name[];
+} __attribute__((packed));
+*/
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/request-application-launch.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/request-application-launch.h
new file mode 100644
index 0000000000..8a6ecc3cd7
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/request-application-launch.h
@@ -0,0 +1,9 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRequestApplicationLaunchPayload {
+    uint8_t reserved1;
+    uint8_t flag;
+    uint8_t reserved2;
+    char    app_id[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/set-available-current.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/set-available-current.h
new file mode 100644
index 0000000000..ed9cf86f30
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/set-available-current.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPSetAvailableCurrentPayload {
+    uint16_t current_limit_ma;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/set-internal-battery-charging-state.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/set-internal-battery-charging-state.h
new file mode 100644
index 0000000000..b5804cc4cb
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/set-internal-battery-charging-state.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPInternalBatteryChargingState {
+    IAPInternalBatteryChargingState_DontCharge = 0x00,
+    IAPInternalBatteryChargingState_MayCharge  = 0x01,
+};
+
+struct IAPInternalBatteryChargingStatePayload {
+    uint8_t state; /* IAPInternalBatteryChargingState */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/transport-max-payload-size.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/transport-max-payload-size.h
new file mode 100644
index 0000000000..bf83edd35c
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/transport-max-payload-size.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPReturnTransportMaxPayloadSizePayload {
+    uint16_t max_payload_size;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/ui-mode.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/ui-mode.h
new file mode 100644
index 0000000000..ed0410458a
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/ui-mode.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPUIMode {
+    IAPUIMode_Standard          = 0x00,
+    IAPUIMode_Extended          = 0x01,
+    IAPUIMode_IPodOutFullscreen = 0x02,
+    IAPUIMode_IPodOutActionSafe = 0x03,
+};
+
+struct IAPSetUIModePayload {
+    uint8_t ui_mode; /* IAPUIMode */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/general/wifi-connection-info.h b/firmware/usbstack/iap/libiap/spec/lingoes/general/wifi-connection-info.h
new file mode 100644
index 0000000000..701bbe5fa0
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/general/wifi-connection-info.h
@@ -0,0 +1,24 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPWiFiConnectionInfoStatus {
+    IAPWiFiConnectionInfoStatus_Success     = 0x00,
+    IAPWiFiConnectionInfoStatus_Unavailable = 0x01,
+    IAPWiFiConnectionInfoStatus_Declined    = 0x02,
+    IAPWiFiConnectionInfoStatus_Failed      = 0x03,
+};
+
+enum IAPWiFiConnectionInfoSecurityType {
+    IAPWiFiConnectionInfoSecurityType_Unsecured    = 0x00,
+    IAPWiFiConnectionInfoSecurityType_WEP          = 0x01,
+    IAPWiFiConnectionInfoSecurityType_WPA          = 0x02,
+    IAPWiFiConnectionInfoSecurityType_WPA2         = 0x03,
+    IAPWiFiConnectionInfoSecurityType_MixedWPAWPA2 = 0x04,
+};
+
+struct IAPWiFiConnectionInfoPayload {
+    uint8_t status;        /* IAPWiFiConnectionInfoStatus */
+    uint8_t security_type; /* IAPWiFiConnectionInfoSecurityType */
+    char    ssid[32];
+    char    passphrase[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out.h b/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out.h
new file mode 100644
index 0000000000..cbf6d0b329
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.366 Table 4-258 iPod Out lingo command summary */
+enum IAPIPodOutCommandID {
+    IAPIPodOutCommandID_IPodAck                   = 0x00, /* from dev, general/ipod-ack.h */
+    IAPIPodOutCommandID_GetIPodOutOptions         = 0x01, /* from acc, ipod-out-options.h */
+    IAPIPodOutCommandID_RetIPodOutOptions         = 0x02, /* from dev, ipod-out-options.h */
+    IAPIPodOutCommandID_SetIPodOutOptions         = 0x03, /* from acc, ipod-out-options.h */
+    IAPIPodOutCommandID_AccessoryStateChangeEvent = 0x04, /* from acc, */
+};
+
+#include "ipod-out/accessory-state-change-event.h"
+#include "ipod-out/ipod-out-options.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out/accessory-state-change-event.h b/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out/accessory-state-change-event.h
new file mode 100644
index 0000000000..4863e89554
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out/accessory-state-change-event.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.368 4.12.7 Command 0x04: AccessoryStateChangeEvent */
+
+enum IAPAccessoryStateChangeEvents {
+    IAPAccessoryStateChangeEvents_DisplaySwitchAwayFromIPodOut = 0x00,
+    IAPAccessoryStateChangeEvents_DisplaySwitchBackToIPodOut   = 0x01,
+    IAPAccessoryStateChangeEvents_AudioSwitchAwayFromIPodOut   = 0x02,
+    IAPAccessoryStateChangeEvents_AudioSwitchBackToIPodOut     = 0x03,
+    IAPAccessoryStateChangeEvents_EnterDaytimeMode             = 0x04,
+    IAPAccessoryStateChangeEvents_EnterNighttimeMode           = 0x05,
+};
+
+struct AccessoryStateChangeEventPayload {
+    uint8_t state_change; /* IAPAccessoryStateChangeEvents */
+} __attribute__((packed));
+
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out/ipod-out-options.h b/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out/ipod-out-options.h
new file mode 100644
index 0000000000..0af8b99ce2
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/ipod-out/ipod-out-options.h
@@ -0,0 +1,29 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.366 4.12.4 Command 0x01: GetiPodOutOptions */
+
+struct IAPGetIPodOutOptionsPayload {
+    uint8_t report_current;
+} __attribute__((packed));
+
+enum IAPIPodOutOptionBits {
+    IAPIPodOutOptionBits_DisplayAudio                   = 1 << 0,
+    IAPIPodOutOptionBits_DisplayPhoneCall               = 1 << 1,
+    IAPIPodOutOptionBits_DisplaySMS                     = 1 << 2,
+    IAPIPodOutOptionBits_DisplayVoicemail               = 1 << 4,
+    IAPIPodOutOptionBits_DisplayPushNotifications       = 1 << 5,
+    IAPIPodOutOptionBits_DisplayClockAlarmNotifications = 1 << 6,
+    IAPIPodOutOptionBits_DisplayTestPattern             = 1 << 8,
+    IAPIPodOutOptionBits_ShowMinimalIPodOut             = 1 << 9,
+    IAPIPodOutOptionBits_ShowFullIPodOud                = 1 << 10,
+};
+
+struct IAPRetIPodOutOptionsPayload {
+    uint8_t  report_current;
+    uint32_t options; /* IAPIPodOutOptionBits */
+} __attribute__((packed));
+
+struct IAPSetIPodOutOptionsPayload {
+    uint32_t options; /* IAPIPodOutOptionBits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/location.h b/firmware/usbstack/iap/libiap/spec/lingoes/location.h
new file mode 100644
index 0000000000..8e2a4aabae
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/location.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.371 Table 4-266 Location lingo commands */
+enum IAPLocationCommandID {
+    IAPLocationCommandID_AccessoryAck        = 0x00, /* from acc */
+    IAPLocationCommandID_GetAccessoryCaps    = 0x01, /* from dev */
+    IAPLocationCommandID_AccessoryCaps       = 0x02, /* from acc */
+    IAPLocationCommandID_GetAccessoryControl = 0x03, /* from dev */
+    IAPLocationCommandID_RetAccessoryControl = 0x04, /* from acc */
+    IAPLocationCommandID_SetAccessoryControl = 0x05, /* from dev */
+    IAPLocationCommandID_GetAccessoryData    = 0x06, /* from dev */
+    IAPLocationCommandID_RetAccessoryData    = 0x07, /* from acc */
+    IAPLocationCommandID_SetAccessoryData    = 0x08, /* from dev */
+    IAPLocationCommandID_AsyncAccessoryData  = 0x09, /* from acc */
+    IAPLocationCommandID_IPodAck             = 0x80, /* from dev */
+};
+
+/* TODO: add payload definitions */
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/microphone.h b/firmware/usbstack/iap/libiap/spec/lingoes/microphone.h
new file mode 100644
index 0000000000..3c90b3800c
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/microphone.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.534 Table C-12 Microphone lingo command summary */
+enum IAPMicrophoneCommandID {
+    IAPMicrophoneCommandID_BeginRecord      = 0x00, /* deprecated */
+    IAPMicrophoneCommandID_EndRecord        = 0x01, /* deprecated */
+    IAPMicrophoneCommandID_BeginPlayback    = 0x02, /* deprecated */
+    IAPMicrophoneCommandID_EndPlayback      = 0x03, /* deprecated */
+    IAPMicrophoneCommandID_AccessoryAck     = 0x04, /* from acc, general/acc-ack.h */
+    IAPMicrophoneCommandID_GetAccessoryAck  = 0x05, /* from dev, no payload */
+    IAPMicrophoneCommandID_IPodModeChange   = 0x06, /* from dev, ipod-mode-change.h */
+    IAPMicrophoneCommandID_GetAccessoryCaps = 0x07, /* from dev, no payload */
+    IAPMicrophoneCommandID_RetAccessoryCaps = 0x08, /* from acc, acc-caps.h */
+    IAPMicrophoneCommandID_GetAccessoryCtrl = 0x09, /* from dev, acc-ctrl.h */
+    IAPMicrophoneCommandID_RetAccessoryCtrl = 0x0A, /* from acc, acc-ctrl.h */
+    IAPMicrophoneCommandID_SetAccessoryCtrl = 0x0B, /* from dev, acc-ctrl.h */
+};
+
+#include "microphone/acc-caps.h"
+#include "microphone/acc-ctrl.h"
+#include "microphone/ipod-mode-change.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/microphone/acc-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/microphone/acc-caps.h
new file mode 100644
index 0000000000..3a98aadb32
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/microphone/acc-caps.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPAccCapsBits {
+    IAPAccCapsBits_StereoInput           = 1 << 0,
+    IAPAccCapsBits_StereoMonoInputSwitch = 1 << 1,
+    IAPAccCapsBits_VariableRecordLevel   = 1 << 2,
+    IAPAccCapsBits_RecordLevelLimit      = 1 << 3,
+    IAPAccCapsBits_DuplexAudio           = 1 << 4,
+};
+
+struct IAPRetAccCapsPayload {
+    uint8_t bits; /* IAPAccCapsBits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/microphone/acc-ctrl.h b/firmware/usbstack/iap/libiap/spec/lingoes/microphone/acc-ctrl.h
new file mode 100644
index 0000000000..fe3506aad9
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/microphone/acc-ctrl.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPAccCtrlType {
+    IAPAccCtrlType_LineInput                    = 0x01,
+    IAPAccCtrlType_RecordingLevelControl        = 0x02,
+    IAPAccCtrlType_RecordingLevelLimiterControl = 0x03,
+};
+
+struct IAPGetAccCtrlPayload {
+    uint8_t type; /* IAPAccCtrlType */
+} __attribute__((packed));
+
+struct IAPRetAccCtrlPayload {
+    uint8_t type; /* IAPAccCtrlType */
+    uint8_t value;
+} __attribute__((packed));
+
+struct IAPSetAccCtrlPayload {
+    uint8_t type; /* IAPAccCtrlType */
+    uint8_t value;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/microphone/ipod-mode-change.h b/firmware/usbstack/iap/libiap/spec/lingoes/microphone/ipod-mode-change.h
new file mode 100644
index 0000000000..55a49be67a
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/microphone/ipod-mode-change.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.537 Table C-18 Mode values */
+enum IAPIPodModeChangeModes {
+    IAPIPodModeChangeModes_BeginAudioRecord   = 0x00,
+    IAPIPodModeChangeModes_EndAudioRecord     = 0x01,
+    IAPIPodModeChangeModes_BeginAudioPlayback = 0x02,
+    IAPIPodModeChangeModes_EndAudioPlayback   = 0x03,
+};
+
+struct IAPIPodModeChangePayload {
+    uint8_t mode; /* IAPIPodModeChangeModes */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner.h
new file mode 100644
index 0000000000..11a540b6f6
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner.h
@@ -0,0 +1,69 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.288 Table 4-111 RF Tuner lingo command summary */
+enum IAPRFTunerCommandID {
+    IAPRFTunerCommandID_AccessoryAck             = 0x00, /* 1.00, from acc, acc-ack.h */
+    IAPRFTunerCommandID_GetTunerCaps             = 0x01, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerCaps             = 0x02, /* 1.00, from acc, tuner-caps.h */
+    IAPRFTunerCommandID_GetTunerCtrl             = 0x03, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerCtrl             = 0x04, /* 1.00, from acc, tuner-ctrl.h */
+    IAPRFTunerCommandID_SetTunerCtrl             = 0x05, /* 1.00, from dev, tuner-ctlr.h */
+    IAPRFTunerCommandID_GetTunerBand             = 0x06, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerBand             = 0x07, /* 1.00, from acc, tuner-band.h */
+    IAPRFTunerCommandID_SetTunerBand             = 0x08, /* 1.00, from dev, tuner-band.h*/
+    IAPRFTunerCommandID_GetTunerFreq             = 0x09, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerFreq             = 0x0A, /* 1.00, from acc, tuner-freq.h */
+    IAPRFTunerCommandID_SetTunerFreq             = 0x0B, /* 1.00, from dev, tuner-freq.h */
+    IAPRFTunerCommandID_GetTunerMode             = 0x0C, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerMode             = 0x0D, /* 1.00, from acc, tuner-mode.h */
+    IAPRFTunerCommandID_SetTunerMode             = 0x0E, /* 1.00, from dev, tuner-mode.h */
+    IAPRFTunerCommandID_GetTunerSeekRssi         = 0x0F, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerSeekRssi         = 0x10, /* 1.00, from acc, tuner-seek-rssi.h */
+    IAPRFTunerCommandID_SetTunerSeekRssi         = 0x11, /* 1.00, from dev, tuner-seek-rssi.h */
+    IAPRFTunerCommandID_TunerSeekStart           = 0x12, /* 1.00, from dev, tuner-seek.h */
+    IAPRFTunerCommandID_TunerSeekDone            = 0x13, /* 1.00, from acc, tuner-seek.h */
+    IAPRFTunerCommandID_GetTunerStatus           = 0x14, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetTunerStatus           = 0x15, /* 1.00, from acc, tuner-status.h */
+    IAPRFTunerCommandID_GetStatusNotifyMask      = 0x16, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetStatusNotifyMask      = 0x17, /* 1.00, from acc, status-notification.h */
+    IAPRFTunerCommandID_SetStatusNotifyMask      = 0x18, /* 1.00, from dev, status-notification.h */
+    IAPRFTunerCommandID_StatusChangeNotify       = 0x19, /* 1.00, from acc, status-notification.h */
+    IAPRFTunerCommandID_GetRdsReadyStatus        = 0x1A, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetRdsReadyStatus        = 0x1B, /* 1.00, from acc, rds-ready-status.h */
+    IAPRFTunerCommandID_GetRdsData               = 0x1C, /* 1.00, from dev, rds-data.h */
+    IAPRFTunerCommandID_RetRdsData               = 0x1D, /* 1.00, from acc, rds-data.h */
+    IAPRFTunerCommandID_GetRdsNotifyMask         = 0x1E, /* 1.00, from dev, no payload */
+    IAPRFTunerCommandID_RetRdsNotifyMask         = 0x1F, /* 1.00, from acc, rds-notify-mask.h */
+    IAPRFTunerCommandID_SetRdsNotifyMask         = 0x20, /* 1.00, from dev, rds-notify-mask.h */
+    IAPRFTunerCommandID_RdsReadyNotify           = 0x21, /* 1.00, from acc, rds-data.h */
+    IAPRFTunerCommandID_GetHDProgramServiceCount = 0x25, /* 1.01, from dev, no paylaod */
+    IAPRFTunerCommandID_RetHDProgramServiceCount = 0x26, /* 1.01, from acc, hd-program.h */
+    IAPRFTunerCommandID_GetHDProgramService      = 0x27, /* 1.01, from dev, no payload */
+    IAPRFTunerCommandID_RetHDProgramService      = 0x28, /* 1.01, from acc, hd-program.h */
+    IAPRFTunerCommandID_SetHDProgramService      = 0x29, /* 1.01, from dev, hd-program.h */
+    IAPRFTunerCommandID_GetHDDataReadyStatus     = 0x2A, /* 1.01, from dev, no payload */
+    IAPRFTunerCommandID_RetHDDataReadyStatus     = 0x2B, /* 1.01, from acc, hd-data.h */
+    IAPRFTunerCommandID_GetHDData                = 0x2C, /* 1.01, from dev, hd-data.h */
+    IAPRFTunerCommandID_RetHDData                = 0x2D, /* 1.01, from acc, hd-data.h */
+    IAPRFTunerCommandID_GetHDDataNotifyMask      = 0x2E, /* 1.01, from dev, no payload */
+    IAPRFTunerCommandID_RetHDDataNotifyMask      = 0x2F, /* 1.01, from acc, hd-data.h */
+    IAPRFTunerCommandID_SetHDDataNotifyMask      = 0x30, /* 1.01, from dev, hd-data.h */
+    IAPRFTunerCommandID_HDDataReadyNotify        = 0x31, /* 1.01, from acc, hd-data.h */
+};
+
+#include "rf-tuner/acc-ack.h"
+#include "rf-tuner/hd-data.h"
+#include "rf-tuner/hd-program.h"
+#include "rf-tuner/rds-data.h"
+#include "rf-tuner/rds-notify-mask.h"
+#include "rf-tuner/rds-ready-status.h"
+#include "rf-tuner/status-notification.h"
+#include "rf-tuner/tuner-band.h"
+#include "rf-tuner/tuner-caps.h"
+#include "rf-tuner/tuner-ctrl.h"
+#include "rf-tuner/tuner-freq.h"
+#include "rf-tuner/tuner-mode.h"
+#include "rf-tuner/tuner-seek-rssi.h"
+#include "rf-tuner/tuner-seek.h"
+#include "rf-tuner/tuner-status.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/acc-ack.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/acc-ack.h
new file mode 100644
index 0000000000..e16d3eab7d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/acc-ack.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRFTunerAccAckPayload {
+    uint8_t status; /* = IAPAckStatus */
+    uint8_t id;
+} __attribute__((packed));
+
+struct IAPRFTunerAccAckPendingPayload {
+    uint8_t  status; /* = IAPAckStatus_CommandPending */
+    uint8_t  id;
+    uint32_t pending_time_ms;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/hd-data.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/hd-data.h
new file mode 100644
index 0000000000..0e95dc46b2
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/hd-data.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.319 4.7.45 Command 0x2B: RetHDDataReadyStatus */
+
+enum IAPHDDataType {
+    IAPHDDataType_PSD                 = 0x00,
+    IAPHDDataType_SISStationIDNumber  = 0x02,
+    IAPHDDataType_SISStationNameShort = 0x03,
+    IAPHDDataType_SISStationNameLong  = 0x04,
+    IAPHDDataType_SISALFN             = 0x05,
+    IAPHDDataType_SISStationLocation  = 0x06,
+    IAPHDDataType_SISStationMessage   = 0x07,
+    IAPHDDataType_SISSlogan           = 0x08,
+    IAPHDDataType_SISParameterMessage = 0x09,
+};
+
+struct IAPRetHDDataReadyStatusPayload {
+    uint32_t status; /* 1 << IAPHDDataType | ... */
+} __attribute__((packed));
+
+struct IAPGetHDDataPayload {
+    uint8_t type; /* IAPHDDataType */
+} __attribute__((packed));
+
+struct IAPRetHDDataPayload {
+    uint8_t type;   /* IAPHDDataType */
+    uint8_t data[]; /* TODO: add difinitions */
+} __attribute__((packed));
+
+struct IAPRetHDDataNotifyMaskPayload {
+    uint32_t status; /* 1 << IAPHDDataType | ... */
+} __attribute__((packed));
+
+struct IAPSetHDDataNotifyMaskPayload {
+    uint32_t status; /* 1 << IAPHDDataType | ... */
+} __attribute__((packed));
+
+struct IAPHDDataReadyNotifyPayload {
+    uint8_t type;   /* IAPHDDataType */
+    uint8_t data[]; /* TODO: add difinitions */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/hd-program.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/hd-program.h
new file mode 100644
index 0000000000..a02d8ba5d3
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/hd-program.h
@@ -0,0 +1,17 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.317 4.7.40 Command 0x26: RetHDProgramServiceCount */
+
+struct IAPRetHDProgramServiceCountPayload {
+    uint8_t count;
+    uint8_t analog_program_exists;
+} __attribute__((packed));
+
+struct IAPRetHDProgramServicePayload {
+    uint8_t index;
+} __attribute__((packed));
+
+struct IAPSetHDProgramServicePayload {
+    uint8_t index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-data.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-data.h
new file mode 100644
index 0000000000..eebb783ea7
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-data.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.311 4.7.33 Command 0x1C: GetRdsData */
+
+struct IAPGetRdsDataPayload {
+    uint8_t type; /* IAPRdsData{Parsed,Raw}Type */
+};
+
+struct IAPRetRdsDataPayload {
+    uint8_t type;   /* IAPRdsData{Parsed,Raw}Type */
+    uint8_t data[]; /* TODO: add definitions */
+};
+
+struct IAPReadyNotifyPayload {
+    uint8_t type;   /* IAPRdsData{Parsed,Raw}Type */
+    uint8_t data[]; /* TODO: add definitions */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-notify-mask.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-notify-mask.h
new file mode 100644
index 0000000000..d3d67434e4
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-notify-mask.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.314 4.7.36 Command 0x1F: RetRdsNotifyMask */
+
+struct IAPRetRdsNotifyMaskPayload {
+    uint32_t mask; /* 1 << IAPRdsData{Parsed,Raw}Type | ... */
+} __attribute__((packed));
+
+struct IAPSetRdsNotifyMaskPayload {
+    uint32_t mask; /* 1 << IAPRdsData{Parsed,Raw}Type | ... */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-ready-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-ready-status.h
new file mode 100644
index 0000000000..713ebec3bf
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/rds-ready-status.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.310 4.7.32 Command 0x1B: RetRdsReadyStatus */
+
+enum IAPRdsDataParsedType {
+    IAPRdsDataParsedType_RadioText          = 0x04,
+    IAPRdsDataParsedType_ProgramServiceName = 0x1E,
+};
+
+enum IAPRdsDataRawType {
+    IAPRdsDataRawType_RdsGroupData = 0x05,
+};
+
+struct IAPRetRdsReadyStatusPayload {
+    uint32_t status; /* 1 << IAPRdsData{Parsed,Raw}Type | ... */
+} __attribute__((packed));
+
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/status-notification.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/status-notification.h
new file mode 100644
index 0000000000..4d304fb27f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/status-notification.h
@@ -0,0 +1,16 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.307 4.7.28 Command 0x17: RetStatusNotifyMask */
+
+struct IAPRetStatusNotifyMaskPayload {
+    uint8_t mask; /* IAPTunerStatusBits */
+} __attribute__((packed));
+
+struct IAPSetStatusNotifyMaskPayload {
+    uint8_t mask; /* IAPTunerStatusBits */
+} __attribute__((packed));
+
+struct IAPStatusChangeNotifyPayload {
+    uint8_t mask; /* IAPTunerStatusBits */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-band.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-band.h
new file mode 100644
index 0000000000..5ec03a950d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-band.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.297 4.7.12 Command 0x07: RetTunerBand */
+
+enum IAPTunerBandStateBits {
+    AMBandWorldwide = 0x00,
+    FMBandEuropeUS  = 0x01,
+    FMBandJapan     = 0x02,
+    FNBandWide      = 0x03,
+};
+
+struct IAPRetTunerBandPayload {
+    uint8_t state; /* IAPTunerBandStateBits */
+} __attribute__((packed));
+
+struct IAPSetTunerBandPayload {
+    uint8_t state; /* IAPTunerBandStateBits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-caps.h
new file mode 100644
index 0000000000..0b233f89fd
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-caps.h
@@ -0,0 +1,42 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.292 4.7.7 Command 0x02: RetTunerCaps */
+
+enum IAPFMTunerResolutionID {
+    _200kHz = 0b00,
+    _100kHz = 0b01,
+    _50kHz  = 0b10,
+};
+
+enum IAPAMTunerResolutionID {
+    _10kHz = 0b0,
+    _9kHz  = 0b1,
+};
+
+enum IAPTunerCapsBits {
+    IAPTunerCapsBits_AMWorldwide                = 1 << 0,
+    IAPTunerCapsBits_FMEuropeUS                 = 1 << 1,
+    IAPTunerCapsBits_FMJapan                    = 1 << 2,
+    IAPTunerCapsBits_FMWide                     = 1 << 3,
+    IAPTunerCapsBits_HDRadio                    = 1 << 4,
+    IAPTunerCapsBits_PowerOnOff                 = 1 << 8,
+    IAPTunerCapsBits_StatusChangeNotification   = 1 << 9,
+    IAPTunerCapsBits_MinFMResolution            = 3 << 16, /* IAPFMTunerResolutionID */
+    IAPTunerCapsBits_TunerSeekUpDown            = 1 << 18,
+    IAPTunerCapsBits_TunerSeekRSSIThreshold     = 1 << 19,
+    IAPTunerCapsBits_ForcemonophonicMode        = 1 << 20,
+    IAPTunerCapsBits_StereoBlend                = 1 << 21,
+    IAPTunerCapsBits_FMTunerDeemphasisSelect    = 1 << 22,
+    IAPTunerCapsBits_AMTuner9kHzResoluton       = 1 << 23,
+    IAPTunerCapsBits_RDSData                    = 1 << 24,
+    IAPTunerCapsBits_TunerChannelRSSIIndication = 1 << 25,
+    IAPTunerCapsBits_StereoSourceIndicator      = 1 << 26,
+    IAPTunerCapsBits_RDSRawMode                 = 1 << 27,
+};
+
+struct IAPRetTunerCapsPayload {
+    uint32_t caps;      /* IAPTunerCapsBits */
+    uint8_t  reserved1; /* = 0x01 */
+    uint8_t  reserved2; /* = 0x00 */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-ctrl.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-ctrl.h
new file mode 100644
index 0000000000..c09fd2465d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-ctrl.h
@@ -0,0 +1,18 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.294 4.7.9 Command 0x04: RetTunerCtrl */
+
+enum IAPTunerCtrlStateBits {
+    PowerDraw               = 1 << 0,
+    StateChangeNotification = 1 << 1,
+    RDSRawMode              = 1 << 3,
+};
+
+struct IAPRetTunerCtrlPayload {
+    uint8_t state; /* IAPTunerCtrlStateBits */
+} __attribute__((packed));
+
+struct IAPSetTunerCtrlPayload {
+    uint8_t state; /* IAPTunerCtrlStateBits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-freq.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-freq.h
new file mode 100644
index 0000000000..ed70a8f0aa
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-freq.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.298 4.7.15 Command 0x0A: RetTunerFreq */
+
+struct IAPRetTunerFreqPayload {
+    uint32_t freq_khz;
+    uint8_t  rssi_level;
+} __attribute__((packed));
+
+struct IAPSetTunerFreqPayload {
+    uint32_t freq_khz;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-mode.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-mode.h
new file mode 100644
index 0000000000..9ed455875f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-mode.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.300 4.7.18 Command 0x0D: RetTunerMode */
+
+enum IAPTunerModeStatusBits {
+    FMTunerResolution                = 3 << 0, /* IAPFMTunerResolutionID */
+    TunerSeekingUp                   = 1 << 2,
+    TunerSeekingWithMinRSSIThreshold = 1 << 3,
+    ForceMonophonic                  = 1 << 4,
+    StereoBlend                      = 1 << 5,
+    FMTunerDeemphasis50usec          = 1 << 6,
+    AMTunerResolution                = 1 << 7, /* IAPAMTunerResolutionID */
+};
+
+struct IAPRetTunerModePayload {
+    uint8_t status; /* IAPTunerModeStatusBits */
+} __attribute__((packed));
+
+struct IAPSetTunerModePayload {
+    uint8_t status; /* IAPTunerModeStatusBits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-seek-rssi.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-seek-rssi.h
new file mode 100644
index 0000000000..e3cf8fca8e
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-seek-rssi.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.302 4.7.21 Command 0x10: RetTunerSeekRssi */
+
+struct IAPRetTunerSeekRssiPayload {
+    uint8_t rssi_threshold;
+} __attribute__((packed));
+
+struct IAPSetTunerSeekRssiPayload {
+    uint8_t rssi_threshold;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-seek.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-seek.h
new file mode 100644
index 0000000000..ff56682887
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-seek.h
@@ -0,0 +1,33 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.302 4.7.23 Command 0x12: TunerSeekStart */
+
+enum IAPTunerSeekOperationID {
+    IAPTunerSeekOperationID_NoSeek                                          = 0x00,
+    IAPTunerSeekOperationID_SeekUpFromBeginning                             = 0x01,
+    IAPTunerSeekOperationID_SeekDownFromEnd                                 = 0x02,
+    IAPTunerSeekOperationID_SeekUpFromCurrent                               = 0x03,
+    IAPTunerSeekOperationID_SeekDownFromCurrent                             = 0x04,
+    IAPTunerSeekOperationID_SeekUpFromBeginningWithRssiThreshold            = 0x05,
+    IAPTunerSeekOperationID_SeekDownFromEndWithRssiThreshold                = 0x06,
+    IAPTunerSeekOperationID_SeekUpFromCurrentWithRssiThreshold              = 0x07,
+    IAPTunerSeekOperationID_SeekDownFromCurrentWithRssiThreshold            = 0x08,
+    IAPTunerSeekOperationID_SeekUpFromBeginningForHDSignal                  = 0x09,
+    IAPTunerSeekOperationID_SeekDownFromEndForHDSignal                      = 0x0A,
+    IAPTunerSeekOperationID_SeekUpFromCurrentForHDSignal                    = 0x0B,
+    IAPTunerSeekOperationID_SeekDownFromCurrentForHDSignal                  = 0x0C,
+    IAPTunerSeekOperationID_SeekUpFromBeginningForHDSignalWithRssiThreshold = 0x0D,
+    IAPTunerSeekOperationID_SeekDownFromEndForHDSignalWithRssiThreshold     = 0x0E,
+    IAPTunerSeekOperationID_SeekUpFromCurrentForHDSignalWithRssiThreshold   = 0x0F,
+    IAPTunerSeekOperationID_SeekDownFromCurrentForHDSignalWithRssiThreshold = 0x10,
+};
+
+struct IAPTunerSeekStartPayload {
+    uint8_t operation_id; /* IAPTunerSeekOperationID */
+} __attribute__((packed));
+
+struct IAPTunerSeekDonePayload {
+    uint32_t freq_khz;
+    uint8_t  signal_strength;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-status.h
new file mode 100644
index 0000000000..70ba8b01e4
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/rf-tuner/tuner-status.h
@@ -0,0 +1,17 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.306 4.7.26 Command 0x15: RetTunerStatus */
+
+enum IAPTunerStatusBits {
+    IAPTunerStatusBits_DataReceived          = 1 << 0,
+    IAPTunerStatusBits_RSSILevelChanged      = 1 << 1,
+    IAPTunerStatusBits_StereoSourceIndicator = 1 << 2,
+    IAPTunerStatusBits_HDSignalPresent       = 1 << 3,
+    IAPTunerStatusBits_HDDigitalAudioPresent = 1 << 4,
+    IAPTunerStatusBits_HDDataReceived        = 1 << 5,
+};
+
+struct IAPRetTunerStatusPayload {
+    uint8_t status; /* IAPTunerStatusBits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote.h
new file mode 100644
index 0000000000..e96a25bde3
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote.h
@@ -0,0 +1,39 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.213 Table 4-3 Simple remote lingo command summary */
+enum IAPSimpleRemoteCommandID {
+    IAPSimpleRemoteCommandID_ContextButtonStatus             = 0x00, /* from acc, button-status.h */
+    IAPSimpleRemoteCommandID_IPodAck                         = 0x01, /* from dev, general/ipod-ack.h */
+    IAPSimpleRemoteCommandID_ImageButtonStatus               = 0x02, /* from acc, button-status.h, deprecated */
+    IAPSimpleRemoteCommandID_VideoButtonStatus               = 0x03, /* from acc, button-status.h */
+    IAPSimpleRemoteCommandID_AudioButtonStatus               = 0x04, /* from acc, button-status.h */
+    IAPSimpleRemoteCommandID_IPodOutButtonStatus             = 0x0B, /* from acc, ipod-out-button-status.h */
+    IAPSimpleRemoteCommandID_RotationInputStatus             = 0x0C, /* from acc, rotation-input-status.h */
+    IAPSimpleRemoteCommandID_RadioButtonStatus               = 0x0D, /* from acc, radio-button-status.h */
+    IAPSimpleRemoteCommandID_CameraButtonStatus              = 0x0E, /* from acc, camera-button-status.h */
+    IAPSimpleRemoteCommandID_RegisterDescriptor              = 0x0F, /* from acc, hid.h */
+    IAPSimpleRemoteCommandID_IPodHIDReport                   = 0x10, /* from acc, hid.h */
+    IAPSimpleRemoteCommandID_AccessoryHIDReport              = 0x11, /* from dev, hid.h */
+    IAPSimpleRemoteCommandID_UnregisterDescriptor            = 0x12, /* from acc, hid.h */
+    IAPSimpleRemoteCommandID_VoiceOverEvent                  = 0x13, /* from acc, voice-over-event.h */
+    IAPSimpleRemoteCommandID_GetVoiceOverParameter           = 0x14, /* from acc, voice-over-parameter.h */
+    IAPSimpleRemoteCommandID_RetVoiceOverParameter           = 0x15, /* from dev, voice-over-parameter.h */
+    IAPSimpleRemoteCommandID_SetVoiceOverParameter           = 0x16, /* from acc, voice-over-parameter.h */
+    IAPSimpleRemoteCommandID_GetCurrentVoiceOverItemProperty = 0x17, /* from acc, current-voice-over-item-property.h */
+    IAPSimpleRemoteCommandID_RetCurrentVoiceOverItemProperty = 0x18, /* from dev, current-voice-over-item-property.h */
+    IAPSimpleRemoteCommandID_SetVoiceOverContext             = 0x19, /* from acc, set-voice-over-context.h */
+    IAPSimpleRemoteCommandID_VoiceOverParameterChanged       = 0x2A, /* from dev, voice-over-parameter.h */
+    IAPSimpleRemoteCommandID_AccessoryAck                    = 0x81, /* from acc, general/acc-ack.h */
+};
+
+#include "simple-remote/button-status.h"
+#include "simple-remote/camera-button-status.h"
+#include "simple-remote/current-voice-over-item-property.h"
+#include "simple-remote/hid.h"
+#include "simple-remote/ipod-out-button-status.h"
+#include "simple-remote/radio-button-status.h"
+#include "simple-remote/rotation-input-status.h"
+#include "simple-remote/set-voice-over-context.h"
+#include "simple-remote/voice-over-event.h"
+#include "simple-remote/voice-over-parameter.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/button-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/button-status.h
new file mode 100644
index 0000000000..779c2f540b
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/button-status.h
@@ -0,0 +1,98 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPContextButtonBits {
+    /* bits1 */
+    IAPContextButtonStatusButtons_PlayPause  = 1 << 0,
+    IAPContextButtonStatusButtons_VolumeUp   = 1 << 1,
+    IAPContextButtonStatusButtons_VolumeDown = 1 << 2,
+    IAPContextButtonStatusButtons_NextTrack  = 1 << 3,
+    IAPContextButtonStatusButtons_PrevTrack  = 1 << 4,
+    IAPContextButtonStatusButtons_NextAlbum  = 1 << 5,
+    IAPContextButtonStatusButtons_PrevAlbum  = 1 << 6,
+    IAPContextButtonStatusButtons_Stop       = 1 << 7,
+    /* bits2 */
+    IAPContextButtonStatusButtons_PlayResume            = 1 << 0,
+    IAPContextButtonStatusButtons_Pause                 = 1 << 1,
+    IAPContextButtonStatusButtons_MuteToggle            = 1 << 2,
+    IAPContextButtonStatusButtons_NextChapter           = 1 << 3,
+    IAPContextButtonStatusButtons_PrevChapter           = 1 << 4,
+    IAPContextButtonStatusButtons_NextPlaylist          = 1 << 5,
+    IAPContextButtonStatusButtons_PrevPlaylist          = 1 << 6,
+    IAPContextButtonStatusButtons_ShuffleSettingAdvance = 1 << 7,
+    /* bits3 */
+    IAPContextButtonStatusButtons_RepeatSettingAdvance = 1 << 0,
+    IAPContextButtonStatusButtons_PowerOn              = 1 << 1,
+    IAPContextButtonStatusButtons_PowerOff             = 1 << 2,
+    IAPContextButtonStatusButtons_Backlight30Seconds   = 1 << 3,
+    IAPContextButtonStatusButtons_BeginFastForward     = 1 << 4,
+    IAPContextButtonStatusButtons_BeginRewind          = 1 << 5,
+    IAPContextButtonStatusButtons_Menu                 = 1 << 6,
+    IAPContextButtonStatusButtons_Select               = 1 << 7,
+    /* bits4 */
+    IAPContextButtonStatusButtons_UpArrow      = 1 << 0,
+    IAPContextButtonStatusButtons_DownArrow    = 1 << 1,
+    IAPContextButtonStatusButtons_BacklightOff = 1 << 2,
+};
+
+enum IAPImageButtonBits {
+    /* bits1 */
+    IAPImageButtonBits_PlayPause      = 1 << 0,
+    IAPImageButtonBits_NextImage      = 1 << 1,
+    IAPImageButtonBits_PrevImage      = 1 << 2,
+    IAPImageButtonBits_Stop           = 1 << 3,
+    IAPImageButtonBits_PlayResume     = 1 << 4,
+    IAPImageButtonBits_Pause          = 1 << 5,
+    IAPImageButtonBits_ShuffleAdvance = 1 << 6,
+    IAPImageButtonBits_RepeatAdvance  = 1 << 7,
+};
+
+enum IAPVideoButtonBits {
+    /* bits1 */
+    IAPVideoButtonBits_PlayPause  = 1 << 0,
+    IAPVideoButtonBits_NextVideo  = 1 << 1,
+    IAPVideoButtonBits_PrevVideo  = 1 << 2,
+    IAPVideoButtonBits_Stop       = 1 << 3,
+    IAPVideoButtonBits_PlayResume = 1 << 4,
+    IAPVideoButtonBits_Pause      = 1 << 5,
+    IAPVideoButtonBits_BeginFF    = 1 << 6,
+    IAPVideoButtonBits_BeginREW   = 1 << 7,
+    /* bits1 */
+    IAPVideoButtonBitsNextChapter = 1 << 0,
+    IAPVideoButtonBitsPrevChapter = 1 << 1,
+    IAPVideoButtonBitsNextFrame   = 1 << 2,
+    IAPVideoButtonBitsPrevFrame   = 1 << 3,
+};
+
+enum IAPAudioButtonBits {
+    /* bits1 */
+    IAPAudioButtonBits_PlayPause  = 1 << 0,
+    IAPAudioButtonBits_VolumeUp   = 1 << 1,
+    IAPAudioButtonBits_VolumeDown = 1 << 2,
+    IAPAudioButtonBits_NextTrack  = 1 << 3,
+    IAPAudioButtonBits_PrevTrack  = 1 << 4,
+    IAPAudioButtonBits_NextAlbum  = 1 << 5,
+    IAPAudioButtonBits_PrevAlbum  = 1 << 6,
+    IAPAudioButtonBits_Stop       = 1 << 7,
+    /* bits2 */
+    IAPAudioButtonBits_PlayResume            = 1 << 0,
+    IAPAudioButtonBits_Pause                 = 1 << 1,
+    IAPAudioButtonBits_MuteToggle            = 1 << 2,
+    IAPAudioButtonBits_NextChapter           = 1 << 3,
+    IAPAudioButtonBits_PrevChapter           = 1 << 4,
+    IAPAudioButtonBits_NextPlaylist          = 1 << 5,
+    IAPAudioButtonBits_PrevPlaylist          = 1 << 6,
+    IAPAudioButtonBits_ShuffleSettingAdvance = 1 << 7,
+    /* bits3 */
+    IAPAudioButtonBits_RepeatSettingAdvance = 1 << 0,
+    IAPAudioButtonBits_BeginFF              = 1 << 1,
+    IAPAudioButtonBits_BeginREW             = 1 << 2,
+    IAPAudioButtonBits_Record               = 1 << 3,
+};
+
+struct IAPButtonStatusPayload {
+    uint8_t bits1; /* IAP{Context,Image,Video,Audio}ButtonBits(bits1) */
+    uint8_t bits2; /* IAP{Context,Image,Video,Audio}ButtonBits(bits2), optional */
+    uint8_t bits3; /* IAP{Context,Image,Video,Audio}ButtonBits(bits3), optional */
+    uint8_t bits4; /* IAP{Context,Image,Video,Audio}ButtonBits(bits4), optional */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/camera-button-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/camera-button-status.h
new file mode 100644
index 0000000000..7e5fd17b8b
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/camera-button-status.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPCameraButtonStatus {
+    IAPCameraButtonStatus_Up   = 0x00,
+    IAPCameraButtonStatus_Down = 0x01,
+};
+
+struct IAPCameraButtonStatusPayload {
+    uint8_t status; /* IAPCameraButtonStatus */
+};
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/current-voice-over-item-property.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/current-voice-over-item-property.h
new file mode 100644
index 0000000000..fbbf85fe79
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/current-voice-over-item-property.h
@@ -0,0 +1,72 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPCurrentVoiceOverItemPropertyType {
+    IAPCurrentVoiceOverItemPropertyType_Label,
+    IAPCurrentVoiceOverItemPropertyType_Value,
+    IAPCurrentVoiceOverItemPropertyType_Hint,
+    IAPCurrentVoiceOverItemPropertyType_Frame,
+    IAPCurrentVoiceOverItemPropertyType_Traits,
+};
+
+struct IAPGetCurrentVoiceOverItemPropertyPayload {
+    uint8_t type; /* IAPCurrentVoiceOverItemPropertyType */
+} __attribute__((packed));
+
+struct IAPRetCurrentVoiceOverItemPropertyPayload {
+    uint8_t type; /* IAPCurrentVoiceOverItemPropertyType */
+    uint8_t value[];
+} __attribute__((packed));
+
+struct IAPRetCurrentVoiceOverItemPropertyLabelPayload {
+    uint8_t  type; /* = IAPCurrentVoiceOverItemPropertyType_Label */
+    uint16_t current_section_index;
+    uint16_t last_section_index;
+    char     text[];
+} __attribute__((packed));
+
+struct IAPRetCurrentVoiceOverItemPropertyVolumePayload {
+    uint8_t  type; /* = IAPCurrentVoiceOverItemPropertyType_Volume */
+    uint16_t current_section_index;
+    uint16_t last_section_index;
+    char     text[];
+} __attribute__((packed));
+
+struct IAPRetCurrentVoiceOverItemPropertyHintPayload {
+    uint8_t  type; /* = IAPCurrentVoiceOverItemPropertyType_Hint */
+    uint16_t current_section_index;
+    uint16_t last_section_index;
+    char     text[];
+} __attribute__((packed));
+
+struct IAPRetCurrentVoiceOverItemPropertyFramePayload {
+    uint8_t  type; /* = IAPCurrentVoiceOverItemPropertyType_Frame */
+    uint16_t top_x;
+    uint16_t top_y;
+    uint16_t bottom_x;
+    uint16_t bottom_y;
+} __attribute__((packed));
+
+enum IAPRetCurrentVoiceOverItemPropertyTraits {
+    IAPRetCurrentVoiceOverItemPropertyTraits_Button             = 0x0000,
+    IAPRetCurrentVoiceOverItemPropertyTraits_Link               = 0x0001,
+    IAPRetCurrentVoiceOverItemPropertyTraits_SearchField        = 0x0002,
+    IAPRetCurrentVoiceOverItemPropertyTraits_Image              = 0x0003,
+    IAPRetCurrentVoiceOverItemPropertyTraits_Selected           = 0x0004,
+    IAPRetCurrentVoiceOverItemPropertyTraits_Sound              = 0x0005,
+    IAPRetCurrentVoiceOverItemPropertyTraits_KeyboardKey        = 0x0006,
+    IAPRetCurrentVoiceOverItemPropertyTraits_StaticText         = 0x0007,
+    IAPRetCurrentVoiceOverItemPropertyTraits_SummaryElement     = 0x0008,
+    IAPRetCurrentVoiceOverItemPropertyTraits_NotEnabled         = 0x0009,
+    IAPRetCurrentVoiceOverItemPropertyTraits_UpdatesFrequently  = 0x000A,
+    IAPRetCurrentVoiceOverItemPropertyTraits_StartsMediaSession = 0x000B,
+    IAPRetCurrentVoiceOverItemPropertyTraits_Adjustable         = 0x000C,
+    IAPRetCurrentVoiceOverItemPropertyTraits_BackButton         = 0x000D,
+    IAPRetCurrentVoiceOverItemPropertyTraits_Map                = 0x000E,
+    IAPRetCurrentVoiceOverItemPropertyTraits_DeleteKey          = 0x000F,
+};
+
+struct IAPRetCurrentVoiceOverItemPropertyTraitsPayload {
+    uint8_t  type;     /* = IAPCurrentVoiceOverItemPropertyType_Traits */
+    uint16_t traits[]; /* IAPRetCurrentVoiceOverItemPropertyTraits */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/hid.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/hid.h
new file mode 100644
index 0000000000..70b42be1fd
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/hid.h
@@ -0,0 +1,75 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPRegisterDescriptorCountryCodes {
+    IAPRegisterDescriptorCountryCodes_Undefined         = 0x00,
+    IAPRegisterDescriptorCountryCodes_Arabic            = 0x01,
+    IAPRegisterDescriptorCountryCodes_Belgian           = 0x02,
+    IAPRegisterDescriptorCountryCodes_CanadianBilingual = 0x03,
+    IAPRegisterDescriptorCountryCodes_CanadianFrench    = 0x04,
+    IAPRegisterDescriptorCountryCodes_CzechRepublic     = 0x05,
+    IAPRegisterDescriptorCountryCodes_Danish            = 0x06,
+    IAPRegisterDescriptorCountryCodes_Finnish           = 0x07,
+    IAPRegisterDescriptorCountryCodes_French            = 0x08,
+    IAPRegisterDescriptorCountryCodes_German            = 0x09,
+    IAPRegisterDescriptorCountryCodes_Greek             = 0x0A,
+    IAPRegisterDescriptorCountryCodes_Hebrew            = 0x0B,
+    IAPRegisterDescriptorCountryCodes_Hungarian         = 0x0C,
+    IAPRegisterDescriptorCountryCodes_International     = 0x0D,
+    IAPRegisterDescriptorCountryCodes_Italian           = 0x0E,
+    IAPRegisterDescriptorCountryCodes_Japan             = 0x0F,
+    IAPRegisterDescriptorCountryCodes_Korean            = 0x10,
+    IAPRegisterDescriptorCountryCodes_LatinAmerican     = 0x11,
+    IAPRegisterDescriptorCountryCodes_Netherlands       = 0x12,
+    IAPRegisterDescriptorCountryCodes_Norwegian         = 0x13,
+    IAPRegisterDescriptorCountryCodes_Persian           = 0x14,
+    IAPRegisterDescriptorCountryCodes_Poland            = 0x15,
+    IAPRegisterDescriptorCountryCodes_Portuguese        = 0x16,
+    IAPRegisterDescriptorCountryCodes_Russian           = 0x17,
+    IAPRegisterDescriptorCountryCodes_Slovakia          = 0x18,
+    IAPRegisterDescriptorCountryCodes_Spanish           = 0x19,
+    IAPRegisterDescriptorCountryCodes_Swedish           = 0x1A,
+    IAPRegisterDescriptorCountryCodes_SwissFrench       = 0x1B,
+    IAPRegisterDescriptorCountryCodes_SwissGerman       = 0x1C,
+    IAPRegisterDescriptorCountryCodes_Switzerland       = 0x1D,
+    IAPRegisterDescriptorCountryCodes_Taiwan            = 0x1E,
+    IAPRegisterDescriptorCountryCodes_TurkishQ          = 0x1F,
+    IAPRegisterDescriptorCountryCodes_UK                = 0x20,
+    IAPRegisterDescriptorCountryCodes_US                = 0x21,
+    IAPRegisterDescriptorCountryCodes_Croatian          = 0x22,
+    IAPRegisterDescriptorCountryCodes_TurkishF          = 0x23,
+    IAPRegisterDescriptorCountryCodes_Thai              = 0xFA,
+    IAPRegisterDescriptorCountryCodes_Flemish           = 0xFB,
+    IAPRegisterDescriptorCountryCodes_Romanian          = 0xFC,
+    IAPRegisterDescriptorCountryCodes_Bulgarian         = 0xFD,
+    IAPRegisterDescriptorCountryCodes_Chinese           = 0xFE,
+};
+
+struct IAPRegisterDescriptorPayload {
+    uint8_t  index;
+    uint16_t vid;
+    uint16_t pid;
+    uint8_t  country_code; /* IAPRegisterDescriptorCountryCodes */
+    uint8_t  descriptor[];
+} __attribute__((packed));
+
+enum IAPHIDReportReportType {
+    IAPHIDReportReportType_Input  = 0x00,
+    IAPHIDReportReportType_Output = 0x01,
+};
+
+struct IAPIPodHIDReportPayload {
+    uint8_t index;
+    uint8_t report_type; /* = IAPHIDReportReportType_Input */
+    uint8_t report[];
+} __attribute__((packed));
+
+struct IAPAccessoryHIDReportPayload {
+    uint8_t index;
+    uint8_t report_type; /* = IAPHIDReportReportType_Output */
+    uint8_t report[];
+} __attribute__((packed));
+
+struct IAPUnregisterDescriptorPayload {
+    uint8_t index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/ipod-out-button-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/ipod-out-button-status.h
new file mode 100644
index 0000000000..a81839a9c4
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/ipod-out-button-status.h
@@ -0,0 +1,26 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPIPodOutButtonStatusSource {
+    IAPIPodOutButtonStatusSource_CarCenterConsole = 0x00,
+    IAPIPodOutButtonStatusSource_SteeringWheel    = 0x01,
+    IAPIPodOutButtonStatusSource_CarDashboard     = 0x02,
+};
+
+enum IAPIPodOutButtonBits {
+    /* bits1 */
+    IAPIPodOutButtonBits_Select = 1 << 0,
+    IAPIPodOutButtonBits_Left   = 1 << 1,
+    IAPIPodOutButtonBits_Rigth  = 1 << 2,
+    IAPIPodOutButtonBits_Up     = 1 << 3,
+    IAPIPodOutButtonBits_Down   = 1 << 4,
+    IAPIPodOutButtonBits_Menu   = 1 << 5,
+};
+
+struct IAPIPodOutButtonStatusPayload {
+    uint8_t source; /* IAPIPodOutButtonStatusSource */
+    uint8_t bits1;  /* IAPIPodOutButtonBits(bits1) */
+    uint8_t bits2;  /* IAPIPodOutButtonBits(bits2), optional */
+    uint8_t bits3;  /* IAPIPodOutButtonBits(bits3), optional */
+    uint8_t bits4;  /* IAPIPodOutButtonBits(bits4), optional */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/radio-button-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/radio-button-status.h
new file mode 100644
index 0000000000..71b4a7abc5
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/radio-button-status.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPRadioButtonStatus {
+    IAPRadioButtonStatus_Released = 0x00,
+    IAPRadioButtonStatus_Pushed   = 0x02,
+};
+
+struct IAPRadioButtonStatusPayload {
+    uint8_t status; /* IAPRadioButtonStatus */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/rotation-input-status.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/rotation-input-status.h
new file mode 100644
index 0000000000..05ca73726d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/rotation-input-status.h
@@ -0,0 +1,33 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPRotationInputStatusControllerType {
+    IAPRotationInputStatusControllerType_FreeWheel = 0x00,
+};
+
+enum IAPRotationInputStatusRotationDirection {
+    IAPRotationInputStatusRotationDirection_Left  = 0x00, /* CCW */
+    IAPRotationInputStatusRotationDirection_Right = 0x01, /* CW */
+};
+
+enum IAPRotationInputStatusRotationAction {
+    IAPRotationInputStatusRotationAction_Completed  = 0x00,
+    IAPRotationInputStatusRotationAction_InProgress = 0x01,
+    IAPRotationInputStatusRotationAction_Repeat     = 0x02,
+};
+
+enum IAPRotationInputStatusRotationType {
+    IAPRotationInputStatusRotationType_Detent = 0x00,
+    IAPRotationInputStatusRotationType_Degree = 0x01,
+};
+
+struct IAPRotationInputStatusPayload {
+    uint32_t user_action_duration_ms;
+    uint8_t  source;             /* IAPIPodOutButtonStatusSource */
+    uint8_t  controller_type;    /* IAPRotationInputStatusControllerType */
+    uint8_t  rotation_direction; /* IAPRotationInputStatusRotationDirection */
+    uint8_t  rotation_action;    /* IAPRotationInputStatusRotationAction */
+    uint8_t  rotation_type;      /* IAPRotationInputStatusRotationType */
+    uint16_t detents_or_degrees_moved;
+    uint16_t max_detents_or_degrees_moved;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/set-voice-over-context.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/set-voice-over-context.h
new file mode 100644
index 0000000000..05945067f0
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/set-voice-over-context.h
@@ -0,0 +1,17 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPSetVoiceOverContextType {
+    IAPSetVoiceOverContextType_None               = 0x00,
+    IAPSetVoiceOverContextType_Header             = 0x01,
+    IAPSetVoiceOverContextType_Link               = 0x02,
+    IAPSetVoiceOverContextType_Form               = 0x03,
+    IAPSetVoiceOverContextType_Cursor             = 0x04,
+    IAPSetVoiceOverContextType_VerticalNavigation = 0x05,
+    IAPSetVoiceOverContextType_ValueAdjustment    = 0x06,
+    IAPSetVoiceOverContextType_ZoomAdjustment     = 0x07,
+};
+
+struct IAPSetVoiceOverContextPayload {
+    uint8_t type; /* IAPSetVoiceOverContextType */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/voice-over-event.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/voice-over-event.h
new file mode 100644
index 0000000000..f09290cee4
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/voice-over-event.h
@@ -0,0 +1,85 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPVoiceOverEventType {
+    IAPVoiceOverEventType_MovePointer         = 0x00,
+    IAPVoiceOverEventType_MoveToFirst         = 0x01,
+    IAPVoiceOverEventType_MoveToLast          = 0x02,
+    IAPVoiceOverEventType_MoveToNext          = 0x03,
+    IAPVoiceOverEventType_MoveToPrev          = 0x04,
+    IAPVoiceOverEventType_ScrollLeftPage      = 0x05,
+    IAPVoiceOverEventType_ScrollRightPage     = 0x06,
+    IAPVoiceOverEventType_ScrollUpPage        = 0x07,
+    IAPVoiceOverEventType_ScrollDownPage      = 0x08,
+    IAPVoiceOverEventType_ScrollToPoint       = 0x09,
+    IAPVoiceOverEventType_SendTextToInput     = 0x0A,
+    IAPVoiceOverEventType_Cut                 = 0x0B,
+    IAPVoiceOverEventType_Copy                = 0x0C,
+    IAPVoiceOverEventType_Paste               = 0x0D,
+    IAPVoiceOverEventType_Home                = 0x0E,
+    IAPVoiceOverEventType_Touch               = 0x0F,
+    IAPVoiceOverEventType_DisplayScale        = 0x10,
+    IAPVoiceOverEventType_CenterDisplay       = 0x11,
+    IAPVoiceOverEventType_PauseSpeak          = 0x12,
+    IAPVoiceOverEventType_ResumeSpeak         = 0x13,
+    IAPVoiceOverEventType_ReadTextFromCurrent = 0x14,
+    IAPVoiceOverEventType_ReadTextFromTop     = 0x15,
+    IAPVoiceOverEventType_SendTextToSpeech    = 0x16,
+    IAPVoiceOverEventType_Escape              = 0x17,
+};
+
+struct IAPVoiceOverEventPayload {
+    uint8_t type; /* IAPVoiceOverEventType */
+    uint8_t data[];
+} __attribute__((packed));
+
+struct IAPVoiceOverEventMovePointerPayload {
+    uint8_t  type; /* = IAPVoiceOverEventType_MovePointer */
+    uint16_t x;
+    uint16_t y;
+} __attribute__((packed));
+
+struct IAPVoiceOverEventScrollToPointPayload {
+    uint8_t  type; /* = IAPVoiceOverEventType_ScrollToPoint */
+    uint16_t x;
+    uint16_t y;
+} __attribute__((packed));
+
+struct IAPVoiceOverEventSendTextToInputPayload {
+    uint8_t  type; /* = IAPVoiceOverEventType_SendTextToInput */
+    uint16_t current_section_index;
+    uint16_t last_section_index;
+    char     text[];
+} __attribute__((packed));
+
+enum IAPVoiceOverEventTouchEventType {
+    IAPVoiceOverEventTouchEventType_Begin      = 0x00,
+    IAPVoiceOverEventTouchEventType_Moved      = 0x01,
+    IAPVoiceOverEventTouchEventType_Stationary = 0x02,
+    IAPVoiceOverEventTouchEventType_Ended      = 0x03,
+    IAPVoiceOverEventTouchEventType_Canceled   = 0x04,
+};
+
+struct IAPVoiceOverEventTouchPayload {
+    uint8_t type; /* = IAPVoiceOverEventType_Touch */
+    uint8_t reserved[4];
+    uint8_t touch_event_type; /* IAPVoiceOverEventTouchEventType */
+} __attribute__((packed));
+
+struct IAPVoiceOverEventDisplayScalePayload {
+    uint8_t  type; /* = IAPVoiceOverEventType_DisplayScale */
+    uint16_t scale_factor;
+} __attribute__((packed));
+
+struct IAPVoiceOverEventCenterDisplayPayload {
+    uint8_t  type; /* = IAPVoiceOverEventType_CenterDisplay */
+    uint16_t x;
+    uint16_t y;
+} __attribute__((packed));
+
+struct IAPVoiceOverEventSendTextToSpeechPayload {
+    uint8_t  type; /* = IAPVoiceOverEventType_SendTextToSpeech */
+    uint16_t current_section_index;
+    uint16_t last_section_index;
+    char     text[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/voice-over-parameter.h b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/voice-over-parameter.h
new file mode 100644
index 0000000000..0d409294b8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/simple-remote/voice-over-parameter.h
@@ -0,0 +1,28 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPGetVoiceOverParameterType {
+    IAPGetVoiceOverParameterType_VoiceOverVolume  = 0x00,
+    IAPGetVoiceOverParameterType_SpeakingRate     = 0x01,
+    IAPGetVoiceOverParameterType_VoiceOverEnabled = 0x02,
+};
+
+struct IAPGetVoiceOverParameterPayload {
+    uint8_t type; /* IAPGetVoiceOverParameterType */
+} __attribute__((packed));
+
+struct IAPRetVoiceOverParameterPayload {
+    uint8_t type; /* IAPGetVoiceOverParameterType */
+    uint8_t value;
+} __attribute__((packed));
+
+struct IAPSetVoiceOverParameterPayload {
+    uint8_t type; /* IAPGetVoiceOverParameterType */
+    uint8_t value;
+} __attribute__((packed));
+
+
+struct IAPVoiceOverParameterChangedPayload {
+    uint8_t type; /* IAPGetVoiceOverParameterType */
+    uint8_t value;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sound-check-state.h b/firmware/usbstack/iap/libiap/spec/lingoes/sound-check-state.h
new file mode 100644
index 0000000000..a33e179f3a
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sound-check-state.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetSoundCheckStatePayload {
+    uint8_t sound_check_status;
+} __attribute__((packed));
+
+struct IAPSetSoundCheckStatePayload {
+    uint8_t sound_check_status;
+    uint8_t restore_on_exit;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sports.h b/firmware/usbstack/iap/libiap/spec/lingoes/sports.h
new file mode 100644
index 0000000000..7969516dc8
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sports.h
@@ -0,0 +1,25 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.336 Table 4-212 Sports lingo command summary */
+enum IAPSportsCommandID {
+    IAPSportsCommandID_AccessoryAck        = 0x00, /* from acc, general/acc-ack.h */
+    IAPSportsCommandID_GetAccessoryVersion = 0x01, /* from dev, no payload */
+    IAPSportsCommandID_RetAccessoryVersion = 0x02, /* from acc, accessory-version.h */
+    IAPSportsCommandID_GetAccessoryCaps    = 0x03, /* from dev, no payload */
+    IAPSportsCommandID_RetAccessoryCaps    = 0x04, /* from acc, accessory-caps.h */
+    IAPSportsCommandID_IPodAck             = 0x80, /* from dev, general/ipod-ack.h */
+    IAPSportsCommandID_GetIPodCaps         = 0x83, /* from acc, no payload */
+    IAPSportsCommandID_RetIPodCaps         = 0x84, /* from dev, ipod-caps.h */
+    IAPSportsCommandID_GetUserIndex        = 0x85, /* from acc, no payload */
+    IAPSportsCommandID_RetUserIndex        = 0x86, /* from dev, user-index.h */
+    IAPSportsCommandID_GetUserData         = 0x88, /* from acc, user-data.h */
+    IAPSportsCommandID_RetUserData         = 0x89, /* from dev, user-data.h */
+    IAPSportsCommandID_SetUserData         = 0x8A, /* from acc, user-data.h */
+};
+
+#include "sports/accessory-caps.h"
+#include "sports/accessory-version.h"
+#include "sports/ipod-caps.h"
+#include "sports/user-data.h"
+#include "sports/user-index.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sports/accessory-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/sports/accessory-caps.h
new file mode 100644
index 0000000000..ddbdb779b4
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sports/accessory-caps.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPSportsAccessoryCapBits {
+    IAPAccessoryCapBits_LaterCommands = 1 << 9,
+};
+
+struct IAPRetAccessoryCapsPayload {
+    uint16_t mask;     /* IAPSportsAccessoryCapBits */
+    uint8_t  reserved; /* = 0x00 */
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sports/accessory-version.h b/firmware/usbstack/iap/libiap/spec/lingoes/sports/accessory-version.h
new file mode 100644
index 0000000000..6a6be98427
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sports/accessory-version.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetAccessoryVersionPayload {
+    uint8_t major;
+    uint8_t minor;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sports/ipod-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/sports/ipod-caps.h
new file mode 100644
index 0000000000..a203552a0f
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sports/ipod-caps.h
@@ -0,0 +1,12 @@
+#pragma once
+#include <stdint.h>
+
+enum IAPSportsIPodCapBits {
+    Cardio   = 1 << 0,
+    UserData = 1 << 1,
+};
+
+struct IAPRetIPodCapsPayload {
+    uint16_t caps; /* IAPSportsIPodCapBits */
+    uint8_t  user_count;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sports/user-data.h b/firmware/usbstack/iap/libiap/spec/lingoes/sports/user-data.h
new file mode 100644
index 0000000000..5044e64efa
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sports/user-data.h
@@ -0,0 +1,73 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.341 4.9.13 Command 0x88: GetUserData */
+
+enum IAPSportsUserDataType {
+    IAPSportsUserDataType_UnitSystem       = 0x00,
+    IAPSportsUserDataType_Name             = 0x01,
+    IAPSportsUserDataType_Gender           = 0x02,
+    IAPSportsUserDataType_Weight           = 0x03,
+    IAPSportsUserDataType_Age              = 0x04,
+    IAPSportsUserDataType_WorkoutRecording = 0x05,
+};
+
+struct IAPGetUserDataPayload {
+    uint8_t type; /* IAPSportsUserDataType */
+} __attribute__((packed));
+
+struct IAPRetUserDataPayload {
+    uint8_t type; /* IAPSportsUserDataType */
+    uint8_t data[];
+} __attribute__((packed));
+
+enum IAPSportsUnitSystem {
+    IAPSportsUnitSystem_None     = 0x00,
+    IAPSportsUnitSystem_Imperial = 0x01,
+    IAPSportsUnitSystem_Metric   = 0x02,
+};
+
+struct IAPRetUserDataUnitSystemPayload {
+    uint8_t type;                  /* = IAPSportsUserDataType_UnitSystem */
+    uint8_t preferred_unit_system; /* IAPSportsUnitSystem */
+} __attribute__((packed));
+
+struct IAPRetUserDataNamePayload {
+    uint8_t type; /* = IAPSportsUserDataType_Name */
+    char    name[];
+} __attribute__((packed));
+
+enum IAPSportsGender {
+    IAPSportsGender_None   = 0x00,
+    IAPSportsGender_Female = 0x01,
+    IAPSportsGender_Male   = 0x02,
+};
+
+struct IAPRetUserDataGenderPayload {
+    uint8_t type;   /* = IAPSportsUserDataType_Gender */
+    uint8_t gender; /* IAPSportsGender */
+} __attribute__((packed));
+
+struct IAPRetUserDataWeightPayload {
+    uint8_t  type; /* = IAPSportsUserDataType_Weight */
+    uint16_t weight;
+} __attribute__((packed));
+
+struct IAPRetUserDataAgePayload {
+    uint8_t type; /* = IAPSportsUserDataType_Age */
+    uint8_t age;
+} __attribute__((packed));
+
+enum IAPSportsWorkoutRecordingPreferences {
+    IAPSportsWorkoutRecordingPreferences_None   = 0x00,
+    IAPSportsWorkoutRecordingPreferences_Never  = 0x01,
+    IAPSportsWorkoutRecordingPreferences_Ask    = 0x02,
+    IAPSportsWorkoutRecordingPreferences_Always = 0x03,
+};
+
+struct IAPRetUserDataWorkoutRecordingPayload {
+    uint8_t type;       /* = IAPSportsUserDataType_WorkoutRecording */
+    uint8_t preference; /* IAPSportsWorkoutRecordingPreferences */
+} __attribute__((packed));
+
+/* IAPSetUserDataPayload = IAPRetUserDataPayload */
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/sports/user-index.h b/firmware/usbstack/iap/libiap/spec/lingoes/sports/user-index.h
new file mode 100644
index 0000000000..2fce3aa328
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/sports/user-index.h
@@ -0,0 +1,6 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPRetUserIndexPayload {
+    uint8_t user_index;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage.h
new file mode 100644
index 0000000000..cfce9291b1
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage.h
@@ -0,0 +1,27 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.357 Table 4-242 Storage lingo commands */
+
+enum IAPStorageCommandID {
+    IAPStorageCommandID_IPodAck             = 0x00, /* from dev, ipod-ack.h */
+    IAPStorageCommandID_GetIPodCaps         = 0x01, /* from acc, no payload */
+    IAPStorageCommandID_RetIPodCaps         = 0x02, /* from dev, ipod-caps.h */
+    IAPStorageCommandID_RetIPodFileHandle   = 0x04, /* from dev, ipod-file-handle.h */
+    IAPStorageCommandID_WriteIPodFileData   = 0x07, /* from acc, write-ipod-file-data.h */
+    IAPStorageCommandID_CloseIPodFile       = 0x08, /* from acc, ipod-file-handle.h */
+    IAPStorageCommandID_GetIPodFreeSpace    = 0x10, /* from acc, no payload */
+    IAPStorageCommandID_RetIPodFreeSpace    = 0x11, /* from dev, ipod-free-space.h */
+    IAPStorageCommandID_OpenIPodFeatureFile = 0x12, /* from acc, ipod-file-handle.h */
+    IAPStorageCommandID_AccessoryAck        = 0x80, /* from acc, acc-ack.h */
+    IAPStorageCommandID_GetAccessoryCaps    = 0x81, /* from dev, no payload */
+    IAPStorageCommandID_RetAccessoryCaps    = 0x82, /* from acc, */
+};
+
+#include "storage/acc-ack.h"
+#include "storage/accessory-caps.h"
+#include "storage/ipod-ack.h"
+#include "storage/ipod-caps.h"
+#include "storage/ipod-file-handle.h"
+#include "storage/ipod-free-space.h"
+#include "storage/write-ipod-file-data.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/acc-ack.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/acc-ack.h
new file mode 100644
index 0000000000..1980237af2
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/acc-ack.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPStorageAccessoryAckPayload {
+    uint8_t status; /* IAPAckStatus */
+    uint8_t id;
+    uint8_t handle;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/accessory-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/accessory-caps.h
new file mode 100644
index 0000000000..7a7c26deed
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/accessory-caps.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.365 4.11.14 Command 0x82: RetAccessoryCaps */
+
+struct IAPStorageRetAccessoryCapsPayload {
+    uint8_t reserved1; /* = 0x00 */
+    uint8_t reserved2; /* = 0xFF */
+    uint8_t major;
+    uint8_t minor;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-ack.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-ack.h
new file mode 100644
index 0000000000..05b9c012e2
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-ack.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.359 4.11.3 Command 0x00: iPodAck */
+
+struct IAPStorageIPodAckPayload {
+    uint8_t status; /* IAPAckStatus */
+    uint8_t id;
+    uint8_t handle;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-caps.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-caps.h
new file mode 100644
index 0000000000..2a4d04be46
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-caps.h
@@ -0,0 +1,13 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.359 4.11.5 Command 0x02: RetiPodCaps */
+
+struct IAPIPodRetIPodCapsPayload {
+    uint64_t total_space;
+    uint32_t max_file_size;
+    uint16_t max_write_size;
+    uint8_t  reserved[6];
+    uint8_t  major_version;
+    uint8_t  minor_version;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-file-handle.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-file-handle.h
new file mode 100644
index 0000000000..edaeefc7cd
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-file-handle.h
@@ -0,0 +1,30 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.360 4.11.6 Command 0x04: RetiPodFileHandle */
+
+struct IAPRetIPodFileHandlePayload {
+    uint8_t handle;
+} __attribute__((packed));
+
+struct IAPCloseIPodFilePayload {
+    uint8_t handle;
+} __attribute__((packed));
+
+enum IAPOpenIPodFeatureFileFeatureType {
+    IAPOpenIPodFeatureFileFeatureType_TadioTagging           = 0x01,
+    IAPOpenIPodFeatureFileFeatureType_CardioEquipmentWorkout = 0x02,
+};
+
+enum IAPOpenIPodFeatureFileOptionsMask {
+    IAPOpenIPodFeatureFileOptionsMask_AppendFileData  = 1 << 0,
+    IAPOpenIPodFeatureFileOptionsMask_AppendIPodInfo  = 1 << 1,
+    IAPOpenIPodFeatureFileOptionsMask_InsertSignature = 1 << 3,
+};
+
+struct IAPOpenIPodFeatureFilePayload {
+    uint8_t  feature_type; /* IAPOpenIPodFeatureFileFeatureType */
+    uint32_t options_mask; /* IAPOpenIPodFeatureFileOptionsMask */
+    uint8_t  file_data[];
+} __attribute__((packed));
+
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-free-space.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-free-space.h
new file mode 100644
index 0000000000..8f81ec8059
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/ipod-free-space.h
@@ -0,0 +1,8 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.362 4.11.10 Command 0x11: RetiPodFreeSpace */
+
+struct IAPRetIPodFreeSpacePayload {
+    uint64_t free_space;
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/storage/write-ipod-file-data.h b/firmware/usbstack/iap/libiap/spec/lingoes/storage/write-ipod-file-data.h
new file mode 100644
index 0000000000..1c6fe7ce42
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/storage/write-ipod-file-data.h
@@ -0,0 +1,10 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.360 4.11.7 Command 0x07: WriteiPodFileData */
+
+struct IAPWriteIPodFileDataPayload {
+    uint32_t offset;
+    uint8_t  handle;
+    uint8_t  data[];
+} __attribute__((packed));
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/usb-host-mode.h b/firmware/usbstack/iap/libiap/spec/lingoes/usb-host-mode.h
new file mode 100644
index 0000000000..cf4594bdec
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/usb-host-mode.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.283 Table 4-101 USB Host mode commands */
+enum IAPUSBHostModeCommandID {
+    IAPUSBHostModeCommandID_AccessoryAck   = 0x00, /* from acc, general/acc-ack.h */
+    IAPUSBHostModeCommandID_NotifyUSBMode  = 0x04, /* from dev, usb-mode.h */
+    IAPUSBHostModeCommandID_IPodAck        = 0x80, /* from dev, general/ipod-ack.h */
+    IAPUSBHostModeCommandID_GetIPodUSBMode = 0x81, /* from acc, no payload */
+    IAPUSBHostModeCommandID_RetIPodUSBMode = 0x82, /* from dev, usb-mode.h */
+    IAPUSBHostModeCommandID_SetIPodUSBMode = 0x83, /* from acc, usb-mode.h */
+};
+
+#include "usb-host-mode/usb-mode.h"
diff --git a/firmware/usbstack/iap/libiap/spec/lingoes/usb-host-mode/usb-mode.h b/firmware/usbstack/iap/libiap/spec/lingoes/usb-host-mode/usb-mode.h
new file mode 100644
index 0000000000..fdc0f6e58d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/spec/lingoes/usb-host-mode/usb-mode.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <stdint.h>
+
+/* [1] P.285 4.6.3 Command 0x04: NotifyUSBMode */
+
+enum IAPUSBHostModeStatusCodes {
+    Disabled   = 0x00,
+    DeviceMode = 0x01,
+    HostMode   = 0x02,
+};
+
+struct IAPNotifyUSBModePayload {
+    uint8_t mode; /* IAPUSBHostModeStatusCodes */
+};
+
+struct IAPRetIPodUSBModePayload {
+    uint8_t mode; /* IAPUSBHostModeStatusCodes */
+};
+
+struct IAPSetIPodUSBModePayload {
+    uint8_t mode; /* IAPUSBHostModeStatusCodes */
+};
diff --git a/firmware/usbstack/iap/libiap/time.h b/firmware/usbstack/iap/libiap/time.h
new file mode 100644
index 0000000000..304bb4f04d
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/time.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <stdint.h>
+
+struct IAPPlatformTime {
+    uint16_t year;
+    uint8_t  month;
+    uint8_t  day;
+    uint8_t  hour;
+    uint8_t  minute;
+    uint8_t  seconds;
+};
diff --git a/firmware/usbstack/iap/libiap/unaligned.h b/firmware/usbstack/iap/libiap/unaligned.h
new file mode 100644
index 0000000000..864cac0a34
--- /dev/null
+++ b/firmware/usbstack/iap/libiap/unaligned.h
@@ -0,0 +1,7 @@
+#pragma once
+#include <stdint.h>
+
+typedef __attribute__((aligned(1))) uint8_t  uu8;
+typedef __attribute__((aligned(1))) uint16_t uu16;
+typedef __attribute__((aligned(1))) uint32_t uu32;
+typedef __attribute__((aligned(1))) uint64_t uu64;
diff --git a/firmware/usbstack/iap/macros.h b/firmware/usbstack/iap/macros.h
new file mode 100644
index 0000000000..f741398560
--- /dev/null
+++ b/firmware/usbstack/iap/macros.h
@@ -0,0 +1,44 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include <usb_drv.h>
+
+#include "debug.h"
+
+#if DEBUG_ENABLE_INFO == 1
+#define LOG(fmt, ...) logf("%lu %s:%d " fmt, iap_debug_timestamp(), __func__, __LINE__ __VA_OPT__(, __VA_ARGS__));
+#else
+#define LOG(...)
+#endif
+
+#if DEBUG_ENABLE_ERROR == 1
+#define ERROR(fmt, ...) logf("%lu %s:%d " fmt, iap_debug_timestamp(), __func__, __LINE__ __VA_OPT__(, __VA_ARGS__));
+#else
+#define ERROR(...)
+#endif
+
+#define check_act(cond, act, ...)                              \
+    if(!(cond)) {                                              \
+        ERROR("assertion failed" __VA_OPT__(":" __VA_ARGS__)); \
+        act;                                                   \
+    }
+
+#define AS_PACKET_SIZE 192
+#define AS_EP_IN       usb_iap_ep_allocs[0].ep
+#define HID_EP_IN      usb_iap_ep_allocs[1].ep
diff --git a/firmware/usbstack/iap/notification.c b/firmware/usbstack/iap/notification.c
new file mode 100644
index 0000000000..95444f077c
--- /dev/null
+++ b/firmware/usbstack/iap/notification.c
@@ -0,0 +1,111 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include "playback.h"
+
+#include "iap-usb.h"
+#include "libiap/iap.h"
+#include "platform.h"
+
+#include "macros.h"
+
+extern bool iap_initialized;
+
+void iap_on_track_time_position(uint32_t pos_ms) {
+    if(!iap_initialized) {
+        return;
+    }
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+    iap_notify_track_time_position(ctx, pos_ms);
+}
+
+void iap_on_track_playback_index(uint32_t index, bool track_ready) {
+    if(!iap_initialized) {
+        return;
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+
+    if(track_ready) {
+        /* called from from audio_finish_load_track() */
+        goto notify;
+    }
+
+    /* called from audio_playlist_track_change() */
+    struct Platform* plt = ctx->platform;
+    if(plt->aa_slot < 0) {
+        goto notify;
+    }
+    if(playback_current_aa_hid(plt->aa_slot) >= 0) {
+        /* artwork ready, maybe preloaded track */
+        goto notify;
+    } else {
+        /* artwork not ready, maybe after a playlist jump.
+         * in this case, we will be called again from audio_finish_load_track(),
+         * with track_ready == true. */
+        return;
+    }
+notify:
+    iap_notify_track_playback_index(ctx, index);
+}
+
+void iap_on_tracks_count(uint32_t count) {
+    if(!iap_initialized) {
+        return;
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+    iap_notify_tracks_count(ctx, count);
+}
+
+void iap_on_play_status(int status) {
+    if(!iap_initialized) {
+        return;
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+    iap_notify_play_status(ctx, _iap_convert_play_status(status));
+}
+
+void iap_on_volume(int volume) {
+    if(!iap_initialized) {
+        return;
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+    iap_notify_volume(ctx, _iap_convert_volume(volume), iap_false);
+}
+
+void iap_on_shuffle_state(bool state) {
+    if(!iap_initialized) {
+        return;
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+    iap_notify_shuffle_state(ctx, _iap_convert_shuffle_state(state));
+}
+
+void iap_on_repeat_state(int state) {
+    if(!iap_initialized) {
+        return;
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(false);
+    iap_notify_repeat_state(ctx, _iap_convert_repeat_state(state));
+}
diff --git a/firmware/usbstack/iap/platform-macros.h b/firmware/usbstack/iap/platform-macros.h
new file mode 100644
index 0000000000..6b1eb8a5ab
--- /dev/null
+++ b/firmware/usbstack/iap/platform-macros.h
@@ -0,0 +1,35 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#pragma once
+#include <usb_drv.h>
+
+#include "debug.h"
+
+#if DEBUG_ENABLE_INFO == 1
+#define IAP_LOGF(fmt, ...) logf("%lu " fmt, iap_debug_timestamp() __VA_OPT__(, __VA_ARGS__))
+#endif
+
+#if DEBUG_ENABLE_ERROR == 1
+#define IAP_ERRORF(fmt, ...) logf("%lu " fmt, iap_debug_timestamp() __VA_OPT__(, __VA_ARGS__))
+#endif
+
+#define IAP_ARTWORK_WIDTH  128
+#define IAP_ARTWORK_HEIGHT 128
+#define IAP_COLOR_ARTWORK  iap_true
diff --git a/firmware/usbstack/iap/platform.c b/firmware/usbstack/iap/platform.c
new file mode 100644
index 0000000000..1e5ce22a9e
--- /dev/null
+++ b/firmware/usbstack/iap/platform.c
@@ -0,0 +1,482 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include "audio.h"
+#include "buffering.h"
+#include "core_alloc.h"
+#include "metadata.h"
+#include "misc.h"
+#include "pcm_mixer.h"
+#include "pcm_sink.h"
+#include "playback.h"
+#include "playlist.h"
+#include "powermgmt.h"
+#include "settings.h"
+#include "sound.h"
+#include "usb_drv.h"
+
+#include "../usb_iap.h"
+#include "debug.h"
+#include "libiap/iap.h"
+#include "macros.h"
+#include "platform.h"
+
+void* iap_platform_malloc(struct IAPContext* iap_ctx, size_t size, int flags) {
+    struct Platform* plt = iap_ctx->platform;
+    for(size_t i = 0; i < ARRAYLEN(plt->malloc_results); i += 1) {
+        if(plt->malloc_results[i].ptr != NULL) {
+            continue;
+        }
+        struct IAPAllocResult result;
+        if(flags & IAPPlatformMallocFlags_Uncached) {
+            check_act(iap_alloc_usb_send_buffer(size, &result), return NULL);
+        } else {
+            check_act(iap_alloc_buffer(size, &result), return NULL);
+        }
+        plt->malloc_results[i] = result;
+        return result.ptr;
+    }
+    ERROR("no free malloc slot");
+    return NULL;
+}
+
+void iap_platform_free(struct IAPContext* iap_ctx, void* ptr) {
+    struct Platform* plt = iap_ctx->platform;
+    for(size_t i = 0; i < ARRAYLEN(plt->malloc_results); i += 1) {
+        if(plt->malloc_results[i].ptr == ptr) {
+            core_free(plt->malloc_results[i].handle);
+            plt->malloc_results[i].ptr = NULL;
+            return;
+        }
+    }
+    ERROR("no matching malloc slot");
+}
+
+int iap_platform_send_hid_report(struct IAPContext* iap_ctx, const void* ptr, size_t size) {
+    (void)iap_ctx;
+#if DEBUG_DUMP_TX == 1
+    logf("==== dev ==== %p %u > %d", ptr, size, HID_EP_IN);
+    iap_platform_dump_hex(ptr, MIN(size, 48));
+#endif
+    const int ret = usb_drv_send_nonblocking(HID_EP_IN, (void*)ptr, size);
+    return ret == 0 ? (int)size : ret;
+}
+
+IAPBool iap_platform_get_ipod_serial_num(struct IAPContext* iap_ctx, struct IAPSpan* serial) {
+    (void)iap_ctx;
+    static const char* serial_num = "000000000000";
+    return iap_span_append(serial, serial_num, strlen(serial_num) + 1);
+}
+
+IAPBool iap_platform_get_play_status(struct IAPContext* iap_ctx, struct IAPPlatformPlayStatus* status) {
+    struct Platform* plt = iap_ctx->platform;
+
+    status->state = _iap_convert_play_status(audio_status());
+    if(status->state == IAPIPodStatePlayStatus_PlaybackStopped) {
+        return iap_true;
+    }
+
+    struct mp3entry* id3 = audio_current_track();
+    check_act(id3 != NULL, return iap_false);
+    status->track_total_ms = id3->length;
+    status->track_pos_ms   = id3->elapsed;
+    status->track_index    = playlist_get_display_index() - 1;
+    status->track_count    = playlist_amount();
+    status->track_caps     = IAPIPodStateTrackCapBits_HasReleaseDate;
+    if(plt->aa_slot >= 0 && playback_current_aa_hid(plt->aa_slot) >= 0) {
+        status->track_caps |= IAPIPodStateTrackCapBits_HasAlbumArts;
+    }
+
+    return iap_true;
+}
+
+void iap_platform_control(struct IAPContext* iap_ctx, enum IAPPlatformControl control, struct IAPPlatformPendingControl pending) {
+    struct Platform* plt = iap_ctx->platform;
+
+    long button = BUTTON_NONE;
+    switch(control) {
+    case IAPPlatformControl_TogglePlayPause:
+        button = BUTTON_MULTIMEDIA_PLAYPAUSE;
+        break;
+    case IAPPlatformControl_Play:
+        if(audio_status() != AUDIO_STATUS_PLAY) {
+            button = BUTTON_MULTIMEDIA_PLAYPAUSE;
+        }
+        break;
+    case IAPPlatformControl_Pause:
+        if(audio_status() == AUDIO_STATUS_PLAY) {
+            button = BUTTON_MULTIMEDIA_PLAYPAUSE;
+        }
+        break;
+    case IAPPlatformControl_Stop:
+        button = BUTTON_MULTIMEDIA_STOP;
+        break;
+    case IAPPlatformControl_Next:
+        button = BUTTON_MULTIMEDIA_NEXT;
+        break;
+    case IAPPlatformControl_Prev:
+        button = BUTTON_MULTIMEDIA_PREV;
+        break;
+    case IAPPlatformControl_VolumeUp:
+        adjust_volume(1);
+        break;
+    case IAPPlatformControl_VolumeDown:
+        adjust_volume(-1);
+        break;
+    case IAPPlatformControl_ToggleMute:
+        /* rockbox has no mute/unmute */
+        break;
+    }
+    logf("control=%d button=0x%04lX", control, button);
+
+    IAPBool ret = iap_true;
+    if(button == BUTTON_NONE) {
+        goto exit;
+    }
+    check_act(button_queue_try_post(button, 0), ret = false);
+    if(ret && button == BUTTON_MULTIMEDIA_PLAYPAUSE && audio_status() == 0) {
+        /* we are transitioning to stopped to playing, which may take time.
+         * to maintain synchronization with accessories, do not send an ack until playback actually begins. */
+        plt->control_pending = true;
+        plt->pending_control = pending;
+        return;
+    }
+exit:
+    check_act(iap_control_response(iap_ctx, pending, ret), );
+}
+
+static uint8_t normalize_8(int val, int min, int max) {
+    return 0xFF * (val - min) / (max - min);
+}
+
+IAPBool iap_platform_get_volume(struct IAPContext* iap_ctx, struct IAPPlatformVolumeStatus* status) {
+    (void)iap_ctx;
+
+    status->volume = _iap_convert_volume(global_status.volume);
+    status->muted  = iap_false;
+    return iap_true;
+}
+
+IAPBool iap_platform_get_power_status(struct IAPContext* iap_ctx, struct IAPPlatformPowerStatus* status) {
+    (void)iap_ctx;
+    status->battery_level = _iap_convert_battery_level(battery_level());
+    status->state         = _iap_convert_charge_status(charge_state);
+    return iap_true;
+}
+
+IAPBool iap_platform_get_shuffle_setting(struct IAPContext* iap_ctx, uint8_t* status) {
+    (void)iap_ctx;
+    *status = _iap_convert_shuffle_state(global_settings.playlist_shuffle);
+    return iap_true;
+}
+
+IAPBool iap_platform_set_shuffle_setting(struct IAPContext* iap_ctx, uint8_t status) {
+    (void)iap_ctx;
+
+    if(status == IAPIPodStateShuffleSettingState_Tracks && !global_settings.playlist_shuffle) {
+        global_settings.playlist_shuffle = true;
+        settings_save();
+        if(audio_status() & AUDIO_STATUS_PLAY) {
+            playlist_randomise(NULL, current_tick, true);
+        }
+    } else if(status == IAPIPodStateShuffleSettingState_Off && global_settings.playlist_shuffle) {
+        global_settings.playlist_shuffle = false;
+        settings_save();
+        if(audio_status() & AUDIO_STATUS_PLAY) {
+            playlist_sort(NULL, true);
+        }
+    }
+    return iap_true;
+}
+
+IAPBool iap_platform_get_repeat_setting(struct IAPContext* iap_ctx, uint8_t* status) {
+    (void)iap_ctx;
+    *status = _iap_convert_repeat_state(global_settings.repeat_mode);
+    return iap_true;
+}
+
+IAPBool iap_platform_set_repeat_setting(struct IAPContext* iap_ctx, uint8_t status) {
+    (void)iap_ctx;
+
+    /* there are more repeat options in Rockbox rather than iAP.
+     * e.g. RB Repeat One and AB are both mapped to iAP Repeat One.
+     * so we should not accept iAP Repeat One as RB Repeat One, if we have
+     * RB Repeat AB set. */
+    if(_iap_convert_repeat_state(global_settings.repeat_mode) == status) {
+        /* already the same (or equievalent) mode set */
+        return iap_true;
+    }
+
+    static const uint8_t table[] = {
+        [IAPIPodStateRepeatSettingState_Off] = REPEAT_OFF,
+        [IAPIPodStateRepeatSettingState_One] = REPEAT_ONE,
+        [IAPIPodStateRepeatSettingState_All] = REPEAT_ALL,
+    };
+    check_act(status < ARRAY_SIZE(table), return iap_false);
+    global_settings.repeat_mode = table[status];
+    settings_save();
+    if(audio_status() & AUDIO_STATUS_PLAY) {
+        audio_flush_and_reload_tracks();
+    }
+    return iap_true;
+}
+
+IAPBool iap_platform_get_date_time(struct IAPContext* iap_ctx, struct IAPDateTime* time) {
+    (void)iap_ctx;
+    _iap_convert_datetime(get_time(), time);
+    return iap_true;
+}
+
+IAPBool iap_platform_get_backlight_level(struct IAPContext* iap_ctx, uint8_t* level) {
+    (void)iap_ctx;
+
+#ifdef HAVE_BACKLIGHT_BRIGHTNESS
+    *level = normalize_8(global_settings.brightness, MIN_BRIGHTNESS_SETTING, MAX_BRIGHTNESS_SETTING);
+#else
+    *level = 0xFF;
+#endif
+    return iap_true;
+}
+
+IAPBool iap_platform_get_hold_switch_state(struct IAPContext* iap_ctx, IAPBool* state) {
+    (void)iap_ctx;
+
+#ifdef HAS_BUTTON_HOLD
+    *state = button_hold();
+#else
+    *state = iap_false;
+#endif
+    return iap_true;
+}
+
+/* taken from iap-core.c */
+static void get_trackinfo(const unsigned int track, struct mp3entry* id3) {
+    int tracknum = track;
+    tracknum += playlist_get_first_index(NULL);
+    if(tracknum >= playlist_amount()) {
+        tracknum -= playlist_amount();
+    }
+
+    if(playlist_next(0) != tracknum) {
+        struct playlist_track_info info;
+        playlist_get_track_info(NULL, tracknum, &info);
+        get_metadata(id3, -1, info.filename);
+    } else {
+        memcpy(id3, audio_current_track(), sizeof(*id3));
+    }
+}
+
+IAPBool iap_platform_get_indexed_track_info(struct IAPContext* iap_ctx, uint32_t index, struct IAPPlatformTrackInfo* info) {
+    struct Platform* plt = iap_ctx->platform;
+
+    struct playlist_track_info track;
+    struct mp3entry            id3;
+    check_act(playlist_get_track_info(NULL, index, &track) == 0, return iap_false);
+    get_trackinfo(index, &id3);
+
+    if(info->total_ms != NULL) {
+        *info->total_ms = id3.length;
+    }
+    if(info->caps != NULL) {
+        *info->caps = IAPIPodStateTrackCapBits_HasReleaseDate;
+        /* FIXME: respect index */
+        if(plt->aa_slot >= 0 && playback_current_aa_hid(plt->aa_slot) >= 0) {
+            *info->caps |= IAPIPodStateTrackCapBits_HasAlbumArts;
+        }
+    }
+    if(info->release_date != NULL) {
+        info->release_date->year    = id3.year;
+        info->release_date->month   = 0;
+        info->release_date->day     = 0;
+        info->release_date->hour    = 0;
+        info->release_date->minute  = 0;
+        info->release_date->seconds = 0;
+    }
+    if(info->artist != NULL) {
+        check_act(iap_span_append(info->artist, id3.artist, strlen(id3.artist) + 1), return iap_false);
+    }
+    if(info->composer != NULL) {
+        check_act(iap_span_append(info->composer, id3.composer, strlen(id3.composer) + 1), return iap_false);
+    }
+    if(info->album != NULL) {
+        check_act(iap_span_append(info->album, id3.album, strlen(id3.album) + 1), return iap_false);
+    }
+    if(info->title != NULL) {
+        check_act(iap_span_append(info->title, id3.title, strlen(id3.title) + 1), return iap_false);
+    }
+    return iap_true;
+}
+
+IAPBool iap_platform_set_playing_track(struct IAPContext* iap_ctx, uint32_t index) {
+    (void)iap_ctx;
+    audio_skip((int)index - playlist_next(0));
+    return iap_true;
+}
+
+IAPBool iap_platform_open_artwork(struct IAPContext* iap_ctx, uint32_t index, struct IAPPlatformArtwork* artwork) {
+    struct Platform* plt = iap_ctx->platform;
+    /* only aa for currently playing track is available */
+    check_act((int)index == playlist_get_display_index() - 1, return iap_false);
+    const int hid = playback_current_aa_hid(plt->aa_slot);
+    check_act(hid >= 0, return iap_false, "%d %d", plt->aa_slot, hid);
+    struct bitmap* bmp;
+    check_act(bufgetdata(hid, 0, (void*)&bmp) > 0, return iap_false);
+    artwork->color  = iap_true;
+    artwork->width  = bmp->width;
+    artwork->height = bmp->height;
+    artwork->opaque = hid;
+    return iap_true;
+}
+
+IAPBool iap_platform_get_artwork_ptr(struct IAPContext* iap_ctx, struct IAPPlatformArtwork* artwork, struct IAPSpan* span) {
+    struct Platform* plt = iap_ctx->platform;
+
+    /* check the albumart has not reloaded */
+    /* FIXME: not a correct check due to possibility of hid confliction */
+    const int hid = playback_current_aa_hid(plt->aa_slot);
+    check_act(hid == (int)artwork->opaque, return iap_false);
+
+    struct bitmap* bmp;
+    /* more checks */
+    check_act(bufgetdata(hid, 0, (void*)&bmp) > 0, return iap_false);
+    check_act(bmp->width == artwork->width && bmp->height == artwork->height, return iap_false);
+
+    span->ptr  = bmp->data;
+    span->size = bmp->width * bmp->height * 2;
+    return iap_true;
+}
+
+IAPBool iap_platform_close_artwork(struct IAPContext* iap_ctx, struct IAPPlatformArtwork* artwork) {
+    (void)iap_ctx;
+    (void)artwork;
+    return iap_true;
+}
+
+void iap_platform_dump_hex(const void* ptr, size_t size) {
+    if(ptr == NULL) {
+        logf("(null)");
+        return;
+    }
+
+#if DEBUG_HEXDUMP_NOLIMIT != 1
+    size = MIN(size, 32);
+#endif
+
+    static const char chars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+    char line[4 * (8 + 1) + 1];
+    for(size_t l = 0; l * 16 < size; l += 1) {
+        char* c = line;
+        for(size_t b = 0; b < 4; b += 1) {
+            for(size_t i = 0; i < 4; i += 1) {
+                size_t index = l * 16 + b * 4 + i;
+                if(index >= size) {
+                    break;
+                }
+                *c++ = chars[((uint8_t*)ptr)[index] >> 4];
+                *c++ = chars[((uint8_t*)ptr)[index] & 0xF];
+            }
+            *c++ = ' ';
+        }
+        *c++ = '\0';
+        logf("%04X: %s", l * 16, line);
+    }
+}
+
+IAPBool iap_platform_on_acc_samprs_received(struct IAPContext* iap_ctx, struct IAPSpan* samprs) {
+    (void)iap_ctx;
+
+    bool has_44k = false;
+    bool has_48k = false;
+    while(samprs->size > 0) {
+        uint32_t sample_rate;
+        check_act(iap_span_read_32(samprs, &sample_rate), return iap_false);
+        has_44k |= sample_rate == SAMPR_44;
+        has_48k |= sample_rate == SAMPR_48;
+    }
+    check_act(has_44k && has_48k, return iap_false, "accessory lacks mandatory freq support: 44k=%d 48k=%d", has_44k, has_48k);
+    check_act(mixer_switch_sink(PCM_SINK_IAP), return false);
+    return iap_true;
+}
+
+uint8_t _iap_convert_play_status(int rb_audio_status) {
+    if(rb_audio_status & AUDIO_STATUS_PAUSE) {
+        return IAPIPodStatePlayStatus_PlaybackPaused;
+    } else if(rb_audio_status & AUDIO_STATUS_PLAY) {
+        return IAPIPodStatePlayStatus_Playing;
+    } else {
+        return IAPIPodStatePlayStatus_PlaybackStopped;
+    }
+}
+
+uint8_t _iap_convert_volume(int rb_volume) {
+    return normalize_8(rb_volume, sound_min(SOUND_VOLUME), sound_max(SOUND_VOLUME));
+}
+
+uint8_t _iap_convert_shuffle_state(bool rb_state) {
+    return rb_state ? IAPIPodStateShuffleSettingState_Tracks : IAPIPodStateShuffleSettingState_Off;
+}
+
+uint8_t _iap_convert_repeat_state(int rb_state) {
+    static const uint8_t table[] = {
+        [REPEAT_OFF]     = IAPIPodStateRepeatSettingState_Off,
+        [REPEAT_ALL]     = IAPIPodStateRepeatSettingState_All,
+        [REPEAT_ONE]     = IAPIPodStateRepeatSettingState_One,
+        [REPEAT_SHUFFLE] = IAPIPodStateRepeatSettingState_All,
+#ifdef AB_REPEAT_ENABLE
+        [REPEAT_AB] = IAPIPodStateRepeatSettingState_One,
+#endif
+    };
+
+    if(rb_state == REPEAT_OFF && global_settings.next_folder) {
+        /* when repeat is off and next folder is enabled, report repeat all.
+         * without this hack, the accessory will likely set repeat to all,
+         * resulting in trapping us in the current directory. */
+        return IAPIPodStateRepeatSettingState_All;
+    } else {
+        check_act(rb_state < ARRAY_SIZE(table), return iap_false);
+        return table[rb_state];
+    }
+}
+
+uint8_t _iap_convert_battery_level(int rb_battery_level) {
+    return 0xFF * rb_battery_level / 100;
+}
+
+uint8_t _iap_convert_charge_status(enum charge_state_type rb_charge_state) {
+    switch(rb_charge_state) {
+    case CHARGING:
+        return IAPIPodStatePowerState_ExternalCharging;
+    case TOPOFF:
+    case TRICKLE:
+        return IAPIPodStatePowerState_ExternalCharged;
+        break;
+    default:
+        return IAPIPodStatePowerState_Internal;
+    }
+}
+
+void _iap_convert_datetime(struct tm* rb_time, struct IAPDateTime* time) {
+    time->year    = rb_time->tm_year + 1900;
+    time->month   = rb_time->tm_mon + 1;
+    time->day     = rb_time->tm_mday;
+    time->hour    = rb_time->tm_hour;
+    time->minute  = rb_time->tm_min;
+    time->seconds = rb_time->tm_sec;
+}
diff --git a/firmware/usbstack/iap/platform.h b/firmware/usbstack/iap/platform.h
new file mode 100644
index 0000000000..2707722b04
--- /dev/null
+++ b/firmware/usbstack/iap/platform.h
@@ -0,0 +1,49 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#pragma once
+#include <stdint.h>
+
+#include "libiap/datetime.h"
+#include "libiap/platform.h"
+#include "powermgmt.h"
+#include "time.h"
+
+#include "buffer.h"
+
+/* usb_iap.c */
+struct IAPContext* _iap_acquire_ctx(bool lock);
+void               _iap_release_ctx(void);
+
+struct Platform {
+    struct IAPAllocResult            malloc_results[4]; /* allow up to 4 mallocs */
+    struct IAPPlatformPendingControl pending_control;
+    bool                             control_pending;
+
+    int aa_slot;
+};
+
+/* helper functions */
+uint8_t _iap_convert_play_status(int rb_audio_status);
+uint8_t _iap_convert_volume(int rb_volume);
+uint8_t _iap_convert_shuffle_state(bool rb_state);
+uint8_t _iap_convert_repeat_state(int rb_state);
+uint8_t _iap_convert_battery_level(int rb_battery_level);
+uint8_t _iap_convert_charge_status(enum charge_state_type rb_charge_state);
+void    _iap_convert_datetime(struct tm* rb_time, struct IAPDateTime* time);
diff --git a/firmware/usbstack/usb_core.c b/firmware/usbstack/usb_core.c
index 9b90fbd5ad..ba4bd82145 100644
--- a/firmware/usbstack/usb_core.c
+++ b/firmware/usbstack/usb_core.c
@@ -53,6 +53,10 @@
 #include "usb_audio_def.h" // DEBUG
 #endif
 
+#ifdef USB_ENABLE_IAP
+#include "usb_iap.h"
+#endif
+
 /* TODO: Move target-specific stuff somewhere else (serial number reading) */
 
 #if defined(IPOD_ARCH) && defined(CPU_PP)
@@ -72,7 +76,11 @@
 #define USB_MAX_CURRENT 500
 #endif
 
+#ifdef USB_ENABLE_IAP
+#define NUM_CONFIGS 2
+#else
 #define NUM_CONFIGS 1
+#endif
 
 /*-------------------------------------------------------------------------*/
 /* USB protocol descriptors: */
@@ -107,7 +115,7 @@ static struct usb_config_descriptor __attribute__((aligned(2)))
     .bDescriptorType     = USB_DT_CONFIG,
     .wTotalLength        = 0, /* will be filled in later */
     .bNumInterfaces      = 1,
-    .bConfigurationValue = 1,
+    .bConfigurationValue = 0, /* will be filled in later */
     .iConfiguration      = 0,
     .bmAttributes        = USB_CONFIG_ATT_ONE | USB_CONFIG_ATT_SELFPOWER,
     .bMaxPower           = (USB_MAX_CURRENT + 1) / 2, /* In 2mA units */
@@ -303,6 +311,30 @@ static struct usb_class_driver drivers[USB_NUM_DRIVERS] =
         .get_interface = usb_audio_get_interface,
     },
 #endif
+#ifdef USB_ENABLE_IAP
+    [USB_DRIVER_IAP] = {
+        .enabled = false,
+        .needs_exclusive_storage = false,
+        .config = 2,
+        .first_interface = 0,
+        .last_interface = 0,
+        .ep_allocs_size = ARRAYLEN(usb_iap_ep_allocs),
+        .ep_allocs = usb_iap_ep_allocs,
+        .set_first_interface = usb_iap_set_first_interface,
+        .get_config_descriptor = usb_iap_get_config_descriptor,
+        .init_connection = usb_iap_init_connection,
+        .init = usb_iap_init,
+        .disconnect = usb_iap_disconnect,
+        .transfer_complete = usb_iap_transfer_complete,
+        .fast_transfer_complete = usb_iap_fast_transfer_complete,
+        .control_request = usb_iap_control_request,
+#ifdef HAVE_HOTSWAP
+        .notify_hotswap = NULL,
+#endif
+        .set_interface = usb_iap_set_interface,
+        .get_interface = usb_iap_get_interface,
+    },
+#endif
 };
 
 #ifdef USB_LEGACY_CONTROL_API
diff --git a/firmware/usbstack/usb_iap.c b/firmware/usbstack/usb_iap.c
new file mode 100644
index 0000000000..3126ec6bc3
--- /dev/null
+++ b/firmware/usbstack/usb_iap.c
@@ -0,0 +1,679 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#include "audio.h"
+#include "pcm_mixer.h"
+#include "pcm_sink.h"
+#include "playback.h"
+#include "powermgmt.h"
+#include "timefuncs.h"
+#include "usb_drv.h"
+
+#include "iap/audio.h"
+#include "iap/libiap/iap.h"
+#include "iap/libiap/platform.h"
+#include "iap/macros.h"
+#include "iap/platform-macros.h"
+#include "iap/platform.h"
+#include "usb_audio_def.h"
+#include "usb_class_driver.h"
+#include "usb_hid_def.h"
+#include "usb_iap.h"
+
+struct usb_class_driver_ep_allocation usb_iap_ep_allocs[2] = {
+    /* uac input */
+    {.type = USB_ENDPOINT_XFER_ISOC, .dir = DIR_IN, .optional = false},
+    /* hid input */
+    {.type = USB_ENDPOINT_XFER_INT, .dir = DIR_IN, .optional = false},
+};
+
+/* interface 0 (audio control) */
+static struct usb_interface_descriptor ipod_audio_control_desc = {
+    .bLength            = USB_DT_INTERFACE_SIZE,
+    .bDescriptorType    = USB_DT_INTERFACE,
+    .bInterfaceNumber   = -1, /* dynamic */
+    .bAlternateSetting  = 0,
+    .bNumEndpoints      = 0,
+    .bInterfaceClass    = USB_CLASS_AUDIO,
+    .bInterfaceSubClass = USB_SUBCLASS_AUDIO_CONTROL,
+};
+
+static struct usb_ac_header ipod_audio_control_uac_header = {
+    .bLength            = USB_AC_SIZEOF_HEADER(1),
+    .bDescriptorType    = USB_DT_CS_INTERFACE,
+    .bDescriptorSubType = USB_AC_HEADER,
+    .bcdADC             = 0x0100, /* 1.00 */
+    .wTotalLength       = -1,     /* dynamic */
+    .bInCollection      = 1,
+    .baInterfaceNr      = {-1}, /* dynamic */
+};
+
+static struct usb_ac_input_terminal ipod_audio_control_uac_input_terminal = {
+    .bLength            = sizeof(struct usb_ac_input_terminal),
+    .bDescriptorType    = USB_DT_CS_INTERFACE,
+    .bDescriptorSubType = USB_AC_INPUT_TERMINAL,
+    .bTerminalId        = 1,
+    .wTerminalType      = USB_AC_INPUT_TERMINAL_MICROPHONE,
+    .bAssocTerminal     = 2, /* ipod_audio_control_uac_output_terminal */
+    .bNrChannels        = 2,
+    .wChannelConfig     = USB_AC_CHANNELS_LEFT_RIGHT_FRONT,
+};
+
+static struct usb_ac_output_terminal ipod_audio_control_uac_output_terminal = {
+    .bLength            = sizeof(struct usb_ac_output_terminal),
+    .bDescriptorType    = USB_DT_CS_INTERFACE,
+    .bDescriptorSubType = USB_AC_OUTPUT_TERMINAL,
+    .bTerminalId        = 2,
+    .wTerminalType      = USB_AC_TERMINAL_STREAMING,
+    .bAssocTerminal     = 1, /* ipod_audio_control_uac_input_terminal */
+    .bSourceId          = 1,
+};
+
+/* interface 1 (audio stream) */
+/* interface 1 alt 0 */
+static struct usb_interface_descriptor ipod_audio_stream_0_desc = {
+    .bLength            = USB_DT_INTERFACE_SIZE,
+    .bDescriptorType    = USB_DT_INTERFACE,
+    .bInterfaceNumber   = -1, /* dynamic */
+    .bAlternateSetting  = 0,
+    .bNumEndpoints      = 0,
+    .bInterfaceClass    = USB_CLASS_AUDIO,
+    .bInterfaceSubClass = USB_SUBCLASS_AUDIO_STREAMING,
+};
+
+/* interface 1 alt 1 */
+static struct usb_interface_descriptor ipod_audio_stream_1_desc = {
+    .bLength            = USB_DT_INTERFACE_SIZE,
+    .bDescriptorType    = USB_DT_INTERFACE,
+    .bInterfaceNumber   = -1, /* dynamic */
+    .bAlternateSetting  = 1,
+    .bNumEndpoints      = 1,
+    .bInterfaceClass    = USB_CLASS_AUDIO,
+    .bInterfaceSubClass = USB_SUBCLASS_AUDIO_STREAMING,
+};
+
+static struct usb_as_interface ipod_audio_stream_1_uac_header = {
+    .bLength            = sizeof(struct usb_as_interface),
+    .bDescriptorType    = USB_DT_CS_INTERFACE,
+    .bDescriptorSubType = USB_AS_GENERAL,
+    .bTerminalLink      = 2, /* ipod_audio_control_uac_output_terminal */
+    .bDelay             = 1,
+    .wFormatTag         = USB_AS_FORMAT_TYPE_I_PCM,
+};
+
+/* TODO: remove unsupported freqs */
+static struct usb_as_format_type_i_discrete ipod_audio_stream_1_uac_discrete = {
+    .bLength            = USB_AS_SIZEOF_FORMAT_TYPE_I_DISCRETE(9),
+    .bDescriptorType    = USB_DT_CS_INTERFACE,
+    .bDescriptorSubType = USB_AS_FORMAT_TYPE,
+    .bFormatType        = USB_AS_FORMAT_TYPE_I,
+    .bNrChannels        = 2,
+    .bSubframeSize      = 2, /* bBitResolution / 8 */
+    .bBitResolution     = 16,
+    .bSamFreqType       = 9,
+    .tSamFreq           = {
+        {0x40, 0x1F, 0x00}, /* 8000  */
+        {0x11, 0x2B, 0x00}, /* 11025 */
+        {0xE0, 0x2E, 0x00}, /* 12000 */
+        {0x80, 0x3E, 0x00}, /* 16000 */
+        {0x22, 0x56, 0x00}, /* 22050 */
+        {0xC0, 0x5D, 0x00}, /* 24000 */
+        {0x00, 0x7D, 0x00}, /* 32000 */
+        {0x44, 0xAC, 0x00}, /* 44100 */
+        {0x80, 0xBB, 0x00}, /* 48000 */
+    },
+};
+
+/* interface 1 endpoint 0 */
+static struct usb_as_iso_audio_endpoint ipod_audio_stream_1_endpoint = {
+    .bLength          = USB_DT_ENDPOINT_AUDIO_SIZE,
+    .bDescriptorType  = USB_DT_ENDPOINT,
+    .bEndpointAddress = -1, /* dynamic */
+    .bmAttributes     = USB_ENDPOINT_XFER_ISOC,
+    .wMaxPacketSize   = AS_PACKET_SIZE,
+    .bInterval        = -1, /* dynamic */
+    .bRefresh         = 0,
+    .bSynchAddress    = 0,
+};
+
+static struct usb_as_iso_ctrldata_endpoint ipod_audio_stream_1_endpoint_uac = {
+    .bLength            = sizeof(struct usb_as_iso_ctrldata_endpoint),
+    .bDescriptorType    = USB_DT_CS_ENDPOINT,
+    .bDescriptorSubType = USB_AS_EP_GENERAL,
+    .bmAttributes       = USB_AS_EP_CS_SAMPLING_FREQ_CTL,
+    .bLockDelayUnits    = 0,
+    .wLockDelay         = 0,
+};
+
+/* interface 2 (hid) */
+static struct usb_interface_descriptor ipod_hid_desc = {
+    .bLength            = USB_DT_INTERFACE_SIZE,
+    .bDescriptorType    = USB_DT_INTERFACE,
+    .bInterfaceNumber   = -1, /* dynamic */
+    .bAlternateSetting  = 0,
+    .bNumEndpoints      = 1,
+    .bInterfaceClass    = USB_CLASS_HID,
+    .bInterfaceSubClass = 0,
+};
+
+#define INPUT_REPORT(id, count) 0x09, 0x01,      /* Usage 0x01 */   \
+                                0x85, id,        /* Report ID */    \
+                                0x95, count,     /* Report Count */ \
+                                0x82, 0x02, 0x01 /* Input 0x0102 (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Buffered Bytes) */
+
+#define OUTPUT_REPORT(id, count) 0x09, 0x01,      /* Usage 0x01 */   \
+                                 0x85, id,        /* Report ID */    \
+                                 0x95, count,     /* Report Count */ \
+                                 0x92, 0x02, 0x01 /* Output 0x0102 (...) */
+
+#define INPUT_REPORT2(id, count1, count2) 0x09, 0x01,           /* Usage 0x01 */   \
+                                          0x85, id,             /* Report ID */    \
+                                          0x96, count1, count2, /* Report Count */ \
+                                          0x82, 0x02, 0x01      /* Input 0x0102 (...) */
+
+#define OUTPUT_REPORT2(id, count1, count2) 0x09, 0x01,           /* Usage 0x01 */   \
+                                           0x85, id,             /* Report ID */    \
+                                           0x96, count1, count2, /* Report Count */ \
+                                           0x92, 0x02, 0x01      /* Output 0x0102 (...) */
+
+// clang-format off
+static const uint8_t ipod_hid_report_fs[] = {
+    0x06, 0x00, 0xFF, /* Usage Page 0xFF00 (Vendor-defined) */
+    0x09, 0x01,       /* Usage 0x01 */
+    0xA1, 0x01,       /* Collection 0x01 (Application) */
+    0x75, 0x08,       /* Report Size 0x08 */
+    0x26, 0x80, 0x00, /* Logical Maximum 0x0081 (128) */
+    0x15, 0x00,       /* Logical Minumum 0x0000 (0) */
+
+    INPUT_REPORT(0x01, 0x0C),
+    INPUT_REPORT(0x02, 0x0E),
+    INPUT_REPORT(0x03, 0x14),
+    INPUT_REPORT(0x04, 0x3F),
+
+    OUTPUT_REPORT(0x05, 0x08),
+    OUTPUT_REPORT(0x06, 0x0A),
+    OUTPUT_REPORT(0x07, 0x0E),
+    OUTPUT_REPORT(0x08, 0x14),
+    OUTPUT_REPORT(0x09, 0x3F),
+
+    0xC0, /* End Collection */
+};
+
+static const uint8_t ipod_hid_report_hs[] = {
+    0x06, 0x00, 0xFF, /* Usage Page 0xFF00 (Vendor-defined) */
+    0x09, 0x01,       /* Usage 0x01 */
+    0xA1, 0x01,       /* Collection 0x01 (Application) */
+    0x75, 0x08,       /* Report Size 0x08 */
+    0x26, 0x80, 0x00, /* Logical Maximum 0x0081 (128) */
+    0x15, 0x00,       /* Logical Minumum 0x0000 (0) */
+
+    INPUT_REPORT(0x01, 0x05),
+    INPUT_REPORT(0x02, 0x09),
+    INPUT_REPORT(0x03, 0x0D),
+    INPUT_REPORT(0x04, 0x11),
+    INPUT_REPORT(0x05, 0x19),
+    INPUT_REPORT(0x06, 0x31),
+    INPUT_REPORT(0x07, 0x5F),
+    INPUT_REPORT(0x08, 0xC1),
+    INPUT_REPORT2(0x09, 0x01, 0x01),
+    INPUT_REPORT2(0x0A, 0x81, 0x01),
+    INPUT_REPORT2(0x0B, 0x01, 0x02),
+    INPUT_REPORT2(0x0C, 0xFF, 0x02),
+
+    OUTPUT_REPORT(0x0D, 0x05),
+    OUTPUT_REPORT(0x0E, 0x09),
+    OUTPUT_REPORT(0x1F, 0x0D),
+    OUTPUT_REPORT(0x10, 0x11),
+    OUTPUT_REPORT(0x11, 0x19),
+    OUTPUT_REPORT(0x12, 0x31),
+    OUTPUT_REPORT(0x13, 0x5F),
+    OUTPUT_REPORT(0x14, 0xC1),
+    OUTPUT_REPORT(0x15, 0xFF),
+
+    0xC0, /* End Collection */
+};
+// clang-format on
+
+static struct usb_hid_descriptor ipod_hid_hid_desc = {
+    .bLength            = sizeof(struct usb_hid_descriptor),
+    .bDescriptorType    = USB_DT_HID,
+    .wBcdHID            = 0x0111, /* 1.11 */
+    .bCountryCode       = 0,
+    .bNumDescriptors    = 1,
+    .bDescriptorType0   = USB_DT_REPORT,
+    .wDescriptorLength0 = -1, /* dynamic */
+};
+
+/* interface 2 endpoint 0 */
+static struct usb_endpoint_descriptor ipod_hid_endpoint = {
+    .bLength          = USB_DT_ENDPOINT_SIZE,
+    .bDescriptorType  = USB_DT_ENDPOINT,
+    .bEndpointAddress = -1, /* dynamic */
+    .bmAttributes     = USB_ENDPOINT_XFER_INT,
+    .wMaxPacketSize   = -1, /* dynamic */
+    .bInterval        = 1,
+};
+
+static struct {
+    int interface;
+} ctrl;
+
+static struct {
+    uint32_t sample_rate;
+    int      interface;
+    int8_t   alt;
+} stream;
+
+static struct {
+    int interface;
+} hid;
+
+static struct mutex    iap_ctx_mutex;
+static struct Platform platform;
+static struct timeout  tick_tmo;
+static bool            iap_ctx_mutex_initialized = false;
+
+struct IAPContext* _iap_acquire_ctx(bool lock) {
+    static struct IAPContext ctx;
+    if(lock) {
+        mutex_lock(&iap_ctx_mutex);
+    }
+    return &ctx;
+}
+
+void _iap_release_ctx() {
+    mutex_unlock(&iap_ctx_mutex);
+}
+
+bool iap_initialized;
+
+/* those notifications need pollling */
+static enum charge_state_type last_charge_state;
+static uint8_t                last_battery_level;
+static int8_t                 last_minute;
+static int8_t                 last_hold_switch_state;
+
+enum Notify {
+    Notify_Tick,
+};
+
+static int tick_callback(struct timeout* tmo) {
+    (void)tmo;
+    usb_signal_class_notify(USB_DRIVER_IAP, Notify_Tick);
+    return HZ / 10;
+}
+
+int usb_iap_set_first_interface(int interface) {
+    ctrl.interface   = interface + 0;
+    stream.interface = interface + 1;
+    hid.interface    = interface + 2;
+    return interface + 3;
+}
+
+#define PACK_DESC(desc) pack_data(&dest, &desc, ((struct usb_descriptor_header*)&desc)->bLength)
+
+int usb_iap_get_config_descriptor(unsigned char* dest, int max_packet_size) {
+    (void)max_packet_size;
+
+    unsigned char* orig_dest = dest;
+
+    ipod_audio_control_desc.bInterfaceNumber = ctrl.interface;
+    PACK_DESC(ipod_audio_control_desc);
+    ipod_audio_control_uac_header.baInterfaceNr[0] = stream.interface;
+    ipod_audio_control_uac_header.wTotalLength =
+        sizeof(ipod_audio_control_uac_header) +
+        sizeof(ipod_audio_control_uac_input_terminal) +
+        sizeof(ipod_audio_control_uac_output_terminal);
+    PACK_DESC(ipod_audio_control_uac_header);
+    PACK_DESC(ipod_audio_control_uac_input_terminal);
+    PACK_DESC(ipod_audio_control_uac_output_terminal);
+
+    ipod_audio_stream_0_desc.bInterfaceNumber     = stream.interface;
+    ipod_audio_stream_1_desc.bInterfaceNumber     = stream.interface;
+    ipod_audio_stream_1_endpoint.bEndpointAddress = AS_EP_IN;
+    ipod_audio_stream_1_endpoint.bInterval        = usb_drv_port_speed() ? 4 : 1;
+    PACK_DESC(ipod_audio_stream_0_desc);
+    PACK_DESC(ipod_audio_stream_1_desc);
+    PACK_DESC(ipod_audio_stream_1_uac_header);
+    PACK_DESC(ipod_audio_stream_1_uac_discrete);
+    PACK_DESC(ipod_audio_stream_1_endpoint);
+    PACK_DESC(ipod_audio_stream_1_endpoint_uac);
+
+    ipod_hid_desc.bInterfaceNumber     = hid.interface;
+    ipod_hid_endpoint.bEndpointAddress = HID_EP_IN;
+    if(usb_drv_port_speed()) {
+        ipod_hid_hid_desc.wDescriptorLength0 = sizeof(ipod_hid_report_hs);
+        ipod_hid_endpoint.wMaxPacketSize     = 64;
+    } else {
+        ipod_hid_hid_desc.wDescriptorLength0 = sizeof(ipod_hid_report_fs);
+        ipod_hid_endpoint.wMaxPacketSize     = 64;
+    }
+    PACK_DESC(ipod_hid_desc);
+    PACK_DESC(ipod_hid_hid_desc);
+    PACK_DESC(ipod_hid_endpoint);
+
+    return dest - orig_dest;
+}
+
+void usb_iap_init_connection(void) {
+    stream.sample_rate     = 48000;
+    last_charge_state      = -1;
+    last_minute            = -1;
+    last_hold_switch_state = -1;
+
+    iap_debug_reset_timestamp();
+
+    /* TODO: disable iap on error */
+
+    /* init audio sink */
+    check_act(iap_audio_init(), return);
+
+    /* init libiap */
+    if(!iap_ctx_mutex_initialized) {
+        iap_ctx_mutex_initialized = true;
+        mutex_init(&iap_ctx_mutex);
+    }
+
+    struct IAPContext* ctx = _iap_acquire_ctx(true);
+
+    const struct IAPOpts opts = {
+        .usb_highspeed         = usb_drv_port_speed(),
+        .ignore_hid_report_id  = iap_true,
+        .artwork_single_report = iap_false,
+        .enable_packet_dump    = iap_false,
+    };
+    check_act(iap_init_ctx(ctx, opts, &platform), goto cleanup_audio);
+    _iap_release_ctx();
+
+    /* prepare artwork */
+    struct dim dim   = {IAP_ARTWORK_WIDTH, IAP_ARTWORK_HEIGHT};
+    platform.aa_slot = playback_claim_aa_slot(&dim);
+    if(platform.aa_slot < 0) {
+        ERROR("failed to claim albumart slot");
+    }
+    platform.control_pending = false;
+
+    /* register timer */
+    timeout_register(&tick_tmo, tick_callback, HZ / 10, 0);
+
+    iap_initialized = true;
+    LOG("initialized");
+    return;
+
+cleanup_audio:
+    _iap_release_ctx();
+    iap_audio_deinit();
+}
+
+int usb_iap_set_interface(int intf, int alt) {
+    LOG("set interface interface=%d alt=%d", intf, alt);
+    check_act(intf == stream.interface, return -1);
+    if(alt == 0) {
+        check_act(iap_audio_disable(), return -1);
+    } else if(alt == 1) {
+        check_act(iap_audio_enable(), return -1);
+    } else {
+        ERROR("invalid alt %d", alt);
+        return -1;
+    }
+    return 0;
+}
+
+int usb_iap_get_interface(int intf) {
+    LOG("get interface interface=%d", intf);
+    check_act(intf == stream.interface, return -1);
+    return stream.alt;
+}
+
+void usb_iap_init(void) {
+    LOG("init");
+}
+
+void usb_iap_disconnect(void) {
+    iap_initialized = false;
+    audio_pause();
+    mixer_switch_sink(PCM_SINK_BUILTIN);
+    timeout_cancel(&tick_tmo);
+    if(platform.aa_slot >= 0) {
+        playback_release_aa_slot(platform.aa_slot);
+    }
+    struct IAPContext* ctx = _iap_acquire_ctx(true);
+    check_act(iap_deinit_ctx(ctx), );
+    _iap_release_ctx();
+    check_act(iap_audio_deinit(), );
+    LOG("disconnected");
+}
+
+void usb_iap_transfer_complete(int ep, int dir, int status, int length) {
+    (void)length;
+
+    if((ep | dir) == HID_EP_IN) {
+        check_act(status == 0, return);
+#if DEBUG_DUMP_TX
+        LOG("ep=%d dir=%d state=%d length=%d", ep, dir, status, length);
+#endif
+        struct IAPContext* ctx = _iap_acquire_ctx(true);
+        check_act(iap_notify_send_complete(ctx), );
+        _iap_release_ctx();
+    }
+}
+
+bool usb_iap_fast_transfer_complete(int ep, int dir, int status, int length) {
+    (void)status;
+    (void)length;
+    return (ep | dir) == AS_EP_IN;
+}
+
+static unsigned char ctrl_buf[256] USB_DEVBSS_ATTR;
+
+static void respond_zero(struct usb_ctrlrequest* req) {
+    if(req->wLength > sizeof(ctrl_buf)) {
+        ERROR("required data too long %u > %u", req->wLength, sizeof(ctrl_buf));
+        usb_drv_control_response(USB_CONTROL_STALL, NULL, 0);
+    } else {
+        memset(ctrl_buf, 0, req->wLength);
+        usb_drv_control_response(USB_CONTROL_ACK, ctrl_buf, req->wLength);
+    }
+}
+
+/* returns true when ctrl_buf has received data */
+static bool receive_data(struct usb_ctrlrequest* req, void* reqdata) {
+    if(reqdata == NULL) {
+        /* setup */
+        if(req->wLength > sizeof(ctrl_buf)) {
+            ERROR("parameter too long");
+            usb_drv_control_response(USB_CONTROL_STALL, NULL, 0);
+        } else {
+            usb_drv_control_response(USB_CONTROL_RECEIVE, ctrl_buf, req->wLength);
+        }
+        return false;
+    } else {
+        /* data */
+        return true;
+    }
+}
+
+static bool control_request_if_std(struct usb_ctrlrequest* req, void* reqdata, unsigned char* dest) {
+    (void)reqdata;
+
+    unsigned char* const orig_dest = dest;
+    switch(req->bRequest) {
+    case USB_REQ_GET_DESCRIPTOR: {
+        const uint8_t desc_type  = req->wValue >> 8;
+        const uint8_t desc_index = req->wValue & 0xff;
+        LOG("descriptor request type=%x index=%x", desc_type, desc_index);
+        (void)desc_index;
+        switch(desc_type) {
+        case USB_DT_HID:
+            PACK_DATA(&dest, ipod_hid_hid_desc);
+            break;
+        case USB_DT_REPORT:
+            if(usb_drv_port_speed()) {
+                PACK_DATA(&dest, ipod_hid_report_hs);
+            } else {
+                PACK_DATA(&dest, ipod_hid_report_fs);
+            }
+            break;
+        }
+        if(dest != orig_dest) {
+            usb_drv_control_response(USB_CONTROL_ACK, orig_dest, MIN(dest - orig_dest, req->wLength));
+            return true;
+        }
+    } break;
+    }
+    return false;
+}
+
+static bool control_request_if_class(struct usb_ctrlrequest* req, void* reqdata, unsigned char* dest) {
+    (void)dest;
+
+    const uint8_t recip_interface = req->wIndex & 0xff;
+    if(recip_interface == hid.interface) {
+        switch(req->bRequest) {
+        case USB_HID_GET_REPORT:
+            respond_zero(req);
+            return true;
+        case USB_HID_SET_REPORT: {
+            if(!receive_data(req, reqdata)) {
+                return true;
+            }
+#if DEBUG_DUMP_RX == 1
+            logf("==== acc: %u bytes ====", req->wLength);
+            iap_platform_dump_hex(reqdata, req->wLength);
+#endif
+
+            struct IAPContext* ctx = _iap_acquire_ctx(true);
+            const bool         ret = iap_feed_hid_report(ctx, reqdata, req->wLength);
+            _iap_release_ctx();
+
+            check_act(ret, return false);
+            usb_drv_control_response(USB_CONTROL_ACK, NULL, 0);
+            return true;
+        }
+        case USB_HID_SET_IDLE:
+            usb_drv_control_response(USB_CONTROL_ACK, NULL, 0);
+            return true;
+        }
+    }
+    return false;
+}
+
+static bool control_request_if_endpoint(struct usb_ctrlrequest* req, void* reqdata, unsigned char* dest) {
+    (void)dest;
+
+    LOG("ctrl to endpoint %x (stream=%x, hid=%x)", req->wIndex, AS_EP_IN, HID_EP_IN);
+    if(req->wIndex == AS_EP_IN) {
+        const uint8_t recip_entity     = req->wIndex >> 8;
+        const uint8_t control_selector = req->wValue >> 8;
+        (void)recip_entity;
+        switch(req->bRequest) {
+        case USB_AC_SET_CUR:
+            if(!receive_data(req, reqdata)) {
+                return true;
+            }
+            LOG("audio ctrl to stream endpoint entity=0x%02X request=0x%02X length=%u", recip_entity, req->bRequest, req->wLength);
+            switch(control_selector) {
+            case USB_AS_EP_CS_SAMPLING_FREQ_CTL:
+                check_act(req->wLength == 3, goto stall);
+                stream.sample_rate = ctrl_buf[0] | (ctrl_buf[1] << 8) | (ctrl_buf[2] << 16);
+                LOG("audio stream sampling rate %lu", stream.sample_rate);
+                check_act(iap_audio_set_sampr(stream.sample_rate), goto stall);
+                break;
+            }
+            usb_drv_control_response(USB_CONTROL_ACK, NULL, 0);
+            return true;
+        case USB_AC_GET_CUR:
+            switch(control_selector) {
+            case USB_AS_EP_CS_SAMPLING_FREQ_CTL:
+                check_act(req->wLength == 3, goto stall);
+                ctrl_buf[2] = (stream.sample_rate >> 16) & 0xff;
+                ctrl_buf[1] = (stream.sample_rate >> 8) & 0xff;
+                ctrl_buf[0] = (stream.sample_rate & 0xff);
+                usb_drv_control_response(USB_CONTROL_ACK, ctrl_buf, req->wLength);
+                return true;
+            }
+            /* fallthrough */
+        case USB_AC_GET_MIN:
+        case USB_AC_GET_MAX:
+        case USB_AC_GET_RES:
+            respond_zero(req);
+            return true;
+        stall:
+            usb_drv_control_response(USB_CONTROL_STALL, NULL, 0);
+            return true;
+        }
+    }
+    return false;
+}
+
+bool usb_iap_control_request(struct usb_ctrlrequest* req, void* reqdata, unsigned char* dest) {
+    const uint8_t req_recipient = req->bRequestType & USB_RECIP_MASK;
+    const uint8_t req_type      = req->bRequestType & USB_TYPE_MASK;
+#if 0
+    LOG("bRequestType=%x, bRequest=%x, wValue=%x, wIndex=%x, wLength=%x", req->bRequestType, req->bRequest, req->wValue, req->wIndex, req->wLength);
+    LOG("recip=%x type=%x", req_recipient, req_type);
+#endif
+    if(req_recipient == USB_RECIP_INTERFACE && req_type == USB_TYPE_STANDARD) {
+        return control_request_if_std(req, reqdata, dest);
+    } else if(req_recipient == USB_RECIP_INTERFACE && req_type == USB_TYPE_CLASS) {
+        return control_request_if_class(req, reqdata, dest);
+    } else if(req_recipient == USB_RECIP_ENDPOINT && req_type == USB_TYPE_CLASS) {
+        return control_request_if_endpoint(req, reqdata, dest);
+    }
+    return false;
+}
+
+void usb_iap_notify_event(intptr_t data) {
+    switch(data) {
+    case Notify_Tick: {
+        struct IAPContext* ctx = _iap_acquire_ctx(true);
+        struct Platform*   plt = ctx->platform;
+        if(plt->control_pending) {
+            /* waiting for playback begins */
+            _iap_release_ctx();
+            return;
+        }
+
+        if(last_charge_state < 0 || last_charge_state != charge_state || last_battery_level != battery_level()) {
+            last_charge_state  = charge_state;
+            last_battery_level = battery_level();
+            iap_notify_power_state(ctx, _iap_convert_charge_status(last_charge_state), _iap_convert_battery_level(last_battery_level));
+        }
+
+        struct tm* tm = get_time();
+        if(last_minute == -1 || last_minute != tm->tm_min) {
+            last_minute = tm->tm_min;
+            struct IAPDateTime time;
+            _iap_convert_datetime(get_time(), &time);
+            iap_notify_time_setting(ctx, &time);
+        }
+
+#ifdef HAS_BUTTON_HOLD
+        int8_t hold = button_hold() ? 1 : 0;
+        if(hold != last_hold_switch_state) {
+            last_hold_switch_state = hold;
+            iap_notify_hold_switch_state(ctx, last_hold_switch_state);
+        }
+#endif
+
+        check_act(iap_periodic_tick(ctx), );
+        _iap_release_ctx();
+    } break;
+    }
+}
diff --git a/firmware/usbstack/usb_iap.h b/firmware/usbstack/usb_iap.h
new file mode 100644
index 0000000000..8825d56514
--- /dev/null
+++ b/firmware/usbstack/usb_iap.h
@@ -0,0 +1,39 @@
+/***************************************************************************
+ *             __________               __   ___.
+ *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
+ *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
+ *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
+ *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
+ *                     \/            \/     \/    \/            \/
+ *
+ * Copyright (C) 2025 by Sho Tanimoto
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ ****************************************************************************/
+#pragma once
+#include "usb_core.h"
+#include "usb_class_driver.h"
+
+extern struct usb_class_driver_ep_allocation usb_iap_ep_allocs[2];
+
+int  usb_iap_request_endpoints(struct usb_class_driver*);
+int  usb_iap_set_first_interface(int interface);
+int  usb_iap_get_config_descriptor(unsigned char* dest, int max_packet_size);
+void usb_iap_init_connection(void);
+bool usb_iap_set_alt_interface(int interface, int alt);
+int usb_iap_get_alt_interface(int interface);
+void usb_iap_init(void);
+void usb_iap_disconnect(void);
+void usb_iap_transfer_complete(int ep, int dir, int state, int length);
+bool usb_iap_fast_transfer_complete(int ep, int dir, int status, int length);
+bool usb_iap_control_request(struct usb_ctrlrequest* req, void* reqdata, unsigned char* dest);
+int usb_iap_set_interface(int intf, int alt);
+int usb_iap_get_interface(int intf);
+void usb_iap_notify_event(intptr_t data);
-- 
rockbox-cvs mailing list
[email protected]
https://lists.haxx.se/mailman/listinfo/rockbox-cvs