This commit is contained in:
844143714@qq,com 2026-04-26 20:38:42 +08:00
parent 69140b73c8
commit 803fcb38d2
6 changed files with 212 additions and 130 deletions

View File

@ -157,49 +157,35 @@
<!-- PTT 业务测试模块 -->
<div class="ws-card ptt-card">
<div style="display: flex; align-items: center; justify-content: space-between;">
<strong style="color: #047857;">PTT 业务接口测试</strong>
<strong style="color: #047857;">PTT 业务功能测试</strong>
<span id="ptt-status-tag" class="ws-status ws-disconnected">PTT 未登录</span>
</div>
<div class="ws-input-group" style="margin-top: 10px;">
<div style="display: flex; gap: 8px;">
<div style="flex: 1;">
<span class="ptt-label">POC 账号:</span>
<input type="text" id="ptt-account" class="ws-input" value="test">
</div>
<div style="flex: 1;">
<span class="ptt-label">频道 ID:</span>
<input type="text" id="ptt-gid" class="ws-input" value="126953">
</div>
</div>
<div style="display: flex; gap: 8px;">
<div style="flex: 1;">
<span class="ptt-label">频道/群组名称:</span>
<input type="text" id="ptt-name" class="ws-input" value="测试频道">
</div>
<div style="flex: 1;">
<span class="ptt-label">录音相对路径:</span>
<input type="text" id="ptt-record-path" class="ws-input" placeholder="path/to/record.wav">
<span class="ptt-label">群组 ID (十六进制):</span>
<input type="text" id="ptt-gid" class="ws-input" value="00000001">
</div>
</div>
</div>
<!-- PTT 核心对讲按钮 -->
<div style="margin-top: 15px;">
<button id="ptt-speak-btn" class="btn btn-red"
style="height: 80px; font-size: 20px; background: #94a3b8; cursor: not-allowed;"
disabled>长按对讲 (PTT)</button>
</div>
<div class="ptt-btn-grid">
<button class="btn ptt-btn btn-green" onclick="pttCreateGroup()">创建频道</button>
<button class="btn ptt-btn btn-red" onclick="pttDeleteGroup()">删除频道</button>
<button class="btn ptt-btn" style="background: #3b82f6;" onclick="pttAddMembers()">拉人入频道</button>
<button class="btn ptt-btn btn-orange" onclick="pttKickMembers()">踢出频道</button>
<button class="btn ptt-btn" style="background: #6366f1;" onclick="pttGetGroups()">频道列表</button>
<button class="btn ptt-btn" style="background: #8b5cf6;" onclick="pttGetTempGroups()">临时列表</button>
<button class="btn ptt-btn" style="background: #ec4899;" onclick="pttGetRecords()">查询录音</button>
<button class="btn ptt-btn" style="background: #06b6d4;" onclick="pttGetRecordText()">录音转文字</button>
<button class="btn ptt-btn btn-orange" style="background: #f59e0b;" onclick="pttPlayRecord()">播放录音</button>
<button class="btn ptt-btn btn-green" onclick="pttCreateTempGroup()">创建临时频道</button>
<button class="btn ptt-btn btn-red" onclick="pttDeleteTempGroup()">删除临时频道</button>
<button class="btn ptt-btn" style="background: #4f46e5;" onclick="pttGetTempMembers()">查临时成员</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #6366f1;" onclick="pttGetGroups()" disabled>获取群组</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #8b5cf6;" onclick="pttGetMembers()" disabled>获取成员</button>
<button class="btn ptt-btn ptt-action-btn" style="background: #3b82f6;" onclick="pttJumpGroup()" disabled>切换群组</button>
<button class="btn ptt-btn ptt-action-btn btn-orange" onclick="pttForceRelease()" disabled>强制释放</button>
</div>
<div id="ptt-result" class="log-container" style="background: #0f172a; margin-top: 10px; height: 120px;">
<div class="log-item">PTT 接口响应将在此展示...</div>
<div class="log-item">等待 PTT 登录成功后激活功能...</div>
</div>
</div>
@ -637,121 +623,109 @@
setTimeout(loadHistory, 500);
// === PTT 接口测试逻辑 ===
var PTT_BASE = "http://127.0.0.1:8080/api/external/ptt";
// === PTT 业务测试逻辑 ===
var pttBtn = document.getElementById('ptt-speak-btn');
var pttStatusTag = document.getElementById('ptt-status-tag');
var pttResult = document.getElementById('ptt-result');
function showPttResult(msg) {
var container = document.getElementById('ptt-result');
var time = new Date().toLocaleTimeString();
var item = document.createElement('div');
item.className = 'log-item';
var displayMsg = typeof msg === 'string' ? msg : JSON.stringify(msg, null, 2);
item.innerHTML = '<span class="log-time">[' + time + ']</span><span class="log-msg">' + displayMsg + '</span>';
container.insertBefore(item, container.firstChild);
item.innerHTML = '<span class="log-time">[' + time + ']</span><span class="log-msg">' + msg + '</span>';
pttResult.insertBefore(item, pttResult.firstChild);
}
// 移除 async/await使用 Promise
function pttRequest(path, method, query, body) {
if (method === undefined) method = 'GET';
if (query === undefined) query = {};
if (body === undefined) body = null;
// PTT 登录成功回调 (由 Android 调用)
function onPttLoginSuccess() {
pttStatusTag.innerText = "PTT 已登录";
pttStatusTag.className = "ws-status ws-connected";
var url = PTT_BASE + path;
var qParams = [];
for (var key in query) {
qParams.push(encodeURIComponent(key) + "=" + encodeURIComponent(query[key]));
// 激活按钮
pttBtn.disabled = false;
pttBtn.style.background = "#ef4444";
pttBtn.style.cursor = "pointer";
var actionBtns = document.querySelectorAll('.ptt-action-btn');
for (var i = 0; i < actionBtns.length; i++) {
actionBtns[i].disabled = false;
}
if (qParams.length > 0) url += "?" + qParams.join("&");
var options = {
method: method,
headers: { 'Content-Type': 'application/json' }
};
if (body) options.body = JSON.stringify(body);
showPttResult("PTT 业务初始化成功,对讲功能已就绪");
}
fetch(url, options)
.then(function(response) {
if (path === '/record/play') {
showPttResult("流式播放接口已调用,请查看网络请求或直接点击预览链接");
return;
// 修复 PTT 按键逻辑:支持 TouchEvents 并处理异常中断
if (pttBtn) {
var isDown = false;
var startSpeak = function(e) {
if (pttBtn.disabled) return;
e.preventDefault();
if (!isDown) {
isDown = true;
pttBtn.style.background = "#b91c1c";
pttBtn.innerText = "正在通话...";
if (window.android && window.android.pttSpeakPlay) {
window.android.pttSpeakPlay();
}
return response.json();
})
.then(function(data) {
if (data) showPttResult(data);
})
.catch(function(e) {
showPttResult("请求失败: " + e.message);
});
}
showPttResult("申请话权...");
}
};
function pttCreateGroup() {
var name = document.getElementById('ptt-name').value;
var account = document.getElementById('ptt-account').value;
pttRequest('/group', 'POST', { gname: name }, [account]);
}
var stopSpeak = function(e) {
if (isDown) {
isDown = false;
pttBtn.style.background = "#ef4444";
pttBtn.innerText = "长按对讲 (PTT)";
if (window.android && window.android.pttSpeakRelease) {
window.android.pttSpeakRelease();
}
showPttResult("释放话权");
}
};
function pttDeleteGroup() {
var gid = document.getElementById('ptt-gid').value;
pttRequest('/group/' + gid, 'DELETE');
}
// 绑定事件
pttBtn.addEventListener('touchstart', startSpeak);
pttBtn.addEventListener('mousedown', startSpeak);
function pttAddMembers() {
var gid = document.getElementById('ptt-gid').value;
var account = document.getElementById('ptt-account').value;
pttRequest('/group/' + gid + '/members', 'POST', {}, [account]);
}
function pttKickMembers() {
var gid = document.getElementById('ptt-gid').value;
var account = document.getElementById('ptt-account').value;
pttRequest('/group/' + gid + '/members', 'DELETE', {}, [account]);
// 关键:除了 end/up还要处理 cancel 和 leave
pttBtn.addEventListener('touchend', stopSpeak);
pttBtn.addEventListener('touchcancel', stopSpeak);
pttBtn.addEventListener('mouseup', stopSpeak);
pttBtn.addEventListener('mouseleave', stopSpeak);
}
function pttGetGroups() {
pttRequest('/groups', 'GET', { page: 1, limit: 10 });
if (window.android && window.android.pttGetGroupInfo) {
window.android.pttGetGroupInfo();
showPttResult("已请求群组列表");
}
}
function pttGetTempGroups() {
pttRequest('/temp-groups', 'GET', { page: 1, limit: 10 });
}
function pttGetRecords() {
function pttGetMembers() {
var gid = document.getElementById('ptt-gid').value;
var now = new Date();
var end = now.getFullYear() + '-' + (now.getMonth() + 1) + '-' + now.getDate() + ' ' + now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
var start = "2024-01-01 00:00:00";
pttRequest('/group/' + gid + '/records', 'POST', { start: start, end: end, page: 1, limit: 5 }, []);
if (window.android && window.android.pttGetGroupMemberInfo) {
window.android.pttGetGroupMemberInfo(gid);
showPttResult("已请求群组 [" + gid + "] 成员");
}
}
function pttGetRecordText() {
var path = document.getElementById('ptt-record-path').value;
if (!path) return alert("请输入录音路径");
pttRequest('/record/text', 'GET', { path: path });
}
function pttPlayRecord() {
var path = document.getElementById('ptt-record-path').value;
if (!path) return alert("请输入录音路径");
var url = PTT_BASE + '/record/play?path=' + encodeURIComponent(path);
window.open(url, '_blank');
showPttResult("已尝试打开播放链接: " + url);
}
function pttCreateTempGroup() {
var name = document.getElementById('ptt-name').value;
var account = document.getElementById('ptt-account').value;
pttRequest('/temp-group', 'POST', { gname: name, creatorid: 1001 }, [account]);
}
function pttDeleteTempGroup() {
function pttJumpGroup() {
var gid = document.getElementById('ptt-gid').value;
pttRequest('/temp-group/' + gid, 'DELETE');
if (window.android && window.android.pttJumpGroup) {
window.android.pttJumpGroup(gid);
showPttResult("尝试切换至群组: " + gid);
}
}
function pttGetTempMembers() {
var gid = document.getElementById('ptt-gid').value;
pttRequest('/temp-group/' + gid + '/members', 'GET');
function pttForceRelease() {
if (window.android && window.android.pttForceRelease) {
window.android.pttForceRelease();
isDown = false;
pttBtn.style.background = "#ef4444";
pttBtn.innerText = "长按对讲 (PTT)";
showPttResult("执行强制释放");
}
}
</script>
</body>

View File

@ -36,6 +36,9 @@ public class CallBackResolution {
CallBackUtil.callBackLoinState(errorNum, loginId);
loginState = isLoginConflict(errorNum);
CallBackUtil.callBackLoginConflict(loginState);
if ("02".equals(errorNum)) {
com.stand.standapp.MainActivity.onPttLoginSuccess();
}
break;
case "a2"://账号有视频功能
@ -122,6 +125,8 @@ public class CallBackResolution {
GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo);
if (memberNo == 1) memberInfoDtos.clear();
memberInfoDtos.add(infoDto);
//输出群成员信息memberInfoDtos
Timber.tag(TAG).d("at_OEM_AT_cb group member info: " + infoDto);
}
break;

View File

@ -11,10 +11,79 @@ import com.stand.standapp.R
import com.stand.standapp.utils.LogManager
import com.stand.standapp.printer.PrinterManager
import timber.log.Timber
import com.example.kingway.ptt.SendAtUtil
import org.json.JSONObject
class AndroidInterface(private val activity: MainActivity) {
private var isSpeaking = false
/**
* PTT 登录
*/
@JavascriptInterface
fun pttLogin(account: String, pwd: String, ip: String) {
SendAtUtil.toLogin(account, pwd, ip, 1, 1)
}
/**
* PTT 开始说话 (带状态保护)
*/
@JavascriptInterface
fun pttSpeakPlay() {
if (!isSpeaking) {
isSpeaking = true
SendAtUtil.speakPlay()
Timber.tag("PTT").d("Speak Play")
}
}
/**
* PTT 停止说话
*/
@JavascriptInterface
fun pttSpeakRelease() {
if (isSpeaking) {
isSpeaking = false
SendAtUtil.speakRelease()
Timber.tag("PTT").d("Speak Release")
}
}
/**
* 强制释放 PTT (用于异常恢复)
*/
@JavascriptInterface
fun pttForceRelease() {
isSpeaking = false
SendAtUtil.speakRelease()
Timber.tag("PTT").d("Force Release")
}
/**
* PTT 获取群组
*/
@JavascriptInterface
fun pttGetGroupInfo() {
SendAtUtil.getGroupInfo()
}
/**
* PTT 获取成员
*/
@JavascriptInterface
fun pttGetGroupMemberInfo(gid: String) {
SendAtUtil.getGroupMemberInfo(gid)
}
/**
* PTT 切换群组
*/
@JavascriptInterface
fun pttJumpGroup(gid: String) {
SendAtUtil.jumpGroup(gid)
}
/**
* JS 调用获取打印机当前连接状态
*/

View File

@ -131,7 +131,17 @@ class LoginActivity : AppCompatActivity() {
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
try {
// 确保 PTT 引擎已初始化
com.example.kingway.ptt.MyApplication.init(this@LoginActivity)
// 异步初始化 PTT 登录 (区分 Android 业务登录)
com.example.kingway.ptt.SendAtUtil.toLogin("gzzdweb01", "123456", "61.243.1.123", 1, 1)
} catch (e: Exception) {
Timber.tag("Login").e(e, "检查更新失败")
}
// 初始化打印机 (异步)
PrinterManager.init(this@LoginActivity)
startActivity(Intent(this@LoginActivity, MainActivity::class.java))

View File

@ -20,7 +20,27 @@ import com.stand.standapp.UpdateManager
class MainActivity : AppCompatActivity() {
companion object {
private var instance: MainActivity? = null
@JvmStatic
fun executeJs(script: String) {
instance?.let { activity ->
activity.runOnUiThread {
activity.mAgentWeb?.jsAccessEntrace?.quickCallJs(script)
}
}
}
@JvmStatic
fun onPttLoginSuccess() {
Timber.tag("MainActivity").d("onPttLoginSuccess called from Native")
executeJs("onPttLoginSuccess")
}
}
private var mAgentWeb: AgentWeb? = null
private val mHandler = android.os.Handler(android.os.Looper.getMainLooper())
private val TIMEOUT_MS = 30000L
@ -30,8 +50,9 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = this
try {
setContentView(R.layout.activity_main)
val container = findViewById<LinearLayout>(R.id.container)

View File

@ -39,16 +39,19 @@ object PrinterManager {
if (sf != null) return
mContext = context.applicationContext
// 全异步初始化,不阻塞主线程跳转
executor.execute {
// Serialport_Factory 构造函数内部会创建 Handler必须在有 Looper 的线程(如主线程)调用
mHandler.post {
try {
Timber.tag("Printer").i("开始异步初始化打印机组件...")
Timber.tag("Printer").i("正在主线程初始化打印机组件...")
sf = Serialport_Factory.getSerialport_Factory(mContext, mHandler)
// 启动自动重连定时任务
executor.scheduleWithFixedDelay({
checkAndConnect()
}, 0, 10, TimeUnit.SECONDS)
// 成功创建 factory 后,将后续耗时的连接任务丢给子线程
executor.execute {
// 启动自动重连定时任务
executor.scheduleWithFixedDelay({
checkAndConnect()
}, 0, 10, TimeUnit.SECONDS)
}
Timber.tag("Printer").i("打印机组件初始化任务已分发")
} catch (e: Exception) {