C#开发纽曼USB来电小秘书客户端:API调用与录音上传实现

键盘旁的猫
键盘旁的猫
发布时间:2026-10-15| 6人浏览过
原创

为解决客户对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),程序必须主动查询硬件状态来判断呼叫何时结束。实现这一目标主要有两种思路:

  1. Timer(定时器)轮询: Delphi的原生Demo使用了这种方式。但在C#中使用Timer处理复杂的同步逻辑(如等待用户挂断电话)会导致代码结构碎片化,需要大量异步回调拼接,可读性和灵活性较差。
  2. 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#硬件开发与网络通信的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。