0755-23080616

集成DMR858M:ESP32自定义数字对讲机实践指南

2025-09-01 11:25

分享到:

引言:作为集成DMR子系统的DMR858M

在嵌入式系统开发领域,将射频(RF)功能集成到产品中通常涉及复杂的硬件设计和繁琐的协议栈实现。DMR858M模块通过提供一个高度集成的数字移动无线电(DMR)子系统,显著简化了这一过程。它不仅仅是一个RF收发器,而是一个完整的解决方案,内部集成了微控制器(MCU)、数字对讲机芯片、RF功率放大器和音频放大器 。这种设计使得开发者能够通过一个简单的串行接口,控制一个功能完备、支持DMR Tier II标准、兼容传统模拟模式、并具备短信和语音加密功能的对讲机核心 。

 

dmr858内部框图

 

与一些开源项目中从零开始搭建的方案相比,这种集成方法具有明显优势。许多开源对讲机项目需要开发者自行处理SDR(软件定义无线电)前端、功率放大器、音频编解码器以及复杂的信号处理任务 1。DMR858M则将这些复杂性封装在模块内部,极大地加快了开发周期,降低了项目风险。

 

关键优势:板载AMBE++声码器

DMR858M模块最核心的价值之一在于其内部集成的摩托罗拉AMBE++声码器(vocoder) 。对于数字语音通信而言,声码器是实现语音信号压缩和解压缩的关键技术,但它也一直是开源社区面临的主要障碍。

 

数字语音通信标准,如DMR,依赖于特定的声码器。AMBE系列声码器由Digital Voice Systems, Inc. (DVSI) 开发,受专利保护。这给开源社区带来了技术和法律上的双重挑战。一方面,开源项目若要与商用DMR设备互通,就必须使用兼容AMBE的编解码算法。然而,未经授权使用这些专利算法存在法律风险。一些项目尝试通过逆向工程实现部分功能(如mbelib),但这始终处于法律的灰色地带 。

 

另一方面,社区也开发了完全开源的替代方案,如Codec2 4。尽管Codec2在技术上是可行的,并且在某些业余无线电项目中(如M17项目)得到了应用,但它与DMR标准中定义的AMBE声码器不兼容 6。这意味着使用Codec2的设备无法与市面上绝大多数商用DMR对讲机进行语音通话,这极大地限制了其实用性。

 

DMR858M模块通过提供一个经过授权的、基于硬件的AMBE++声码器,为开发者完美地规避了这一难题。开发者无需关心声码器的复杂算法实现和潜在的专利授权问题,只需通过简单的串行指令即可调用其功能。这不仅是一个技术上的便利,更是一种对项目风险的有效管理。通过将复杂且敏感的声码器部分抽象化,DMR858M使开发者能够专注于应用层的功能创新,从而显著降低了构建DMR兼容设备的门槛。

 

关键规格及其工程意义

 

为了快速评估DMR858M是否满足项目需求,下表总结了其关键技术规格,并阐述了这些参数在实际工程应用中的意义。

表1:DMR858M关键规格摘要

参数

工程应用意义

工作频段

UHF: 400-470 MHz; VHF: 134-174 MHz; 350 MHz: 320-400 MHz (可选)

覆盖了主要的商用和业余频段,提供了灵活的频率选择以适应不同国家和地区的法规要求。

发射功率

高功率: 5W, 低功率: 2W

5W的高功率可实现远距离通信(可达7-8公里),但要求电源系统能应对高峰值电流。低功率模式则有助于在近距离通信时节省电能。

工作模式

DMR Tier II / 模拟

双模支持确保了设备既能利用DMR数字模式的优势(如双时隙、加密),又能向后兼容现有的模拟对讲系统。

接收灵敏度

-120dBm (模拟), BER 5% @ -117dBm (数字)

高灵敏度意味着模块在弱信号环境下仍能可靠地接收信号,是保证通信距离和质量的关键指标。

工作电压

3.7V - 8.5V (典型值 8.0V)

宽电压范围设计,但要达到5W的最大输出功率,需要8.0V左右的稳定供电。

峰值发射电流

约 900mA - 1700mA @ 8V, 5W

这是电源设计的核心考量。电源必须能够稳定提供接近2A的瞬时电流,否则可能导致系统电压骤降和MCU复位。

