最近項目中,因為需要在WEB頁面上操作串口,包括串口查詢、打開、發送指令、接收數據、關閉串口等功能。如下所示:
考慮使用ActiveX來實現。因為以前沒有這方面的經驗,開發過程中也是遇到各種問題。廢話不多說,下面進入正題:
1:打開VS2008,新建項目,以下是具體代碼:

1 using System; 2 using System.Collections.Generic; 3 using System.Windows.Forms; 4 using System.IO.Ports; 5 using System.IO; 6 using System.Reflection; 7 using System.Runtime.InteropServices; 8 using mshtml; 9 using System.Text; 10 using Microsoft.Win32; 11 using System.Threading; 12 13 namespace WebSerial 14 { 15 [ProgId("WebSerial")]//控件名稱 16 [ClassInterface(ClassInterfaceType.AutoDual), ComSourceInterfaces(typeof(ControlEvents))] 17 [Guid("6C6A0DE4-193A-48f5-BA91-3C180558785B")]//控件的GUID,用於COM注冊和HTML中Object對象classid引用 18 [ComVisible(true)] 19 public partial class SerialPortControl : UserControl,IObjectSafety 20 { 21 ExSerialPort serialPort; 22 List<byte> buffer = new List<byte>(4096); 23 24 /// <summary> 25 /// 是否准備關閉串口 26 /// </summary> 27 private static bool m_IsTryToClosePort = false; 28 /// <summary> 29 /// 是否正在接收數據 30 /// </summary> 31 private static bool m_IsReceiving = false; 32 public SerialPortControl() 33 { 34 35 } 36 37 /// <summary> 38 /// 獲取本地串口列表,以逗號隔開 39 /// </summary> 40 /// <returns></returns> 41 public string getComPorts() 42 { 43 string ports = ""; 44 foreach (string s in ExSerialPort.GetPortNames()) 45 { 46 ports += "," + s; 47 } 48 return ports.Length > 0 ? ports.Substring(1) : ""; 49 } 50 51 /// <summary> 52 /// 以指定串口號和波特率連接串口 53 /// </summary> 54 /// <param name="com">端口號</param> 55 /// <param name="baudRate">波特率</param> 56 /// <returns></returns> 57 [ComVisible(true)] 58 public string connect(string com, int baudRate) 59 { 60 close(); 61 serialPort = null; 62 serialPort = new ExSerialPort(com); 63 serialPort.BaudRate = baudRate; 64 serialPort.Parity = Parity.None; 65 serialPort.DataBits = 8; 66 serialPort.Encoding = Encoding.ASCII; 67 serialPort.ReceivedBytesThreshold = 5; 68 serialPort.ReadBufferSize = 102400; 69 70 try 71 { 72 serialPort.Open(); 73 if (serialPort.IsOpen) 74 { 75 m_IsTryToClosePort = false; 76 this.clear(); 77 serialPort.DataReceived += new SerialDataReceivedEventHandler(serialPort_DataReceived); 78 return "true"; 79 } 80 } 81 catch { } 82 83 return "false"; 84 } 85 86 /// <summary> 87 /// 清理串口數據並關閉串口 88 /// </summary> 89 [ComVisible(true)] 90 public void close() 91 { 92 m_IsTryToClosePort = true; 93 while (m_IsReceiving) 94 { 95 Application.DoEvents(); 96 } 97 98 if (serialPort != null) 99 { 100 serialPort.Dispose(); 101 } 102 } 103 104 /// <summary> 105 /// 清理串口數據 106 /// </summary> 107 [ComVisible(true)] 108 public void clear() 109 { 110 if (serialPort != null && serialPort.IsOpen) 111 { 112 serialPort.Clear(); 113 } 114 } 115 116 /// <summary> 117 /// 發送字符串 118 /// </summary> 119 /// <param name="s"></param> 120 [ComVisible(true)] 121 public void writeString(string hexString) 122 { 123 if (serialPort != null && serialPort.IsOpen) 124 { 125 byte[] bytes = strToToHexByte(hexString); 126 serialPort.Write(bytes, 0, bytes.Length); 127 } 128 } 129 130 /// <summary> 131 /// 字符串轉16進制字節數組 132 /// </summary> 133 /// <param name="hexString"></param> 134 /// <returns></returns> 135 private static byte[] strToToHexByte(string hexString) 136 { 137 hexString = hexString.Replace(" ", ""); 138 if ((hexString.Length % 2) != 0) 139 hexString += " "; 140 byte[] returnBytes = new byte[hexString.Length / 2]; 141 for (int i = 0; i < returnBytes.Length; i++) 142 returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); 143 return returnBytes; 144 } 145 146 /// <summary> 147 /// 字節數組轉字符串16進制 148 /// </summary> 149 /// <param name="InBytes"> 二進制字節 </param> 150 /// <returns>類似"01 02 0F" </returns> 151 public static string ByteToString(byte[] InBytes) 152 { 153 string StringOut = ""; 154 foreach (byte InByte in InBytes) 155 { 156 //StringOut += String.Format("{0:X2}", InByte) + " "; 157 StringOut += " " + InByte.ToString("X").PadLeft(2, '0'); 158 } 159 160 return StringOut.Trim(); 161 } 162 163 /// <summary> 164 /// 接收數據 165 /// </summary> 166 /// <param name="sender"></param> 167 /// <param name="e"></param> 168 void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) 169 { 170 if (m_IsTryToClosePort) 171 { 172 return; 173 } 174 175 m_IsReceiving = true; 176 177 try 178 { 179 int n = serialPort.BytesToRead; 180 if (n > 0) 181 { 182 byte[] buf = new byte[n]; 183 serialPort.Read(buf, 0, n); 184 string dataString = ByteToString(buf); 185 Receive(dataString); 186 } 187 188 ////1.緩存數據 189 //buffer.AddRange(buf); 190 ////2.完整性判斷 191 //while (buffer.Count >= 5) //至少包含幀頭(2字節)、長度(1字節)、校驗位(1字節);根據設計不同而不同 192 //{ 193 // //2.1 查找數據頭 194 // if (buffer[0] == 0xff && buffer[1] == 0x55) //傳輸數據有幀頭,用於判斷 195 // { 196 // int len = buffer[2]; 197 // if (buffer.Count < len + 4) //數據區尚未接收完整 198 // { 199 // break; 200 // } 201 // //得到完整的數據,復制到ReceiveBytes中進行校驗 202 // byte[] ReceiveBytes = new byte[len + 4]; 203 // buffer.CopyTo(0, ReceiveBytes, 0, len + 4); 204 205 // byte checkByte = ReceiveBytes[len + 3];//獲取校驗字節 206 // byte realCheckByte = 0x00; 207 // realCheckByte -= buffer[2]; 208 // for (int packIndex = 0; packIndex < len; packIndex++)//將后面的數據加起來 209 // { 210 // realCheckByte -= ReceiveBytes[packIndex + 3]; 211 // } 212 213 // if (checkByte == realCheckByte)//驗證,看數據是否合格 214 // { 215 // string dataString = ByteToString(ReceiveBytes); 216 // Receive(dataString); 217 // } 218 // buffer.RemoveRange(0, len + 4); 219 // } 220 // else //幀頭不正確時,清除 221 // { 222 // buffer.RemoveAt(0); 223 // } 224 225 //} 226 } 227 finally 228 { 229 m_IsReceiving = false; 230 } 231 } 232 233 public event ControlEventHandler OnReceive; 234 [ComVisible(true)] 235 private void Receive(string dataString) 236 { 237 if (OnReceive != null) 238 { 239 OnReceive(dataString); //Calling event that will be catched in JS 240 } 241 } 242 243 /// <summary> 244 /// Register the class as a control and set it's CodeBase entry 245 /// </summary> 246 /// <param name="key">The registry key of the control</param> 247 [ComRegisterFunction()] 248 public static void RegisterClass(string key) 249 { 250 // Strip off HKEY_CLASSES_ROOT\ from the passed key as I don't need it 251 StringBuilder sb = new StringBuilder(key); 252 253 sb.Replace(@"HKEY_CLASSES_ROOT\", ""); 254 // Open the CLSID\{guid} key for write access 255 RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true); 256 257 // And create the 'Control' key - this allows it to show up in 258 // the ActiveX control container 259 RegistryKey ctrl = k.CreateSubKey("Control"); 260 ctrl.Close(); 261 262 // Next create the CodeBase entry - needed if not string named and GACced. 263 RegistryKey inprocServer32 = k.OpenSubKey("InprocServer32", true); 264 inprocServer32.SetValue("CodeBase", Assembly.GetExecutingAssembly().CodeBase); 265 inprocServer32.Close(); 266 // Finally close the main key 267 k.Close(); 268 MessageBox.Show("Registered"); 269 } 270 271 /// <summary> 272 /// Called to unregister the control 273 /// </summary> 274 /// <param name="key">Tke registry key</param> 275 [ComUnregisterFunction()] 276 public static void UnregisterClass(string key) 277 { 278 StringBuilder sb = new StringBuilder(key); 279 sb.Replace(@"HKEY_CLASSES_ROOT\", ""); 280 281 // Open HKCR\CLSID\{guid} for write access 282 RegistryKey k = Registry.ClassesRoot.OpenSubKey(sb.ToString(), true); 283 284 // Delete the 'Control' key, but don't throw an exception if it does not exist 285 k.DeleteSubKey("Control", false); 286 287 // Next open up InprocServer32 288 //RegistryKey inprocServer32 = 289 k.OpenSubKey("InprocServer32", true); 290 291 // And delete the CodeBase key, again not throwing if missing 292 k.DeleteSubKey("CodeBase", false); 293 294 // Finally close the main key 295 k.Close(); 296 MessageBox.Show("UnRegistered"); 297 } 298 299 #region IObjectSafety 成員 300 public void GetInterfacceSafyOptions(int riid, out int pdwSupportedOptions, out int pdwEnabledOptions) 301 { 302 pdwSupportedOptions = 1; 303 pdwEnabledOptions = 2; 304 } 305 public void SetInterfaceSafetyOptions(int riid, int dwOptionsSetMask, int dwEnabledOptions) 306 { 307 throw new NotImplementedException(); 308 } 309 #endregion 310 311 #region IObjectSafety 成員 312 313 private const string _IID_IDispatch = "{00020400-0000-0000-C000-000000000046}"; 314 private const string _IID_IDispatchEx = "{a6ef9860-c720-11d0-9337-00a0c90dcaa9}"; 315 private const string _IID_IPersistStorage = "{0000010A-0000-0000-C000-000000000046}"; 316 private const string _IID_IPersistStream = "{00000109-0000-0000-C000-000000000046}"; 317 private const string _IID_IPersistPropertyBag = "{37D84F60-42CB-11CE-8135-00AA004BB851}"; 318 319 private const int INTERFACESAFE_FOR_UNTRUSTED_CALLER = 0x00000001; 320 private const int INTERFACESAFE_FOR_UNTRUSTED_DATA = 0x00000002; 321 private const int S_OK = 0; 322 private const int E_FAIL = unchecked((int)0x80004005); 323 private const int E_NOINTERFACE = unchecked((int)0x80004002); 324 325 private bool _fSafeForScripting = true; 326 private bool _fSafeForInitializing = true; 327 328 329 public int GetInterfaceSafetyOptions(ref Guid riid, ref int pdwSupportedOptions, ref int pdwEnabledOptions) 330 { 331 int Rslt = E_FAIL; 332 333 string strGUID = riid.ToString("B"); 334 pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA; 335 switch (strGUID) 336 { 337 case _IID_IDispatch: 338 case _IID_IDispatchEx: 339 Rslt = S_OK; 340 pdwEnabledOptions = 0; 341 if (_fSafeForScripting == true) 342 pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER; 343 break; 344 case _IID_IPersistStorage: 345 case _IID_IPersistStream: 346 case _IID_IPersistPropertyBag: 347 Rslt = S_OK; 348 pdwEnabledOptions = 0; 349 if (_fSafeForInitializing == true) 350 pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_DATA; 351 break; 352 default: 353 Rslt = E_NOINTERFACE; 354 break; 355 } 356 357 return Rslt; 358 } 359 360 public int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions) 361 { 362 int Rslt = E_FAIL; 363 364 string strGUID = riid.ToString("B"); 365 switch (strGUID) 366 { 367 case _IID_IDispatch: 368 case _IID_IDispatchEx: 369 if (((dwEnabledOptions & dwOptionSetMask) == INTERFACESAFE_FOR_UNTRUSTED_CALLER) && 370 (_fSafeForScripting == true)) 371 Rslt = S_OK; 372 break; 373 case _IID_IPersistStorage: 374 case _IID_IPersistStream: 375 case _IID_IPersistPropertyBag: 376 if (((dwEnabledOptions & dwOptionSetMask) == INTERFACESAFE_FOR_UNTRUSTED_DATA) && 377 (_fSafeForInitializing == true)) 378 Rslt = S_OK; 379 break; 380 default: 381 Rslt = E_NOINTERFACE; 382 break; 383 } 384 385 return Rslt; 386 } 387 388 #endregion 389 } 390 391 /// <summary> 392 /// Event handler for events that will be visible from JavaScript 393 /// </summary> 394 public delegate void ControlEventHandler(string dataString); 395 396 /// <summary> 397 /// This interface shows events to javascript 398 /// </summary> 399 [Guid("68BD4E0D-D7BC-4cf6-BEB7-CAB950161E79")] 400 [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] 401 public interface ControlEvents 402 { 403 //Add a DispIdAttribute to any members in the source interface to specify the COM DispId. 404 [DispId(0x60020001)] 405 void OnReceive(string dataString); //This method will be visible from JS 406 } 407 408 public class ExSerialPort : SerialPort 409 { 410 public ExSerialPort(string name) 411 : base(name) 412 { 413 } 414 415 public void Clear() 416 { 417 try 418 { 419 if (this.IsOpen) 420 { 421 this.DiscardInBuffer(); 422 this.DiscardOutBuffer(); 423 } 424 } 425 catch { } 426 } 427 428 protected override void Dispose(bool disposing) 429 { 430 Clear(); 431 432 var stream = (Stream)typeof(SerialPort).GetField("internalSerialStream", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(this); 433 434 if (stream != null) 435 { 436 stream.Dispose(); 437 } 438 439 base.Dispose(disposing); 440 } 441 } 442 }
2:新建安裝項目,將上面項目DLL打包生成Msi安裝文件。這里不多說,網上大把文章教你怎么做。
3:新建Web項目,具體代碼如下:

1 <html> 2 <head> 3 <title>JavaScript串口測試</title> 4 <meta http-equiv="Content-Type" content="text/html; charset=GB2312" /> 5 <script language="javascript" type="text/javascript"> 6 <!-- 7 function getComPorts() 8 { 9 var ports = webSerial.getComPorts(); 10 var arr = ports.split(','); 11 var ctl = document.getElementById("ComName"); 12 if(ctl) 13 { 14 ctl.options.length = 0; 15 for(var i=0;i<arr.length;i++) 16 { 17 ctl.options[i] = new Option(arr[i],arr[i]); 18 } 19 } 20 } 21 22 function connectComPort() 23 { 24 var com = document.getElementById("ComName").value.toString(); 25 var baudRate = document.getElementById("BaudRate").value; 26 var result = webSerial.connect(com,baudRate); 27 alert(result); 28 } 29 30 function clearComPort() 31 { 32 webSerial.clear(); 33 alert("清理串口成功!"); 34 } 35 36 function closeComPort() 37 { 38 webSerial.close(); 39 alert("關閉串口成功!"); 40 } 41 42 function writeString() 43 { 44 var hexString = document.getElementById("txtSend").value.toString(); 45 webSerial.writeString(hexString); 46 alert("發送指令成功!"); 47 } 48 49 function clearSend() 50 { 51 document.getElementById("txtSend").innerText=""; 52 } 53 54 function clearReceive() 55 { 56 document.getElementById("txtReceive").innerText=""; 57 } 58 59 function startTime() 60 { 61 var today=new Date() 62 var h=today.getHours() 63 var m=today.getMinutes() 64 var s=today.getSeconds() 65 // add a zero in front of numbers<10 66 m=checkTime(m) 67 s=checkTime(s) 68 document.getElementById('lblTime').innerHTML=h+":"+m+":"+s 69 t=setTimeout('startTime()',500) 70 } 71 72 function checkTime(i) 73 { 74 if (i<10) 75 {i="0" + i} 76 return i 77 } 78 79 --> 80 </script> 81 82 </head> 83 <body onload="startTime()" onunload="closeComPort()"> 84 <form name="form1"> 85 <fieldset style="width:225px;height:180px;text-align:center;"> 86 <legend>串口</legend> 87 <div style="float:left;width:250px"> 88 <br/> 89 <span>串口號:</span> 90 <select name="ComName" id="ComName" style="width:100px" > 91 </select> 92 <br/> 93 <span>波特率:</span> 94 <select name="BaudRate" id="BaudRate" style="width:100px" > 95 <option value="9600" selected="selected">9600</option> 96 <option value="57600" >57600</option> 97 <option value="115200" >115200</option> 98 <option value="1382400" >1382400</option> 99 <option value="3000000" >3000000</option> 100 </select> 101 <br/> 102 <br/> 103 <input type="button" id="btnGetPort" style="width:80px;height:30px;font-size:13px" name="btnGetPort" value="獲取串口" onclick="getComPorts()"/> 104 <input type="button" id="btnOpenPort" style="width:80px;height:30px;font-size:13px" name="btnOpenPort" value="打開串口" onclick="connectComPort()"/> 105 <input type="button" id="btnClearPort" style="width:80px;height:30px;font-size:13px" name="btnClearPort" value="清理串口" onclick="clearComPort()"/> 106 <input type="button" id="btnClosePort" style="width:80px;height:30px;font-size:13px" name="btnClosePort" value="關閉串口" onclick="closeComPort()"/> 107 </div> 108 </fieldset> 109 <br /><br /> 110 <fieldset style="width:800px;height:150px;text-align:center;"> 111 <legend>發送區域</legend> 112 <div align="left">V6-握手指令: FF 55 01 01 FE</div> 113 <div align="left">V6-50HZ采集:FF 55 05 03 20 4E 00 00 8A</div> 114 <div align="left">V6-停止指令: FF 55 01 10 EF</div> 115 <div style="float:left;"> 116 <textarea id="txtSend" name="txtSend" style="width:800px;height:80px"></textarea> 117 <br/> 118 <input type="button" id="btnSend" style="width:100px;height:30px" name="btnSend" value="發送" onclick="writeString()"/> 119 <input type="button" id="btnClearSend" style="width:100px;height:30px" name="btnClearSend" value="清空" onclick="clearSend()"/> 120 </div> 121 </fieldset> 122 <br /><br /> 123 <fieldset style="width:800px;height:500px;text-align:center;"> 124 <legend>接收區域</legend> 125 <div style="float:left;"> 126 <textarea id="txtReceive" readonly="readonly" name="txtReceive" style="width:800px;height:430px"></textarea> 127 <br/> 128 <input type="button" id="btnClearReceive" style="width:100px;height:30px" name="btnClearReceive" value="清空" onclick="clearReceive()"/> 129 </div> 130 </fieldset> 131 <span id="lblTime"></span> 132 </form> 133 134 <p> 135 136 <object classid="clsid:6C6A0DE4-193A-48f5-BA91-3C180558785B" codebase="../WebSerialSetup.msi" width="442" height="87" id="webSerial" name="webSerial"> 137 </object> 138 </p> 139 140 <!-- Attaching to an ActiveX event--> 141 <script language="javascript" type="text/javascript"> 142 function webSerial::OnReceive(dataString) 143 { 144 document.getElementById("txtReceive").value += dataString+"\r\n";; 145 } 146 </script> 147 </body> 148 </html>
在使用JS調用ActiveX的時候碰上問題一:方法可以成功調用,而事件卻調用失敗。網上文章大都是說JS如何調ActiveX,而ActiveX這邊的方法或者事件需要滿足什么條件才能被JS成功調用卻少有涉及。正當我山窮水盡疑無路的時候,事情有了轉折,無意中看到一篇老外寫的文章。鏈接地址是:http://www.codeproject.com/Articles/24089/Create-ActiveX-in-NET-Step-by-Step。才知道事件需要實現一個接口才能被JS識別。所以這部分代碼后面被加上去了:
/// <summary> /// Event handler for events that will be visible from JavaScript /// </summary> public delegate void ControlEventHandler(string dataString); /// <summary> /// This interface shows events to javascript /// </summary> [Guid("68BD4E0D-D7BC-4cf6-BEB7-CAB950161E79")] [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface ControlEvents { //Add a DispIdAttribute to any members in the source interface to specify the COM DispId. [DispId(0x60020001)] void OnReceive(string dataString); //This method will be visible from JS }
控件類名前面也加上這個
[ClassInterface(ClassInterfaceType.AutoDual), ComSourceInterfaces(typeof(ControlEvents))]
事件定義如下:
public event ControlEventHandler OnReceive; [ComVisible(true)] private void Receive(string dataString) { if (OnReceive != null) { OnReceive(dataString); //Calling event that will be catched in JS } }
本地打開網頁,執行全部功能,操作正常,大功告成!!!
隨后我把網頁在本地發布並使用局域網中其他電腦(操作系統WIN7)IE訪問該網頁。那么問題來了:
1:機器彈出對話框拒絕安裝當前ActiveX。經過對IE internet選項設置,放開對無簽名ActiveX訪問限制后。有的機器能彈出安裝對話框,有的機器仍然堅決禁止。而且就算是彈出安裝對話框,在確認安裝后,對話框消失,插件也沒裝上。。。
好吧,這個問題也是沒搞明白啥原因。后面時間緊迫,只好給客戶一個下載鏈接,自己去點擊下載。
2:下載安裝包並安裝完畢后,在客戶機器上操作網頁功能。前面幾個按鈕功能都OK,但是在填入指令點擊發送,網頁出現崩潰重新刷新的情況,而且換了幾台機器都是這樣。后面想起在生成安裝包的時候,有彈出一個對話框,提示Visual Studio registry capture utility 已停止工作。百度一番后,找到解決方法:在Microsoft Visual Studio 9.0/Common7/Tools/Deployment 路徑下面的regcap.exe文件,點擊右鍵在屬性頁面中,選擇兼容性頁面,選中“以兼容模式運行”框就好了。兼容win7 就行。
相應設置后,重新生成安裝文件。。。在客戶機上安裝后,一切正常!!!
最后附上完整項目代碼:http://pan.baidu.com/s/1hq8MzJ2