mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-11 12:27:28 +00:00
Compare commits
1 Commits
t-watch-ul
...
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_OFF 0xf2
|
||||
#define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA
|
||||
#define INPUT_BROKER_MSG_VOICEMEMO 0xAD
|
||||
#define INPUT_BROKER_MSG_TAB 0x09
|
||||
#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] = {
|
||||
Key::ESC, // 1
|
||||
Key::UP, // 2
|
||||
Key::NONE, // 3
|
||||
Key::LEFT, // 4
|
||||
Key::NONE, // 5
|
||||
Key::RIGHT, // 6
|
||||
Key::NONE, // 7
|
||||
Key::DOWN, // 8
|
||||
Key::NONE, // 9
|
||||
Key::BSP, // *
|
||||
Key::NONE, // 0
|
||||
Key::NONE, // #
|
||||
Key::ESC, // 1
|
||||
Key::UP, // 2
|
||||
Key::NONE, // 3
|
||||
Key::LEFT, // 4
|
||||
Key::NONE, // 5
|
||||
Key::RIGHT, // 6
|
||||
Key::NONE, // 7
|
||||
Key::DOWN, // 8
|
||||
Key::NONE, // 9
|
||||
Key::BSP, // *
|
||||
Key::VOICEMEMO, // 0
|
||||
Key::NONE, // #
|
||||
};
|
||||
|
||||
TCA8418Keyboard::TCA8418Keyboard()
|
||||
|
||||
@@ -25,6 +25,7 @@ class TCA8418KeyboardBase
|
||||
BT_TOGGLE = 0xAA,
|
||||
GPS_TOGGLE = 0x9E,
|
||||
MUTE_TOGGLE = 0xAC,
|
||||
VOICEMEMO = 0xAD,
|
||||
SEND_PING = 0xAF,
|
||||
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},
|
||||
{0x00, 0x00, 0x00},
|
||||
{0x20, 0x00, 0x00},
|
||||
{0x00, 0x00, '0'},
|
||||
{0x20, 0x00, Key::VOICEMEMO},
|
||||
{Key::VOICEMEMO, 0x00, 0x00},
|
||||
{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', '_'},
|
||||
{'x', 'X', '$'},
|
||||
{'c', 'C', ';'},
|
||||
{'v', 'V', '?'},
|
||||
{'v', 'V', Key::VOICEMEMO},
|
||||
{'b', 'B', '!'},
|
||||
{'n', 'N', ','},
|
||||
{'m', 'M', '.'},
|
||||
|
||||
@@ -163,6 +163,16 @@ int32_t KbI2cBase::runOnce()
|
||||
e.kbchar = key.key;
|
||||
}
|
||||
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
|
||||
is_sym = !is_sym;
|
||||
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||
@@ -370,6 +380,10 @@ int32_t KbI2cBase::runOnce()
|
||||
|
||||
if (i2cBus->available()) {
|
||||
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 = {};
|
||||
e.inputEvent = INPUT_BROKER_NONE;
|
||||
e.source = this->_originName;
|
||||
@@ -443,6 +457,17 @@ int32_t KbI2cBase::runOnce()
|
||||
e.kbchar = c;
|
||||
}
|
||||
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
|
||||
e.inputEvent = INPUT_BROKER_CANCEL;
|
||||
break;
|
||||
@@ -466,9 +491,11 @@ int32_t KbI2cBase::runOnce()
|
||||
e.inputEvent = INPUT_BROKER_RIGHT;
|
||||
e.kbchar = 0;
|
||||
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.
|
||||
is_sym = !is_sym;
|
||||
LOG_DEBUG("T-Deck: Modifier key pressed, is_sym now=%d", is_sym);
|
||||
e.inputEvent = INPUT_BROKER_ANYKEY;
|
||||
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
|
||||
|
||||
@@ -89,9 +89,8 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);
|
||||
void handleSetCannedMessageModuleMessages(const char *from_msg);
|
||||
|
||||
#ifdef RAK14014
|
||||
// Get current run state (used by VoiceMemoModule to avoid conflicts)
|
||||
cannedMessageModuleRunState getRunState() const { return runState; }
|
||||
#endif
|
||||
|
||||
// === Packet Interest Filter ===
|
||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
||||
#include "modules/esp32/AudioModule.h"
|
||||
#endif
|
||||
#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_VOICEMEMO
|
||||
#include "modules/VoiceMemoModule.h"
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
#include "modules/esp32/PaxcounterModule.h"
|
||||
#endif
|
||||
@@ -285,6 +288,9 @@ void setupModules()
|
||||
#if defined(USE_SX1280) && !MESHTASTIC_EXCLUDE_AUDIO
|
||||
audioModule = new AudioModule();
|
||||
#endif
|
||||
#if defined(HAS_I2S) && !MESHTASTIC_EXCLUDE_VOICEMEMO
|
||||
voiceMemoModule = new VoiceMemoModule();
|
||||
#endif
|
||||
#if !MESHTASTIC_EXCLUDE_PAXCOUNTER
|
||||
if (moduleConfig.has_paxcounter && moduleConfig.paxcounter.enabled) {
|
||||
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