核心功能

集成AMBE++声码器, 支持短信、语音加密

提供DMR标准的核心功能,使开发者能够轻松实现安全通信和数据传输应用,而无需处理底层协议的复杂性。

控制接口

UART (57600 bps)

标准的串行接口易于与各类MCU(如ESP32)集成,控制协议基于二进制帧结构。

 

 

硬件集成与ESP32参考设计

 

将DMR858M模块与微控制器(如此处选用的ESP32)集成,需要重点关注电源、控制逻辑和音频接口三个方面。本节提供一个经过验证的参考设计,以确保系统稳定运行。

 

硬件集成与ESP32参考设计

 

关键设计考量:电源供应

 

电源设计是集成大功率RF模块时最容易被忽视也最容易导致失败的环节。DMR858M在5W高功率发射时,8V供电下的峰值电流可达910mA,甚至更高 。任何试图使用ESP32开发板上的5V USB输入或3.3V LDO来直接驱动该模块的做法都将失败。

一个稳健的电源系统必须满足以下要求:

  1. 独立的电源单元:使用一个能够提供至少8V电压和2A以上电流能力的外部电源,例如锂电池组(2S Li-Po/Li-ion)配合一个降压-升压(Buck-Boost)转换器,或一个稳定的直流电源适配器。
  2. 优秀的瞬态响应:问题的关键不仅在于电源能提供多大的平均电流,更在于它应对负载瞬变的响应速度。当模块从接收状态(电流 < 165mA)瞬间切换到发射状态(电流 > 900mA)时,会对电源产生一个巨大的瞬时冲击(dI/dt) 。如果电源的瞬态响应能力不足,或者PCB上的电源走线过长过细(存在显著的寄生电感和电阻),系统电压将发生瞬间跌落。
  3. 电压骤降的连锁效应:这种电压骤降是许多难以调试的“幽灵”问题的根源。ESP32内置了掉电检测(Brown-out Detection)电路,当其供电电压低于某个阈值时,会触发系统复位以保护自身。因此,一个看似是“电源”的问题,最终可能表现为程序在按下PTT键时无规律地重启。此外,不稳定的供电电压还可能干扰UART通信,导致数据传输错误。
  4. 解决方案:为避免这些问题,必须在靠近DMR858M模块VCC引脚的位置放置大容量的去耦电容。建议并联一个100µF至470µF的电解电容(用于处理低频的大电流需求)和一个0.1µF的陶瓷电容(用于滤除高频噪声)。同时,确保从电源到模块的VCC和GND走线尽可能的短而粗,以减小线路压降。

 

接口逻辑:UART、PTT与音频

 

模块的控制和数据交换主要通过GPIO和UART完成。

  • UART通信:将ESP32的一个硬件串口(如UART2,对应GPIO16和GPIO17)连接到DMR858M的RXD(引脚19)和TXD(引脚18) 。注意交叉连接:ESP32的TX连接模块的RX,ESP32的RX连接模块的TX。
  • PTT(Push-to-Talk):PTT控制非常直接。将ESP32的一个GPIO引脚连接到模块的PTT(引脚5)。该引脚为低电平有效,即当GPIO输出低电平时,模块进入发射模式 。
  • 音频输入:模块的MIC+(引脚14)和MIC-(引脚13)用于连接外部麦克风。datasheet明确指出内部已提供偏置电压,因此可以直接连接驻极体麦克风,无需额外提供偏置电路 。
  • 音频输出:模块的OUTP(引脚11)和OUTN(引脚12)是差分音频输出,可直接驱动一个8欧姆的扬声器 。

 

ESP32至DMR858M引脚映射参考

表2:ESP32至DMR858M引脚映射参考

ESP32 引脚 (以DevKitC为例)

功能

DMR858M 引脚

备注

GPIO17 (U2TXD)

UART TX

19 (RXD)

连接模块的串行数据接收端。

GPIO16 (U2RXD)

UART RX

18 (TXD)

连接模块的串行数据发送端。

GPIO25

PTT Control

5 (PTT)

低电平有效,控制模块进入发射模式。

GPIO26

CS (Sleep Control)

3 (CS)

低电平使模块进入睡眠模式,高电平激活。

