QT 绿米QTA协议 (二)AS
几圈年轮 Android 串口通信(一) giuhub demo 地址 https://github.com/MickJson/AndroidUSBSerialPort
一.PowerUpdate
1.1.UI
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | <?xml version= "1.0" encoding= "utf-8" ?> <LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android" android:layout_width= "match_parent" android:layout_height= "match_parent" android:orientation= "vertical" > <LinearLayout android:layout_width= "match_parent" android:layout_height= "0dp" android:layout_weight= "7" android:gravity= "center" android:orientation= "horizontal" > <ScrollView android:id= "@+id/sv_send" android:layout_width= "match_parent" android:layout_height= "match_parent" android:layout_weight= "5" android:background= "@drawable/bg_white_stroke" > <TextView android:id= "@+id/tv_send" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:hint= "@string/receive_hint" android:padding= "@dimen/margin_small" android:textColor= "@android:color/black" android:textSize= "@dimen/text_tv" /> </ScrollView> <ScrollView android:id= "@+id/sv_result" android:layout_width= "match_parent" android:layout_height= "match_parent" android:layout_weight= "5" android:background= "@drawable/bg_white_stroke" > <TextView android:id= "@+id/tv_result" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:hint= "@string/receive_hint" android:padding= "@dimen/margin_small" android:textColor= "@android:color/black" android:textSize= "@dimen/text_tv" /> </ScrollView> </LinearLayout> <LinearLayout android:layout_width= "match_parent" android:layout_height= "0dp" android:layout_weight= "0.5" android:gravity= "center" android:orientation= "horizontal" > <Button android:id= "@+id/btn_start" style= "@style/average_vertical_widget" android:text= "@string/start_test" /> <Button android:id= "@+id/btn_close" style= "@style/average_vertical_widget" android:text= "@string/close_port" /> </LinearLayout> </LinearLayout> |
styles.xml
1 2 3 4 5 6 7 8 9 10 11 12 | <resources> <!--font Style--> <style name= "average_vertical_widget" > <item name= "android:layout_width" >0dp</item> <item name= "android:layout_height" >match_parent</item> <item name= "android:layout_weight" > 1 </item> <item name= "android:gravity" >center</item> <item name= "android:textSize" > @dimen /text_size</item> <item name= "android:textColor" > @android :color/black</item> </style> </resources> |
1.2.android_serialport_api 串口工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | package android_serialport_api; import android.util.Log; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class SerialPort { private static final String TAG = "SerialPort" ; /* * Do not remove or rename the field mFd: it is used by native method close(); */ private FileDescriptor mFd; private FileInputStream mFileInputStream; private FileOutputStream mFileOutputStream; public SerialPort(File device, int baudrate, int dataBits, int stopBits, char parity) throws SecurityException, IOException { /* Check access permission */ if (!device.canRead() || !device.canWrite()) { try { /* Missing read/write permission, trying to chmod the file */ Process su; su = Runtime.getRuntime().exec( "/system/bin/su" ); String cmd = "chmod 666 " + device.getAbsolutePath() + "\n" + "exit\n" ; su.getOutputStream().write(cmd.getBytes()); if ((su.waitFor() != 0 ) || !device.canRead() || !device.canWrite()) { throw new SecurityException(); } } catch (Exception e) { e.printStackTrace(); throw new SecurityException(); } } mFd = open(device.getAbsolutePath(), baudrate, dataBits, stopBits, parity); if (mFd == null ) { Log.e(TAG, "native open returns null" ); throw new IOException(); } mFileInputStream = new FileInputStream(mFd); mFileOutputStream = new FileOutputStream(mFd); } // Getters and setters public InputStream getInputStream() { return mFileInputStream; } public OutputStream getOutputStream() { return mFileOutputStream; } public void Write_Rs485_Length( int count) { write_rs485_length(count); } // JNI(调用java本地接口,实现串口的打开和关闭) /**串口有五个重要的参数:串口设备名,波特率,检验位,数据位,停止位 其中检验位一般默认位NONE,数据位一般默认为8,停止位默认为1*/ /** * @param path 串口设备的据对路径 * @param baudrate 波特率 * @param dataBits 数据位 * @param stopBits 停止位 * @param parity 校验位 */ private native static FileDescriptor open(String path, int baudrate, int dataBits, int stopBits, char parity); public native void close(); public native void write_rs485_length( int count); static { //加载jni下的C文件库 Log.i(TAG, "loadLibrary..............gatsby" ); System.loadLibrary( "serial_port" ); } } |
二.qtcreator -> AS
2.1.UartTest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 | package com.lvmi.powerupdate; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import android_serialport_api.SerialPort; public class UartTest { private final Context mContext; byte [] Save_Data; // 保存的载入数据 byte [] Temp_file_Data; // 临时文件数据 byte [] file_arry; // 文件数据 byte [] Send_Data; // 发送数据 byte Data_Number; // 数据编码 int flag_EOT; // 结束命令标记 int file_ChkData; // 文件校验 int NAck_Cnt; // NAck次数 int Ack_Cnt; // Ack次数 private OnSerialListener mOnSerialListener; public String DEV_TTYS4 = "/dev/ttyS4" ; private boolean isOpened = false ; private OutputStream mOutputStream; private InputStream mInputStream; private final ExecutorService mThreadPoolExecutor = Executors.newCachedThreadPool(); public UartTest(Context mContext) { super (); this .mContext = mContext; } public void StartUartTest() { openSerialPort(DEV_TTYS4); Btn_Open_File(); Btn_Sand_Data(); //启动 } //打开文件 public void Btn_Open_File() { int val; file_arry = getBytesByFile(mContext, "QT573-LVMI-APP.bin" ); val = file_arry.length % 128 ; if (val != 0 ) { val = 128 - val; //最后一包数据补24个0 byte [] chrVal2 = new byte [val]; for ( int i = 0 ; i < val; i++) { chrVal2[i] = ( byte ) 0 ; } file_arry = addBytes(file_arry, chrVal2); file_ChkData = Dev_CRC16(file_arry); //must F229 Log.d( "gatsby" , "file_ChkData -> " + Integer.toHexString(file_ChkData)); } } //启动 public void Btn_Sand_Data() { // 清除 计数 接收文本 保存数据 NAck_Cnt = 0 ; Ack_Cnt = 0 ; Save_Data = null ; // 开始接收数据 isOpened = true ; // 开始发送进入编译环境 Data_Number = 0 ; // 重新发送计数 flag_EOT = 0 ; // 发送结束标记位重置 Sand_Data(( byte ) 0x93 ); // 发送 } //发送数据 public void Sand_Data( byte cmd) { //StringBuilder ba = new StringBuilder(); byte [] ba = new byte [ 133 ]; int ChkData; //0xffff byte val; //0xff String sendData = null ; if (cmd == 0x01 ) { byte [] data; ba[ 0 ] = cmd; Log.d( "gatsby" , "Data_Number ->" + Data_Number); val = ( byte ) ~Data_Number; ba[ 1 ] = ( byte ) Data_Number; ba[ 2 ] = ( byte ) val; for ( int i = 0 ; i < Send_Data.length; i++) { ba[i + 3 ] = ( byte ) Send_Data[i]; } //System.arraycopy(Send_Data, 0, data, 0, Send_Data.length); data = Send_Data; Log.d( "gatsby" , "data->" + data.length); // 数据校验 ChkData = CRC16(data, 128 ); Log.d( "gatsby" , "ChkData->" + ChkData); val = ( byte ) (ChkData & 0xff ); ba[Send_Data.length + 3 ] = ( byte ) val; val = ( byte ) ((ChkData >> 8 ) & 0xff ); ba[Send_Data.length + 4 ] = ( byte ) val; //打印看发送数据是否正确 // for (int i = 0; i < ba.length; i++) { // Log.d("gatsby", "i--> " + (i) + " ba ->"+ Integer.toHexString(ba[i] & 0xff)); // } sendSerialPort(ba); if (flag_EOT == 0 ) { Log.d( "gatsby" , "Sand Packet " + Data_Number); sendData = "Send Packet " + Data_Number; } // 发送校验 else if (flag_EOT == 1 ) { Log.d( "gatsby" , "Sand Check" ); sendData = "Send Check" ; } if (mOnSerialListener != null ) { mOnSerialListener.onSendData(sendData); } } else { byte [] ba2 = new byte [ 1 ]; ba2[ 0 ] = ( byte ) cmd; sendSerialPort(ba2); Log.d( "gatsby" , "cmd ->" + Integer.toHexString(cmd & 0xff )); if (cmd == ( byte ) 0x93 ) { Log.d( "gatsby" , "Sand_Data will be send data " ); } else if (cmd == ( byte ) 0x04 ) { Log.d( "gatsby" , "Sand_Data End " ); isOpened = false ; } } } public void openSerialPort(String devType) { if (! "" .equals(devType) && new File(devType).exists()) { Log.d( "gatsby" , "isOpenedSerialPort" ); try { //串口配置:9600 数据位8 起始位1 停止位1 奇校验 SerialPort mSerialPort = new SerialPort( new File(DEV_TTYS4), 9600 , 8 , 1 , 'O' ); mOutputStream = mSerialPort.getOutputStream(); mInputStream = mSerialPort.getInputStream(); mThreadPoolExecutor.execute( new ReceiveDataThread()); isOpened = true ; } catch (IOException e) { e.printStackTrace(); if (mOnSerialListener != null ) { mOnSerialListener.onSerialOpenException(e); } } } } /** * 串口 发送字符串 */ public void Sand_Data_QString(String st) { st += "\r\n" ; byte [] ba = st.getBytes(); sendSerialPort(ba); } /** * 串口 发送byte[] */ public void sendSerialPort( byte [] sendbyte) { if (!isOpened) { return ; } try { mOutputStream.write(sendbyte); mOutputStream.flush(); } catch (IOException e) { e.printStackTrace(); } } //5s 未接收数据 提示升级失败 Android11 提示过时 换Looper写法 Handler handler = new Handler(Looper.myLooper()); private final Runnable runnable = new Runnable() { @Override public void run() { if (Data_Number == 1 ) { if (mOnSerialListener != null ) { mOnSerialListener.onUpdateError(); } } } }; /** * 串口返回数据内容读取 */ private class ReceiveDataThread extends Thread { @Override public void run() { super .run(); int val; //uint8_t String SendStr = null ; String ReciveStr = null ; boolean start_flag = false ; byte [] ba = new byte [ 1024 ]; while (isOpened) { String resSaveData; if (mInputStream != null ) { try { int size = mInputStream.read(ba); if (size > 0 ) { resSaveData = new String(ba, 0 , size); Log.d( "gatsby" , "receiver str = " + resSaveData); Save_Data = resSaveData.getBytes(); if (resSaveData.equals( "Apply OTA\r\n" )) { Log.d( "gatsby" , "Apply OTA" ); SendStr = "OTA" ; start_flag = true ; ReciveStr = "Apply OTA" ; Sand_Data_QString(SendStr); } else if (resSaveData.equals( "Ready\r\n" )) { Log.d( "gatsby" , "Ready" ); SendStr = "Start" ; ReciveStr = "Ready" ; Sand_Data_QString(SendStr); NAck_Cnt = 0 ; Ack_Cnt = 0 ; Data_Number = 0 ; // 重新发送计数 flag_EOT = 0 ; // 发送结束标记位重置 Temp_file_Data = file_arry; } // NACK else if (Save_Data[ 0 ] == 0x15 ) { NAck_Cnt++; if (Data_Number == 0 ) { Data_Number = 1 ; // 首次发送 Log.d( "gatsby" , "(NAck) Ready Receive" ); ReciveStr = "(NAck) Ready Receive" ; Send_Data = cutBytes(Temp_file_Data); Sand_Data(( byte ) 0x01 ); } else if (Data_Number > 0 ) { Log.d( "gatsby" , "(NAck) Ready Packet Error" ); ReciveStr = "(NAck) Ready Receive" ; Send_Data = cutBytes(Temp_file_Data); Sand_Data(( byte ) 0x01 ); } handler.postDelayed(runnable, 6000 ); } // ACK else if (Save_Data[ 0 ] == 0x06 ) { Ack_Cnt++; Data_Number++; //Log.d("gatsby", "(Ack) Receive ->" + Save_Data[0]); //Log.d("gatsby", "flag_EOT->" + flag_EOT); Log.d( "gatsby" , "Temp_file_Data aaa->" + Temp_file_Data.length); if (Temp_file_Data.length > 128 ) { Temp_file_Data = removeBytes(Temp_file_Data, Temp_file_Data.length - 128 ); Log.d( "gatsby" , "Temp_file_Data bbb->" + Temp_file_Data.length); // 发送数据 Send_Data = cutBytes(Temp_file_Data); if (Temp_file_Data.length == 128 ) { Temp_file_Data = removeBytes(Temp_file_Data, 0 ); } Sand_Data(( byte ) 0x01 ); Log.d( "gatsby" , "(Ack) Receive Packet " + (Data_Number - 1 ) + " Success" ); ReciveStr = "(Ack) Receive Packet " + (Data_Number - 1 ) + " Success" ; } else { // 发送校验 if (flag_EOT == 0 ) { flag_EOT = 1 ; val = ( byte ) (file_ChkData & 0xff ); Send_Data[ 0 ] = ( byte ) val; val = ( byte ) ((file_ChkData >> 8 ) & 0xff ); Send_Data[ 1 ] = ( byte ) val; for ( int i = 0 ; i < 126 ; i++) { Send_Data[i + 2 ] = ( byte ) 0 ; } Sand_Data(( byte ) 0x01 ); Log.d( "gatsby" , "EOT (Ack) Receive Packet " + (Data_Number - 1 ) + " Success" ); ReciveStr = "(Ack) Receive Packet " + (Data_Number - 1 ) + " Success" ; } // 发送EOT 完成 else if (flag_EOT == 1 ) { flag_EOT = 2 ; Sand_Data(( byte ) 0x04 ); Log.d( "gatsby" , "(Ack) Receive Check" ); ReciveStr = "(Ack) Receive Check" ; } } } //设置监听 if (mOnSerialListener != null ) { mOnSerialListener.onReceivedData(ReciveStr); if (start_flag) { mOnSerialListener.onSendData(SendStr); if (SendStr.equals( "Start" )) { start_flag = false ; } } } } } catch (Exception e) { e.printStackTrace(); } } } } } /** * 关闭串口 */ public void closeSerialPort() { try { if (mOutputStream != null ) mOutputStream.close(); if (mInputStream != null ) mInputStream.close(); isOpened = false ; } catch (IOException e) { e.printStackTrace(); } } /** * 设置串口监听 * * @param onSerialListener 串口监听 */ public void setOnSerialListener(OnSerialListener onSerialListener) { this .mOnSerialListener = onSerialListener; } /** * 串口监听 */ public interface OnSerialListener { /** * 串口打开异常 */ void onSerialOpenException(Exception e); /** * 串口数据发送 */ void onSendData(String sendData); /** * 串口数据返回 */ void onReceivedData(String receivedData); /** * 上位机下发后若5S未接收MCU数据 上位机退出升级过程 */ void onUpdateError(); } /** * CRC校验 128字节 */ public int CRC16( byte [] u8pArray, int u8Size) { int val; //0xff int i, j; int CurVal; //0xffff int CrcReg = 0xFFFF ; for (i = 0 ; i < u8Size; i++) { //CurVal = u8pArray[i] << 8; uint8_t val = u8pArray[i] & 0xff ; CurVal = val << 8 ; for (j = 0 ; j < 8 ; j++) { if (( short ) (CrcReg ^ CurVal) < 0 ) { CrcReg = (((CrcReg << 1 ) & 0xffff ) ^ 0x1021 ); } else { CrcReg = (CrcReg << 1 ) & 0xffff ; } CurVal <<= 1 ; } } return CrcReg; } /** * CRC校验 */ public int Dev_CRC16( byte [] ba) { int val; //0xff int i, j; int CurVal; //0xffff int CrcReg = 0xFFFF ; for (i = 0 ; i < ba.length; i++) { //uint8_t -> int val = ba[i] & 0xff ; CurVal = val << 8 ; for (j = 0 ; j < 8 ; j++) { if (( short ) (CrcReg ^ CurVal) < 0 ) { CrcReg = (((CrcReg << 1 ) & 0xffff ) ^ 0x1021 ); //Log.d("gatsby","file_arry i-> " + i + " CrcReg -> " + CrcReg); } else { CrcReg = (CrcReg << 1 ) & 0xffff ; } CurVal <<= 1 ; } } return CrcReg; } /** * 两个字节数组合并 */ public byte [] addBytes( byte [] firstData, byte [] secondData) { byte [] mergeData = new byte [firstData.length + secondData.length]; System.arraycopy(firstData, 0 , mergeData, 0 , firstData.length); System.arraycopy(secondData, 0 , mergeData, firstData.length, secondData.length); return mergeData; } /** * 字节数组 截取首128字节 128 */ public byte [] cutBytes( byte [] Data) { byte [] cutData = new byte [ 128 ]; System.arraycopy(Data, 0 , cutData, 0 , cutData.length); return cutData; } /** * 字节数组 移除首128字节 128 */ public byte [] removeBytes( byte [] Data, int removeLength) { byte [] removeData = new byte [removeLength]; System.arraycopy(Data, 128 , removeData, 0 , removeData.length); return removeData; } /** * 获取assets文件流 转byte数组 */ public byte [] getBytesByFile(Context context, String fileName) { try { InputStream inStream = context.getResources().getAssets().open(fileName); ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte [] buffer = new byte [ 1024 ]; int len; while ((len = inStream.read(buffer)) != - 1 ) { outStream.write(buffer, 0 , len); } outStream.close(); inStream.close(); return outStream.toByteArray(); } catch (Exception e) { e.printStackTrace(); return null ; } } } |
MainActivity 回调刷新UI 数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | package com.lvmi.powerupdate; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.ScrollView; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity implements View.OnClickListener, UartTest.OnSerialListener { private ScrollView sv_send,sv_result; private Button btn_start, btn_close; private TextView tv_Send, tv_Result; private UartTest uartTest; @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); } private void initData() { if (uartTest == null ) { uartTest = new UartTest( this ); uartTest.setOnSerialListener( this ); } } private void initView() { sv_send = (ScrollView) findViewById(R.id.sv_send); sv_result = (ScrollView) findViewById(R.id.sv_result); tv_Send = (TextView) findViewById(R.id.tv_send); tv_Result = (TextView) findViewById(R.id.tv_result); btn_start = (Button) findViewById(R.id.btn_start); btn_start.setOnClickListener( this ); btn_close = (Button) findViewById(R.id.btn_close); btn_close.setOnClickListener( this ); } @Override public void onClick(View view) { switch (view.getId()) { case R.id.btn_start: tv_Send.setText( "" ); tv_Result.setText( "" ); btn_start.setEnabled( false ); uartTest.StartUartTest(); break ; case R.id.btn_close: uartTest.closeSerialPort(); btn_start.setEnabled( true ); break ; } } @Override public void onSerialOpenException(Exception e) { showData( "串口打开失败" ,tv_Send); } @Override public void onSendData(String sendData) { showData(sendData, tv_Send); if (sendData.equals( "Send Check" )){ showData( "已成功发送完数据包 " , tv_Send); btn_start.setEnabled( true ); } sv_send.fullScroll(ScrollView.FOCUS_DOWN); } @Override public void onReceivedData(String receivedData) { showData(receivedData, tv_Result); if (receivedData.equals( "(Ack) Receive Check" )){ showData( "已接受完数据,升级成功 " , tv_Result); } sv_result.fullScroll(ScrollView.FOCUS_DOWN); } @Override public void onUpdateError() { showData( "未接收MCU数据,升级失败" , tv_Result); } private void showData( final String str, TextView tv) { runOnUiThread( new Runnable() { @Override public void run() { tv.append(str + "\n" ); } }); } } |
分类:
RockChip
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!