C#开发纽曼USB来电小秘书客户端:API调用与录音上传实现
为解决客户对CRM系统来电弹屏及录音存档的需求,需开发纽曼USB来电小秘书客户端。由于该硬件API缺乏来电回调支持且文档不详,开发者通过C# P/Invoke封装底层驱动,利用多线程同步检测挂断状态,实现了呼入呼出号码识别、本地PCM录音转换及大文件到CRM服务器的稳定上传。
开发背景与挑战
在CRM来电弹屏功能上线后,业务端提出了新的要求:不仅要在屏幕上显示来电信息,还需自动录制呼入和呼出的电话录音,并将音频文件上传至服务器以便后续归档和质检。为此,项目组决定开发一个名为“来电小秘书”的后台客户端程序。
本次开发的硬件基础是纽曼品牌的USB电话设备(俗称“来电小秘书”)。虽然它与之前使用的“纽曼USB来电通”同属一家厂商,但两款硬件的API完全不兼容。更棘手的是,“来电小秘书”的官方API没有提供来电触发的回调接口,也没有配套的C#开发示例(Demo仅提供Delphi版本),且配套的开发文档不够详尽。这迫使开发者必须通过逆向分析Delphi Demo和反复测试来摸索出稳定的交互方式。
第一步:硬件驱动封装与接口映射
与USB电话硬件通信的核心在于调用底层的 usbms.dll 动态链接库。在C#环境中,通常使用 P/Invoke (Platform Invoke) 技术将这些非托管函数的声明引入托管代码中。
以下是关键的驱动函数封装代码。它定义了加载驱动、初始化、摘挂机控制、信号检测以及录音管理等核心方法:
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace WindowsApplication1
{
class LDT1
{
// 驱动加载与释放
[DllImport("usbms.dll", EntryPoint = "LoadDRV")]
public static extern int LoadDRV();
[DllImport("usbms.dll", EntryPoint = "FreeDRV")]
public static extern int FreeDRV();
// 硬件状态控制
[DllImport("usbms.dll", EntryPoint = "EnableCard")]
public static extern int EnableCard();
[DllImport("usbms.dll", EntryPoint = "DisableCard")]
public static extern int DisableCard();
[DllImport("usbms.dll", EntryPoint = "ReSetUsb")]
public static extern int ReSetUsb(int Handle);
// 通话控制
[DllImport("usbms.dll", EntryPoint = "HangUp")]
public static extern int HangUp(int Handle);
[DllImport("usbms.dll", EntryPoint = "StopSigCheck")]
public static extern int StopSigCheck(int Handle);
[DllImport("usbms.dll", EntryPoint = "StartSigCheck")]
public static extern void StartSigCheck(int line);
// 信号与状态检测
[DllImport("usbms.dll", EntryPoint = "IsRing")]
public static extern bool IsRing(UInt16 Line);
[DllImport("usbms.dll", EntryPoint = "IsOffHook")]
public static extern bool IsOffHook(UInt16 Line); // 判断是否摘机
[DllImport("usbms.dll", EntryPoint = "ReadUsbState")]
public static extern bool ReadUsbState(int line);
[DllImport("usbms.dll", EntryPoint = "GetRingNum")]
public static extern int GetRingNum(int line);
[DllImport("usbms.dll", EntryPoint = "InitRingNum")]
public static extern void InitRingNum(int line);
// 号码读取
[DllImport("usbms.dll", EntryPoint = "GetCallerIDStr")]
public static extern UInt16 GetCallerIDStr(UInt16 Line, StringBuilder IDStr);
[DllImport("usbms.dll", EntryPoint = "ReadSerialNo")]
public static extern int ReadSerialNo(int line,StringBuilder serialNo);
// DTMF按键检测
[DllImport("usbms.dll", EntryPoint = "GetDtmfCode")]
public static extern int GetDtmfCode(UInt16 Line);
[DllImport("usbms.dll", EntryPoint = "InitDtmfBuf")]
public static extern int InitDtmfBuf(int Handle);
// 录音管理
[DllImport("usbms.dll", EntryPoint = "StartRecordFile")]
public static extern bool StartRecordFile(UInt16 Line, string FileName, UInt32 dwRecordLen);
[DllImport("usbms.dll", EntryPoint = "StopRecordFile")]
public static extern bool StopRecordFile(UInt16 Line);
[DllImport("usbms.dll", EntryPoint = "CheckRecordEnd")]
public static extern bool CheckRecordEnd(UInt16 Line);
[DllImport("usbms.dll", EntryPoint = "PCMtoWave")]
public static extern int PCMtoWave(string SourceFileName, string TargetFileName);
// 其他参数设置
[DllImport("usbms.dll", EntryPoint = "SetDialPara")]
public static extern int SetDialPara(UInt16 RingBack1, UInt16 RingBack0, UInt16 BusyLen, UInt16 RingTimes, UInt16 SendNoSignalLen);
[DllImport("usbms.dll", EntryPoint = "ReadCheckResult")]
public static extern int ReadCheckResult(int line, int mode);
}
}
第二步:挂机检测策略设计
由于纽曼USB来电小秘书的API不支持来电自动回调(Callback),程序必须主动查询硬件状态来判断呼叫何时结束。实现这一目标主要有两种思路:
- Timer(定时器)轮询: Delphi的原生Demo使用了这种方式。但在C#中使用Timer处理复杂的同步逻辑(如等待用户挂断电话)会导致代码结构碎片化,需要大量异步回调拼接,可读性和灵活性较差。
- Thread(线程)轮询: C#创建线程非常便捷。使用独立的后台线程进行循环检测,可以保持同步编程的思维习惯(顺序执行),逻辑更加清晰,且易于与其他业务逻辑隔离。
最终方案采用了Thread模式。主线程负责接收来电弹屏等即时响应,而后台线程专门负责监听线路状态,一旦检测到用户挂机(OffHook变为False),即触发后续的录音保存和数据上传流程。
第三步:去电号码获取与DTMF解码
获取来电号码(CallerID)相对简单,只需在振铃期间调用 GetCallerIDStr 即可。但获取去电号码则较为复杂,因为拨号过程中会包含DTMF(双音多频)按键信号,这些信号会被混入电话号码中。
程序需要在摘机状态下,持续读取DTMF码流,并将其转换为对应的字符。以下代码演示了如何在线程中轮询读取DTMF编码并进行转换:
// 假设 line 是当前占用线路
while (LDT1.IsOffHook((ushort)this.line))
{
int temp = LDT1.GetDtmfCode((ushort)this.line);
if (temp > 0)
{
phonenum = phonenum + convertInt(temp);
}
Thread.Sleep(300); // 适当延时避免CPU占用过高
}
// DTMF码值转字符方法
private string convertInt(int code)
{
string ret="";
switch (code)
{
case 10: ret = "0"; break;
case 11: ret = "*"; break;
case 12: ret = "#"; break;
case 13: ret = "A"; break;
case 14: ret = "B"; break;
case 15: ret = "C"; break;
case 16: ret = "D"; break;
default: ret = code.ToString(); break;
}
return ret;
}
注意:具体的DTMF码值(如10对应'0')依赖于硬件驱动的具体实现版本,若实际运行中发现号码缺失,请核对 usbms.dll 的实际返回值定义。
第四步:解决大文件上传失败问题
电话录音文件通常为WAV或PCM格式,体积较大。将文件上传至CRM服务器时,网络上流传的一些C# HTTP POST示例代码往往无法成功上传。经过调试发现,问题根源在于HTTPmultipart/form-data协议的边界符(Boundary)构造不规范。
许多示例代码在处理HTTP头部的换行符时,错误地将 \r\n 替换为了空字符串或单一的 \n,导致服务器无法正确解析请求体。正确的做法是严格保留 \r\n 作为CRLF序列。
以下是修正后的可靠上传方法,关键改进点在于显式构造 \r\n 字节数组并确保其在Header结尾和Body结束处的正确使用:
public static string UploadFileEx(string uploadfile, string url,
string fileFormName, string contenttype, NameValueCollection querystring,
CookieContainer cookies)
{
if ((fileFormName == null) || (fileFormName.Length == 0)) fileFormName = "file";
if ((contenttype == null) || (contenttype.Length == 0)) contenttype = "application/octet-stream";
string postdata = "?";
if (querystring != null)
{
foreach (string key in querystring.Keys)
{
postdata += key + "=" + querystring.Get(key) + "&";
}
}
Uri uri = new Uri(url + postdata);
string boundary = "----------" + DateTime.Now.Ticks.ToString("x");
HttpWebRequest webrequest = (HttpWebRequest)WebRequest.Create(uri);
webrequest.ContentType = "multipart/form-data; boundary=" + boundary;
webrequest.Method = "POST";
// 【关键修复】明确定义 CRLF
string huanhang = "\r\n";
byte[] huanhangbyte = Encoding.UTF8.GetBytes(huanhang);
// 构建 Header
StringBuilder sb = new StringBuilder();
sb.Append("--" + boundary);
sb.Append(huanhang);
sb.Append("Content-Disposition: form-data; name=\"" + fileFormName + "\"; filename=\"");
sb.Append(System.IO.Path.GetFileName(uploadfile));
sb.Append("\"");
sb.Append(huanhang);
sb.Append("Content-Type: " + contenttype);
sb.Append(huanhang);
sb.Append(huanhang); // 头部结束需要一个额外的 \r\n
string postHeader = sb.ToString();
byte[] postHeaderBytes = Encoding.UTF8.GetBytes(postHeader);
// 尾部 Boundary
byte[] boundaryBytes = Encoding.ASCII.GetBytes("--" + boundary + "--"); // 注意最后的 --
using (FileStream fileStream = new FileStream(uploadfile, FileMode.Open, FileAccess.Read))
{
long length = postHeaderBytes.Length + fileStream.Length + boundaryBytes.Length + huanhangbyte.Length;
webrequest.ContentLength = length;
Stream requestStream = webrequest.GetRequestStream();
// 写入 Header
requestStream.Write(postHeaderBytes, 0, postHeaderBytes.Length);
// 写入文件内容
byte[] buffer = new Byte[checked((uint)Math.Min(4096, (int)fileStream.Length))];
int bytesRead;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
requestStream.Write(buffer, 0, bytesRead);
// 【关键修复】文件数据后添加 CRLF
requestStream.Write(huanhangbyte, 0, huanhangbyte.Length);
// 写入尾部 Boundary
requestStream.Write(boundaryBytes, 0, boundaryBytes.Length);
requestStream.Close();
fileStream.Close();
}
WebResponse responce = webrequest.GetResponse();
Stream s = responce.GetResponseStream();
StreamReader sr = new StreamReader(s);
string retval = sr.ReadToEnd();
sr.Dispose();
s.Dispose();
// 上传成功后可选删除本地临时文件
if (File.Exists(uploadfile))
{
try { File.Delete(uploadfile); }
catch (Exception e) { /* 忽略删除异常 */ }
}
return retval;
}
总结
通过与底层DLL的深度交互和自定义多线程状态机,我们克服了硬件API缺乏文档和回调接口的限制。特别是在网络传输层,修复HTTP multipart协议的CRLF解析错误,确保了录音文件能准确送达CRM服务器。此客户端的成功部署,完善了企业的语音沟通闭环管理能力。 以上就是C#开发纽曼USB来电小秘书客户端实现的详细内容,更多关于C#硬件开发与网络通信的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