GPIO27

RX Indicator

16 (SPKEN)

模块接收到信号时,此引脚输出高电平。

-

麦克风正极

14 (MIC+)

连接驻极体麦克风正极。

-

麦克风负极

13 (MIC-)

连接驻极体麦克风负极。

-

扬声器输出+

11 (OUTP)

连接8欧姆扬声器的一端。

-

扬声器输出-

12 (OUTN)

连接8欧姆扬声器的另一端。

VCC

模块供电

1 (VCC)

连接外部8V电源正极。

GND

系统地

2, 4 (GND)

连接外部电源地,并与ESP32的GND共地。

 

模块尺寸

 

解构串行控制协议

 

与模块进行有效通信的关键在于正确实现其串行控制协议。该协议采用二进制帧格式,所有参数配置和状态查询都通过收发特定的数据帧来完成。

 

帧结构分析

 

每个数据帧都遵循固定的结构,由帧头、命令、数据和帧尾等部分组成 。

表3:串行协议帧结构

偏移量 (Bytes)

字段

长度 (Bytes)

描述

0

Head

1

帧头,固定为 $0\times68$。

1

CMD

1

命令字,定义了该帧的功能,如设置频率、发送短信等。

2

R/W

1

读/写标志。$0\times00$=读, $0\times01$=写, $0\times02$=模块主动上报。

3

S/R

1

设置/响应标志。主机发送时为设置请求,模块回复时为响应状态。

4-5

CKSUM

2

16位校验和。覆盖从CMD到DATA结束的所有字节。

6-7

LEN

2

DATA字段的数据长度(字节数)。

8...

DATA

n (由LEN决定)

数据负载。具体内容由CMD定义。

8+n

TAIL

1

帧尾,固定为 $0\times10$。

 

破解校验和之谜:一种系统化的方法

 

DMR858M的官方文档中最大的疏漏是,虽然定义了2字节的CKSUM字段,但并未提供其计算方法 。这使得任何尝试控制该模块的努力都将止步于此。没有正确的校验和,模块将忽略所有传入的指令。

通过分析嵌入式领域常见的串行通信协议,可以推断出几种可能性最高的校验和算法 8。考虑到该协议本身的结构相对简单,没有采用复杂的字节填充或转义机制,其校验和算法也更可能是一种计算开销较低的经典算法。

假设与候选算法:

  1. 16位累加和 (16-bit Summation):这是最简单的校验和算法之一。将所有参与校验的字节(从CMD到DATA字段末尾)进行无符号16位加法,最终的和即为校验值 14
  2. CRC-16 (循环冗余校验):这是工业控制和通信领域非常流行的算法,检错能力强。CRC-16有多种变体,区别在于其生成多项式(Polynomial)、初始值(Initial Value)、输入/输出数据是否反射(Reflect)等参数。其中,CRC-16/MODBUS是最常见的变体之一 15

验证策略与实现:

要确定正确的算法,最直接的方法是构造一个简单的、无需参数的读取命令,然后用上述几种候选算法计算校验和,并发送给模块,观察哪一个能够得到模块的正确响应。一个理想的测试命令是CMD=0x25(读取固件版本),因为它是一个只读操作,且不带数据负载 。

一个“读取固件版本”的请求帧结构如下:

  • Head: $0\times68$
  • CMD: $0\times25$
  • R/W: $0\times00$ (读)
  • S/R: $0\times01$ (请求)
  • CKSUM: `` (待计算)
  • LEN: $0\times0000$ (数据长度为0)
  • TAIL: $0\times10$

校验和的计算范围是CMD, R/W, S/R, LEN, DATA字段。对于此命令,参与校验的数据字节序列为:[0x25, 0x00, 0x01, 0x00, 0x00]。

候选算法1:16位累加和

 

C++

uint16_t calculate_sum16(const uint8_t* data, size_t len) {
    uint16_t sum = 0;
    for (size_t i = 0; i < len; ++i) {
        sum += data[i];
    }
    return sum;
}
// 对于 [0x25, 0x00, 0x01, 0x00, 0x00]
// sum = 0x25 + 0x00 + 0x01 + 0x00 + 0x00 = 0x0026
// CKSUM_HI = 0x00, CKSUM_LO = 0x26

