mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-14 05:47:23 +00:00
Compare commits
1 Commits
develop
...
pager-audi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83889a1c37 |
@@ -39,6 +39,7 @@ enum input_broker_event {
|
|||||||
#define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1
|
#define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1
|
||||||
#define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2
|
#define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2
|
||||||
#define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA
|
#define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA
|
||||||
|
#define INPUT_BROKER_MSG_VOICEMEMO 0xAD
|
||||||
#define INPUT_BROKER_MSG_TAB 0x09
|
#define INPUT_BROKER_MSG_TAB 0x09
|
||||||
#define INPUT_BROKER_MSG_EMOTE_LIST 0x8F
|
#define INPUT_BROKER_MSG_EMOTE_LIST 0x8F
|
||||||
|
|
||||||
|
|||||||
@@ -28,18 +28,18 @@ static unsigned char TCA8418TapMap[_TCA8418_NUM_KEYS][13] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
|
static unsigned char TCA8418LongPressMap[_TCA8418_NUM_KEYS] = {
|
||||||
Key::ESC, // 1
|
Key::ESC, // 1
|
||||||
Key::UP, // 2
|
Key::UP, // 2
|
||||||
Key::NONE, // 3
|
Key::NONE, // 3
|
||||||
Key::LEFT, // 4
|
Key::LEFT, // 4
|
||||||
Key::NONE, // 5
|
Key::NONE, // 5
|
||||||
Key::RIGHT, // 6
|
Key::RIGHT, // 6
|
||||||
Key::NONE, // 7
|
Key::NONE, // 7
|
||||||
Key::DOWN, // 8
|
Key::DOWN, // 8
|
||||||
Key::NONE, // 9
|
Key::NONE, // 9
|
||||||
Key::BSP, // *
|
Key::BSP, // *
|
||||||
Key::NONE, // 0
|
Key::VOICEMEMO, // 0
|
||||||
Key::NONE, // #
|
Key::NONE, // #
|
||||||
};
|
};
|
||||||
|
|
||||||
TCA8418Keyboard::TCA8418Keyboard()
|
TCA8418Keyboard::TCA8418Keyboard()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class TCA8418KeyboardBase
|
|||||||
BT_TOGGLE = 0xAA,
|
BT_TOGGLE = 0xAA,
|
||||||
GPS_TOGGLE = 0x9E,
|
GPS_TOGGLE = 0x9E,
|
||||||
MUTE_TOGGLE = 0xAC,
|
MUTE_TOGGLE = 0xAC,
|
||||||
|
VOICEMEMO = 0xAD,
|
||||||
SEND_PING = 0xAF,
|
SEND_PING = 0xAF,
|
||||||
BL_TOGGLE = 0xAB
|
BL_TOGGLE = 0xAB
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ static unsigned char TDeckProTapMap[_TCA8418_NUM_KEYS][5] = {
|
|||||||
{0x00, 0x00, 0x00}, // Ent, $, m, n, b, v, c, x, z, alt
|
{0x00, 0x00, 0x00}, // Ent, $, m, n, b, v, c, x, z, alt
|
||||||
{0x00, 0x00, 0x00},
|
{0x00, 0x00, 0x00},
|
||||||
{0x00, 0x00, 0x00},
|
{0x00, 0x00, 0x00},
|
||||||
{0x20, 0x00, 0x00},
|
{0x20, 0x00, Key::VOICEMEMO},
|
||||||
{0x00, 0x00, '0'},
|
{Key::VOICEMEMO, 0x00, 0x00},
|
||||||
{0x00, 0x00, 0x00} // R_Shift, sym, space, mic, L_Shift
|
{0x00, 0x00, 0x00} // R_Shift, sym, space, mic, L_Shift
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'},
|
|||||||
{'z', 'Z', '_'},
|
{'z', 'Z', '_'},
|
||||||
{'x', 'X', '$'},
|
{'x', 'X', '$'},
|
||||||
{'c', 'C', ';'},
|
{'c', 'C', ';'},
|
||||||
{'v', 'V', '?'},
|
{'v', 'V', Key::VOICEMEMO},
|
||||||
{'b', 'B', '!'},
|
{'b', 'B', '!'},
|
||||||
{'n', 'N', ','},
|
{'n', 'N', ','},
|
||||||
{'m', 'M', '.'},
|
{'m', 'M', '.'},
|
||||||
|
|||||||
@@ -163,6 +163,16 @@ int32_t KbI2cBase::runOnce()
|
|||||||
e.kbchar = key.key;
|
e.kbchar = key.key;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'v': // sym v - voice memo
|
||||||
|
if (is_sym) {
|
||||||
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
|
e.kbchar = INPUT_BROKER_MSG_VOICEMEMO;
|
||||||
|
is_sym = false; // reset sym state after second keypress
|
||||||
|
} else {
|
||||||
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
|
e.kbchar = key.key;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 0x13: // Code scanner says the SYM key is 0x13
|
case 0x13: // Code scanner says the SYM key is 0x13
|
||||||
is_sym = !is_sym;
|
is_sym = !is_sym;
|
||||||
e.inputEvent = INPUT_BROKER_ANYKEY;
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
@@ -370,6 +380,10 @@ int32_t KbI2cBase::runOnce()
|
|||||||
|
|
||||||
if (i2cBus->available()) {
|
if (i2cBus->available()) {
|
||||||
char c = i2cBus->read();
|
char c = i2cBus->read();
|
||||||
|
// Debug: log every key press
|
||||||
|
if (c != 0x00) {
|
||||||
|
LOG_DEBUG("T-Deck KB: key=0x%02X ('%c'), is_sym=%d", (uint8_t)c, (c >= 0x20 && c < 0x7f) ? c : '?', is_sym);
|
||||||
|
}
|
||||||
InputEvent e = {};
|
InputEvent e = {};
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
e.source = this->_originName;
|
e.source = this->_originName;
|
||||||
@@ -443,6 +457,17 @@ int32_t KbI2cBase::runOnce()
|
|||||||
e.kbchar = c;
|
e.kbchar = c;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 0x76: // letter v. voice memo trigger
|
||||||
|
if (is_sym) {
|
||||||
|
is_sym = false;
|
||||||
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
|
e.kbchar = INPUT_BROKER_MSG_VOICEMEMO;
|
||||||
|
LOG_DEBUG("T-Deck: Sym+V pressed, sending VOICEMEMO 0x%02X", INPUT_BROKER_MSG_VOICEMEMO);
|
||||||
|
} else {
|
||||||
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
|
e.kbchar = c;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 0x1b: // ESC
|
case 0x1b: // ESC
|
||||||
e.inputEvent = INPUT_BROKER_CANCEL;
|
e.inputEvent = INPUT_BROKER_CANCEL;
|
||||||
break;
|
break;
|
||||||
@@ -466,9 +491,11 @@ int32_t KbI2cBase::runOnce()
|
|||||||
e.inputEvent = INPUT_BROKER_RIGHT;
|
e.inputEvent = INPUT_BROKER_RIGHT;
|
||||||
e.kbchar = 0;
|
e.kbchar = 0;
|
||||||
break;
|
break;
|
||||||
case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker))
|
case 0x3F: // Sym key on some T-Deck variants (sends '?')
|
||||||
|
case 0xc: // Modifier key: 0xc is alt+c (Other options could be: 0xea = shift+mic button or 0x4 shift+$(speaker))
|
||||||
// toggle moddifiers button.
|
// toggle moddifiers button.
|
||||||
is_sym = !is_sym;
|
is_sym = !is_sym;
|
||||||
|
LOG_DEBUG("T-Deck: Modifier key pressed, is_sym now=%d", is_sym);
|
||||||
e.inputEvent = INPUT_BROKER_ANYKEY;
|
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||||
e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the
|
e.kbchar = is_sym ? INPUT_BROKER_MSG_FN_SYMBOL_ON // send 0xf1 to tell CannedMessages to display that the
|
||||||
: INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active
|
: INPUT_BROKER_MSG_FN_SYMBOL_OFF; // modifier key is active
|
||||||
|
|||||||
@@ -89,9 +89,8 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
|||||||
void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);
|
void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);
|
||||||
void handleSetCannedMessageModuleMessages(const char *from_msg);
|
void handleSetCannedMessageModuleMessages(const char *from_msg);
|
||||||
|
|
||||||
#ifdef RAK14014
|
// Get current run state (used by VoiceMemoModule to avoid conflicts)
|
||||||
cannedMessageModuleRunState getRunState() const { return runState; }
|
cannedMessageModuleRunState getRunState() const { return runState; }
|
||||||
#endif
|
|
||||||
|
|
||||||
// === Packet Interest Filter ===
|
// === Packet Interest Filter ===
|
||||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
||||||
#include "modules/esp32/AudioModule.h"
|
#include "modules/esp32/AudioModule.h"
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_VOICEMEMO
|
||||||
|
#include "modules/VoiceMemoModule.h"
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||||
#include "modules/esp32/PaxcounterModule.h"
|
#include "modules/esp32/PaxcounterModule.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -285,6 +288,9 @@ void setupModules()
|
|||||||
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
||||||
audioModule = new AudioModule();
|
audioModule = new AudioModule();
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_VOICEMEMO
|
||||||
|
voiceMemoModule = new VoiceMemoModule();
|
||||||
|
#endif
|
||||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||||
if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
|
if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
|
||||||
paxcounterModule = new PaxcounterModule();
|
paxcounterModule = new PaxcounterModule();
|
||||||
|
|||||||
1205
src/modules/VoiceMemoModule.cpp
Normal file
1205
src/modules/VoiceMemoModule.cpp
Normal file
File diff suppressed because it is too large
Load Diff
180
src/modules/VoiceMemoModule.h
Normal file
180
src/modules/VoiceMemoModule.h
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "SinglePortModule.h"
|
||||||
|
#include "concurrency/OSThread.h"
|
||||||
|
#include "configuration.h"
|
||||||
|
#include "input/InputBroker.h"
|
||||||
|
#include "mesh/generated/meshtastic/module_config.pb.h"
|
||||||
|
|
||||||
|
#if defined(ARCH_ESP32) && defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_VOICEMEMO
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ButterworthFilter.h>
|
||||||
|
#include <OLEDDisplay.h>
|
||||||
|
#include <OLEDDisplayUi.h>
|
||||||
|
#include <codec2.h>
|
||||||
|
#include <driver/i2s.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VoiceMemoModule - Store and forward short codec2 encoded audio messages
|
||||||
|
*
|
||||||
|
* Unlike the existing AudioModule which is designed for real-time push-to-talk,
|
||||||
|
* this module is designed for short voice memos that are:
|
||||||
|
* - Recorded when the user holds Shift+Space
|
||||||
|
* - Encoded with Codec2 for compression
|
||||||
|
* - Sent over the mesh with hop_limit=0 (local only)
|
||||||
|
* - Stored on receiving devices for later playback
|
||||||
|
* - Played back when user long-presses on the notification
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Voice memo states
|
||||||
|
enum class VoiceMemoState { IDLE, RECORDING, SENDING, RECEIVING, PLAYING };
|
||||||
|
|
||||||
|
// Codec2 magic header for voice memos
|
||||||
|
const char VOICEMEMO_MAGIC[4] = {0xc0, 0xde, 0xc2, 0x4d}; // c0dec2M (M for Memo)
|
||||||
|
|
||||||
|
struct VoiceMemoHeader {
|
||||||
|
char magic[4];
|
||||||
|
uint8_t mode; // Codec2 mode
|
||||||
|
uint8_t sequence; // Packet sequence number (for multi-packet memos)
|
||||||
|
uint8_t totalParts; // Total number of packets in this memo (0 = unknown/streaming)
|
||||||
|
uint8_t memoId; // Unique ID for this recording session (to identify related packets)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maximum recording time in seconds
|
||||||
|
#define VOICEMEMO_MAX_RECORD_SECS 10
|
||||||
|
#define VOICEMEMO_ADC_BUFFER_SIZE 320 // Codec2 samples per frame
|
||||||
|
#define VOICEMEMO_UPSAMPLE_BUFFER_SIZE 3600 // 320 * (44100/8000) * 2 (stereo) ≈ 3528, rounded up
|
||||||
|
#define VOICEMEMO_I2S_PORT I2S_NUM_0
|
||||||
|
// Codec2 mode - use protobuf enum minus 1 to get codec2 library mode
|
||||||
|
#define VOICEMEMO_CODEC2_MODE (meshtastic_ModuleConfig_AudioConfig_Audio_Baud_CODEC2_700 - 1)
|
||||||
|
|
||||||
|
// Storage for received voice memos
|
||||||
|
#define VOICEMEMO_MAX_STORED 5
|
||||||
|
struct StoredVoiceMemo {
|
||||||
|
NodeNum from;
|
||||||
|
uint32_t timestamp;
|
||||||
|
uint8_t data[meshtastic_Constants_DATA_PAYLOAD_LEN * 4]; // Allow up to 4 packets
|
||||||
|
size_t dataLen;
|
||||||
|
uint8_t codec2Mode;
|
||||||
|
uint8_t memoId; // Memo ID from sender (to identify related packets)
|
||||||
|
uint8_t receivedParts; // Bitmask of received packet sequences
|
||||||
|
uint8_t expectedParts; // Total expected parts (0 = unknown)
|
||||||
|
bool played;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VoiceMemoModule : public SinglePortModule, public Observable<const UIFrameEvent *>, private concurrency::OSThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VoiceMemoModule();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we should draw the UI frame
|
||||||
|
*/
|
||||||
|
bool shouldDraw();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keyboard input for Shift+Space detection
|
||||||
|
*/
|
||||||
|
int handleInputEvent(const InputEvent *event);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a stored voice memo
|
||||||
|
*/
|
||||||
|
void playStoredMemo(int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get number of unplayed memos
|
||||||
|
*/
|
||||||
|
int getUnplayedCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored memo info for UI
|
||||||
|
*/
|
||||||
|
const StoredVoiceMemo *getStoredMemo(int index);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual int32_t runOnce() override;
|
||||||
|
virtual meshtastic_MeshPacket *allocReply() override;
|
||||||
|
virtual bool wantUIFrame() override { return shouldDraw(); }
|
||||||
|
virtual Observable<const UIFrameEvent *> *getUIFrameObservable() override { return this; }
|
||||||
|
|
||||||
|
#if HAS_SCREEN
|
||||||
|
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// State machine
|
||||||
|
VoiceMemoState state = VoiceMemoState::IDLE;
|
||||||
|
|
||||||
|
// Codec2
|
||||||
|
CODEC2 *codec2 = nullptr;
|
||||||
|
int encodeCodecSize = 0;
|
||||||
|
int adcBufferSize = 0;
|
||||||
|
|
||||||
|
// Audio buffers
|
||||||
|
int16_t speechBuffer[VOICEMEMO_ADC_BUFFER_SIZE] = {};
|
||||||
|
int16_t outputBuffer[VOICEMEMO_ADC_BUFFER_SIZE] = {};
|
||||||
|
int16_t upsampleBuffer[VOICEMEMO_UPSAMPLE_BUFFER_SIZE] = {}; // For 8kHz->44.1kHz upsampling
|
||||||
|
uint8_t encodedFrame[meshtastic_Constants_DATA_PAYLOAD_LEN] = {};
|
||||||
|
size_t encodedFrameIndex = 0;
|
||||||
|
|
||||||
|
// Recording state
|
||||||
|
uint32_t recordingStartMs = 0;
|
||||||
|
uint32_t sendingCompleteMs = 0; // When sending completed (for "Sent!" display timeout)
|
||||||
|
uint8_t currentMemoId = 0; // Unique ID for current recording session
|
||||||
|
uint8_t currentSequence = 0; // Current packet sequence number
|
||||||
|
|
||||||
|
// I2S state
|
||||||
|
bool i2sInitialized = false;
|
||||||
|
|
||||||
|
// Stored memos for playback
|
||||||
|
StoredVoiceMemo storedMemos[VOICEMEMO_MAX_STORED];
|
||||||
|
int storedMemoCount = 0;
|
||||||
|
|
||||||
|
// Playback state
|
||||||
|
int playingMemoIndex = -1;
|
||||||
|
size_t playbackPosition = 0;
|
||||||
|
|
||||||
|
// Filter for audio cleanup
|
||||||
|
ButterworthFilter *hpFilter = nullptr;
|
||||||
|
|
||||||
|
// Codec2 task for encoding (needs large stack)
|
||||||
|
TaskHandle_t codec2TaskHandle = nullptr;
|
||||||
|
volatile bool codec2TaskRunning = false;
|
||||||
|
volatile bool audioReady = false;
|
||||||
|
|
||||||
|
// Playback task (also needs large stack for Codec2 decoding)
|
||||||
|
TaskHandle_t playbackTaskHandle = nullptr;
|
||||||
|
volatile bool playbackTaskRunning = false;
|
||||||
|
volatile bool playbackReady = false;
|
||||||
|
const StoredVoiceMemo *currentPlaybackMemo = nullptr;
|
||||||
|
|
||||||
|
// Internal methods
|
||||||
|
bool initES7210();
|
||||||
|
bool initI2S();
|
||||||
|
void deinitI2S();
|
||||||
|
void startRecording();
|
||||||
|
void stopRecording();
|
||||||
|
void processRecordingBuffer();
|
||||||
|
void sendEncodedPayload();
|
||||||
|
void storeMemo(const meshtastic_MeshPacket &mp);
|
||||||
|
void playMemo(const StoredVoiceMemo &memo);
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Called by the codec2 task - needs to be public for task function access
|
||||||
|
void doCodec2Encode();
|
||||||
|
void doCodec2Playback();
|
||||||
|
|
||||||
|
// Keyboard observer
|
||||||
|
CallbackObserver<VoiceMemoModule, const InputEvent *> inputObserver =
|
||||||
|
CallbackObserver<VoiceMemoModule, const InputEvent *>(this, &VoiceMemoModule::handleInputEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern VoiceMemoModule *voiceMemoModule;
|
||||||
|
|
||||||
|
#endif // ARCH_ESP32 && HAS_I2S && !MESHTASTIC_EXCLUDE_VOICEMEMO
|
||||||
Reference in New Issue
Block a user