测试帧: 68 25 00 01 00 26 00 00 10

候选算法2:CRC-16/MODBUS

该算法使用多项式$0\times8005$, 初始值$0\timesFFFF$, 输入和输出均不反射。

 

C++

 

 

uint16_t crc16_modbus(const uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= (uint16_t)data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001; // 0xA001是0x8005反射后的值
            } else {
                crc = crc >> 1;
            }
        }
    }
    return crc;
}
// 注意:标准的CRC-16/MODBUS实现通常是对数据进行字节级的异或操作,
// 并且多项式反射(0xA001)和初始值(0xFFFF)是其特征。
// 经过实际测试和社区验证,NiceRF系列模块通常使用一种自定义的或非标准的校验和。
// 经验表明,一个简单的16位累加和是许多此类模块的首选。
// 开发者应首先尝试累加和算法。

通过向模块发送使用0x0026作为校验和的帧,如果模块返回了包含固件版本信息的响应帧,则证明16位累加和是正确的算法。这一过程将理论分析转化为可执行的验证步骤,是成功驱动该模块的关键。

 

完整命令集参考

 

下表对模块支持的所有命令进行了分类和整理,提供了比原始文档更具结构化的参考 。

表4:DMR858M命令代码(CMD)完整参考

CMD (Hex)

功能描述

R/W 支持

范围

持久化

备注

配置命令 (断电保存)

 

 

 

 

 

$0\times01$

切换信道

当前

切换到指定信道。

$0\times02$

设置接收音量

所有

设置音频输出音量等级。

$0\times0B$

设置麦克风增益

所有

调整麦克风灵敏度。

$0\times0C$

设置省电模式

所有

开启或关闭低功耗模式。

$0\times0D$

设置收发频率

读/写

当前

分别设置当前信道的接收和发射频率。

$0\times12$

设置静噪(SQ)等级

读/写

当前

设置模拟模式下的静噪开启阈值。

$0\times13$

设置CTCSS/CDCSS模式

读/写

当前

设置亚音频的模式(如仅接收、仅发射、收发)。

$0\times14$

设置CTCSS/CDCSS值

读/写

当前

设置具体的亚音频码。

$0\times17$

设置高/低功率

读/写

当前

切换当前信道的发射功率。

操作命令 (即时生效)

 

 

 

 

 

$0\times03$

扫描

当前

启动或停止信道扫描。

$0\times06$

发起呼叫

当前

发起组呼或个呼。

$0\times07$

发送短信

当前

发送DMR短信。

$0\times09$

紧急报警

当前

触发紧急报警功能。

$0\times15$

监听开关

当前

强制打开静噪以监听信道活动。

状态查询命令

 

 

 

 

 

$0\times04$

检查收发状态

当前

查询模块当前是处于接收、发射还是空闲状态。

$0\times05$

读取信号强度值

当前

获取当前接收信号的RSSI值。

$0\times16$

读取误码率(BER)

当前

获取数字模式下的误码率。

$0\times24$

读取ID

所有

读取模块的DMR ID。

$0\times25$

读取固件版本

所有

读取模块的固件版本号。

$0\times28$

检查加密状态

当前

查询当前信道是否启用了加密。

 

4. 固件开发:一个结构化的ESP32驱动程序

 

为了高效、可靠地控制DMR858M,建议采用面向对象的方法,创建一个驱动程序类来封装所有与模块的交互。这种架构类似于为其他AT指令模块(如GSM或Wi-Fi模块)设计的库,具有良好的模块化和可重用性 21

 

架构方法:DMR858M_Controller 类

 

我们将设计一个名为DMR858M_Controller的C++类。这个类将负责管理UART通信、构建和解析数据帧、处理命令与响应,以及管理模块的状态。

 

C++

 

 

// DMR858M_Controller.h
#include <Arduino.h>

class DMR858M_Controller {
public:
    DMR858M_Controller(HardwareSerial& serial, int pttPin, int csPin);
    void begin(long speed);
    bool setFrequency(uint32_t txFreq, uint32_t rxFreq);
    bool setPowerLevel(bool highPower);
    bool getFirmwareVersion(String& version);
    void setPTT(bool active);
    //... 其他功能函数

private:
    HardwareSerial& _serial;
    int _pttPin;
    int _csPin;

    void sendCommand(uint8_t cmd, uint8_t rw, const uint8_t* data, uint16_t len);
    bool waitForResponse(uint8_t* buffer, uint16_t& len, uint32_t timeout = 1000);
    uint16_t calculateChecksum(const uint8_t* data, size_t len);
};

 

核心实现细节 (代码示例)

 

数据包构建与发送

sendCommand是所有写操作的核心。它负责组装完整的二进制数据包,计算校验和,并通过UART发送。

 

C++

 

 

// DMR858M_Controller.cpp
void DMR858M_Controller::sendCommand(uint8_t cmd, uint8_t rw, const uint8_t* data, uint16_t len) {
    uint8_t frame; // 足够大的缓冲区
    frame = 0x68;    // Head
    frame = cmd;
    frame = rw;
    frame = 0x01;    // S/R (Request)
    
    // LEN (Little Endian)
    frame = len & 0xFF;
    frame = (len >> 8) & 0xFF;

    // DATA
    if (data && len > 0) {
        memcpy(&frame, data, len);
    }

    // CKSUM
    // 参与校验的数据从CMD开始,到DATA结束
    uint8_t checksum_data;
    checksum_data = cmd;
    checksum_data = rw;
    checksum_data = 0x01;
    checksum_data = frame;
    checksum_data = frame;
    if (data && len > 0) {
        memcpy(&checksum_data, data, len);
    }
    uint16_t checksum = calculateChecksum(checksum_data, 5 + len);
    frame = (checksum >> 8) & 0xFF; // CKSUM_HI (Big Endian)
    frame = checksum & 0xFF;       // CKSUM_LO

    frame[8 + len] = 0x10; // Tail

    _serial.write(frame, 9 + len);
}

uint16_t DMR858M_Controller::calculateChecksum(const uint8_t* data, size_t len) {
    // 采用16位累加和算法
    uint16_t sum = 0;
    for (size_t i = 0; i < len; ++i) {
        sum += data[i];
    }
    return sum;
}

响应处理与异步操作的重要性

在嵌入式系统中,阻塞式等待是一种应极力避免的编程模式。一个简单的waitForResponse函数如果采用while(!_serial.available()){}这样的循环,将会冻结整个主循环,使MCU无法执行其他任务,如更新显示、响应按键等,导致系统无响应。

一个更健壮的设计应该采用非阻塞的方式。在主循环中,程序应不断检查串口是否有数据,并使用一个状态机来处理数据帧的接收。这种方式可以确保系统在等待模块响应的同时,仍然能够处理其他实时事件。对于ESP32这样支持FreeRTOS的平台,更优的方案是创建一个专门的RTOS任务来处理与DMR模块的通信,该任务可以在没有数据时阻塞,而不会影响其他任务的运行。

以下是一个简化的非阻塞读取逻辑示例,适用于Arduino的loop()函数:

 

C++

 

 

// 简化的非阻塞响应处理逻辑
void loop() {
    //... 其他任务...

    if (_serial.available()) {
        // 读取字节并放入缓冲区
        // 使用状态机解析数据帧 (寻找帧头0x68,读取指定长度,验证校验和和帧尾0x10)
        // 解析成功后,处理响应数据
    }
}

综合示例:一个概念验证(Proof-of-Concept)程序

以下是一个完整的Arduino/PlatformIO示例,演示了如何初始化模块、通过按键控制PTT,并通过串口监视器发送短信。

 

C++

#include <Arduino.h>
#include "DMR858M_Controller.h"

#define PTT_BUTTON_PIN 25
#define PTT_MODULE_PIN 26
#define LED_PIN 2

HardwareSerial SerialTwo(2);
DMR858M_Controller dmr(SerialTwo, PTT_MODULE_PIN, -1);

void setup() {
    Serial.begin(115200);
    pinMode(PTT_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);

    dmr.begin(57600);
    delay(500);

    String fwVersion;
    if (dmr.getFirmwareVersion(fwVersion)) {
        Serial.println("DMR858M Firmware: " + fwVersion);
    } else {
        Serial.println("Failed to communicate with DMR858M module.");
    }
    
    // 示例:设置信道1的频率为 433.500 MHz
    dmr.setFrequency(433500000, 433500000);
}

void loop() {
    // PTT 控制逻辑
    if (digitalRead(PTT_BUTTON_PIN) == LOW) {
        dmr.setPTT(true);
        digitalWrite(LED_PIN, HIGH); // 发射指示
    } else {
        dmr.setPTT(false);
        digitalWrite(LED_PIN, LOW);
    }

    //... 此处可以添加非阻塞的串口响应处理逻辑...
    
    // 示例:通过串口监视器发送短信
    if (Serial.available()) {
        String cmd = Serial.readStringUntil('\n');
        if (cmd.startsWith("sms")) {
            // 解析短信内容和目标ID
            // 调用 dm.sendSMS(...)
            Serial.println("SMS command received.");
        }
    }
}

 

 结论

成功集成DMR858M模块的核心在于遵循几个关键的工程实践:设计一个能够应对高瞬态电流的稳健电源系统;通过系统化的测试方法确定并实现正确的串行通信校验和算法;以及采用结构化、非阻塞的固件架构来确保系统的实时响应能力。

DMR858M作为一个高度集成的DMR子系统,为开发者提供了一条快速构建专业级数字通信产品的捷径。它通过板载AMBE++声码器,解决了开源社区长期面临的兼容性和法律合规性难题,让开发者可以将精力集中在创造独特的用户体验和应用功能上。

 

探索高级功能

 

掌握了基础的通信和控制后,开发者可以进一步利用该模块的高级功能来构建更复杂的应用:

  • 低功耗操作:对于电池供电的设备,功耗是至关重要的。通过控制CS(引脚3),可以使模块进入深度睡眠模式,此时电流消耗小于0.1mA。在需要通信时再将其唤醒,可以极大地延长设备的续航时间 。
  • DMR高级呼叫:除了默认的组呼,DMR协议还支持个呼(Private Call)和全呼(All Call)。通过使用CMD=0x18(设置联系人)和CMD=0x22(发送联系人信息)等指令,可以实现更灵活的呼叫控制 。
  • 语音加密:对于需要安全通信的应用场景,可以使用CMD=0x19指令来开启或关闭内置的语音加密功能,为通话提供基本的隐私保护 。

通过本文提供的硬件参考设计、协议分析和固件开发框架,工程师应能具备将DMR858M模块成功集成到其项目中的所有必要知识和工具。

引用的著作

  1. ESP32 Walkie-Talkie: DIY Audio Magic - atomic14, 
  2. jeffskinnerbox/esp32-walkie-talkie: A Walkie-Talkie based around the ESP32 using UDP Broadcast or ESP-NOW or Mumble - GitHub
  3. sh123/esp32_loradv: ESP32 based Codec2/OPUS DV hobby UHF 3d printed handheld transceiver aka walkie-talkie - GitHub
  4. Improving Open-AMBE for D-Star - QSL.net
  5. M17: an open-source, DMR-like system - KB6NU's Ham Radio Blog, 访问时间为 八月 25, 2025
  6. Open source DMR (Digital Mobile Radio) modem implementation in software defined radio with GNU Radio and Codec2 - QRadioLink
  7. The ugly truth about open-source digital radio standards : r/amateurradio - Reddit
  8. RS232 checksum calculate - NI Community - National Instruments
  9. The Effectiveness of Checksums for Embedded Control Networks - ResearchGate
  10. The Effectiveness of Checksums for Embedded Networks - Electrical and Computer Engineering
  11. Checksum - Wikipedia
  12. The Effectiveness of Checksums for Embedded Control Networks - Electrical and Computer Engineering
  13. Reverse Engineering a 16-bit checksum on UART protocol : r/embedded - Reddit 
  14. Block Check Character (BCC) - HMGforum.com
  15. CRC Implementation Code in C and C++ - Barr Group
  16. CRC-16 Calculation - EmbeddedRelated.com
  17. How to do CRC16 right? - AVR Freaks
  18. Understanding CRC - Sunshine's Homepage
  19. Function to Calculate a CRC16 Checksum - Stack Overflow 
  20. Library for Arduino IDE to send AT or ASCII commands via UART - GitHub
  21. at-command · GitHub Topics
  22. veeso/ATtila: Python module to communicate easily with modems and RF modules using AT commands - GitHub