Android 串口通信筆記2 調(diào)試工具分析 工具類實(shí)現(xiàn)分析已添、項(xiàng)目實(shí)現(xiàn)

1.調(diào)試工具ComAssistant 分析

ComAssistant

Android 端調(diào)試工具ComAssistant 如圖妥箕,處于何人之手已不可考,找到的源碼是用eclipse 寫(xiě)的更舞。源碼見(jiàn)文末分享畦幢。
此串口調(diào)試工具,可以同時(shí)對(duì)四個(gè)串口讀寫(xiě)是四個(gè)獨(dú)立的線程缆蝉,選定串口路徑 ,Linux把每個(gè)硬件也看作是一個(gè)文件,所以都是“dev/ttyS1”這種的呛讲。

注意:官方提供的 demo 沒(méi)有N-8-1( N 不奇偶校驗(yàn)位 8 8個(gè)數(shù)據(jù)位 1 1個(gè)停止位)的設(shè)定禾怠。
第一次根據(jù)設(shè)備終端說(shuō)明或者自己嘗試連接電腦打開(kāi)調(diào)試助手 查看到底哪個(gè)口對(duì)應(yīng)哪個(gè)路徑。

2.源碼分析:

Eclipse版本的從哪個(gè)資源網(wǎng)站下載的忘記了贝搁,不過(guò)解壓看是2012年8月的吗氏,所以這里邊的api 適配到10(API等級(jí)10:Android 2.3.3-2.3.7 Gingerbread 姜餅),Eclipse項(xiàng)目結(jié)構(gòu):


Eclipse項(xiàng)目結(jié)構(gòu)

從結(jié)構(gòu)中可以看出來(lái) 是把Android官方提供的android_serial_api 從項(xiàng)目包中獨(dú)立出來(lái)雷逆,此源碼唯一不好的是 GBK 編碼的 導(dǎo)入Android Studio中時(shí) 亂碼 要從新折騰弦讽。

SerialPortFinder與SerialPort分析:


SerialPortFinder

SerialPortFinder就是遍歷獲取設(shè)備上所有devices以及對(duì)應(yīng)的path;

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 flags) 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, flags);
    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;
}

// JNI
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
    System.loadLibrary("serial_port");
}
}

創(chuàng)建了打開(kāi)串口和關(guān)閉串口的本地方法,在jni中實(shí)現(xiàn)膀哲,給Java層調(diào)用往产。
主要是分析 SerialHelp和 Activity的實(shí)現(xiàn)邏輯,SerialHelper代碼:

public abstract class SerialHelper{

private SerialPort mSerialPort;
private OutputStream mOutputStream;
private InputStream mInputStream;
private ReadThread mReadThread;
private SendThread mSendThread;
private String sPort="/dev/s3c2410_serial0";
private int iBaudRate=9600;
private boolean _isOpen=false;
private byte[] _bLoopData=new byte[]{0x30};
private int iDelay=500;
//----------------------------------------------------
public SerialHelper(String sPort,int iBaudRate){
    this.sPort = sPort;
    this.iBaudRate=iBaudRate;
}
public SerialHelper(){
    this("/dev/s3c2410_serial0",9600);
}
public SerialHelper(String sPort){
    this(sPort,9600);
}
public SerialHelper(String sPort,String sBaudRate){
    this(sPort,Integer.parseInt(sBaudRate));
}
//----------------------------------------------------
public void open() throws SecurityException, IOException,InvalidParameterException{
      File device = new File(sPort);
        //檢查訪問(wèn)權(quán)限某宪,如果沒(méi)有讀寫(xiě)權(quán)限仿村,進(jìn)行文件操作,修改文件訪問(wèn)權(quán)限
        if (!device.canRead() || !device.canWrite()) {
            try {
                //通過(guò)掛在到linux的方式兴喂,修改文件的操作權(quán)限
                Process su = Runtime.getRuntime().exec("/system/bin/su");
                //一般的都是/system/bin/su路徑蔼囊,有的也是/system/xbin/su
                String cmd = "chmod 777 " + 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();
            }
        }
    
    
    
    mSerialPort =  new SerialPort(new File(sPort), iBaudRate, 0);
    mOutputStream = mSerialPort.getOutputStream();
    mInputStream = mSerialPort.getInputStream();
    mReadThread = new ReadThread();
    mReadThread.start();
    mSendThread = new SendThread();
    mSendThread.setSuspendFlag();
    mSendThread.start();
    _isOpen=true;
}
//----------------------------------------------------
public void close(){
    if (mReadThread != null)
        mReadThread.interrupt();
    if (mSerialPort != null) {
        mSerialPort.close();
        mSerialPort = null;
    }
    _isOpen=false;
}
//----------------------------------------------------
public void send(byte[] bOutArray){
    try
    {
        mOutputStream.write(bOutArray);
    } catch (IOException e)
    {
        e.printStackTrace();
    }
}
//----------------------------------------------------
public void sendHex(String sHex){
    byte[] bOutArray = MyFunc.HexToByteArr(sHex);
    send(bOutArray);        
}
//----------------------------------------------------
public void sendTxt(String sTxt){
    byte[] bOutArray =sTxt.getBytes();
    send(bOutArray);        
}
//----------------------------------------------------
private class ReadThread extends Thread {
    @Override
    public void run() {
        super.run();
        while(!isInterrupted()) {
            try
            {
                if (mInputStream == null) return;
                byte[] buffer=new byte[512];
                int size = mInputStream.read(buffer);
                if (size > 0){
                    ComBean ComRecData = new ComBean(sPort,buffer,size);
                    onDataReceived(ComRecData);
                }
                try
                {
                    Thread.sleep(50);//延時(shí)50ms
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            } catch (Throwable e)
            {
                e.printStackTrace();
                return;
            }
        }
    }
}
//----------------------------------------------------
private class SendThread extends Thread{
    public boolean suspendFlag = true;// 控制線程的執(zhí)行
    @Override
    public void run() {
        super.run();
        while(!isInterrupted()) {
            synchronized (this)
            {
                while (suspendFlag)
                {
                    try
                    {
                        wait();
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
            }
            send(getbLoopData());
            try
            {
                Thread.sleep(iDelay);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    //線程暫停
    public void setSuspendFlag() {
    this.suspendFlag = true;
    }
    
    //喚醒線程
    public synchronized void setResume() {
    this.suspendFlag = false;
    notify();
    }
}
//----------------------------------------------------
public int getBaudRate()
{
    return iBaudRate;
}
public boolean setBaudRate(int iBaud)
{
    if (_isOpen)
    {
        return false;
    } else
    {
        iBaudRate = iBaud;
        return true;
    }
}
public boolean setBaudRate(String sBaud)
{
    int iBaud = Integer.parseInt(sBaud);
    return setBaudRate(iBaud);
}
//----------------------------------------------------
public String getPort()
{
    return sPort;
}
public boolean setPort(String sPort)
{
    if (_isOpen)
    {
        return false;
    } else
    {
        this.sPort = sPort;
        return true;
    }
}
//----------------------------------------------------
public boolean isOpen()
{
    return _isOpen;
}
//----------------------------------------------------
public byte[] getbLoopData()
{
    return _bLoopData;
}
//----------------------------------------------------
public void setbLoopData(byte[] bLoopData)
{
    this._bLoopData = bLoopData;
}
//----------------------------------------------------
public void setTxtLoopData(String sTxt){
    this._bLoopData = sTxt.getBytes();
}
//----------------------------------------------------
public void setHexLoopData(String sHex){
    this._bLoopData = MyFunc.HexToByteArr(sHex);
}
//----------------------------------------------------
public int getiDelay()
{
    return iDelay;
}
//----------------------------------------------------
public void setiDelay(int iDelay)
{
    this.iDelay = iDelay;
}
//----------------------------------------------------
public void startSend()
{
    if (mSendThread != null)
    {
        mSendThread.setResume();
    }
}
//----------------------------------------------------
public void stopSend()
{
    if (mSendThread != null)
    {
        mSendThread.setSuspendFlag();
    }
}
//----------------------------------------------------
protected abstract void onDataReceived(ComBean ComRecData);
}

除去一些get set方法 ,主要是 構(gòu)造方法 衣迷,打開(kāi)關(guān)閉方法 以及最后一行的abstract 方法onDataReceived()和一個(gè)讀的線程ReadThread 和一個(gè)發(fā)送命令線程SendThread 畏鼓;在ReadThread 在接收或者叫讀線程中 調(diào)用了onDataReceived()方法這樣在用的時(shí)候 可以直接實(shí)現(xiàn)調(diào)用。

SendThread 中 自動(dòng)發(fā) 的原理就是 執(zhí)行while語(yǔ)句發(fā)送命令 線程sleep()來(lái)間隔循環(huán),控制線程暫停和喚起用的是 wait()和notif()壶谒,所以就可以通過(guò)設(shè)定flag實(shí)現(xiàn)自動(dòng)發(fā)送云矫。

wait() 與 notify/notifyAll 方法必須在同步代碼塊(synchronized關(guān)鍵字)中使用.

由于 wait() 與 notify/notifyAll() 是放在同步代碼塊中的,因此線程在執(zhí)行它們時(shí)汗菜,肯定是進(jìn)入了臨界區(qū)中的让禀,即該線程肯定是獲得了鎖的。
當(dāng)線程執(zhí)行wait()時(shí)陨界,會(huì)把當(dāng)前的鎖釋放堆缘,然后讓出CPU,進(jìn)入等待狀態(tài)普碎。
當(dāng)執(zhí)行notify/notifyAll方法時(shí)吼肥,會(huì)喚醒一個(gè)處于等待該 對(duì)象鎖 的線程,然后繼續(xù)往下執(zhí)行麻车,直到執(zhí)行完退出對(duì)象鎖鎖住的區(qū)域(synchronized修飾的代碼塊)后再釋放鎖缀皱。

ReadThread 就簡(jiǎn)單了也是while()代碼塊 定時(shí)sleep循環(huán) 之后 讀到內(nèi)容之后封裝成實(shí)體對(duì)象調(diào)用抽象方法onDataReceived()傳遞到要實(shí)現(xiàn)的地方。

MyFunc是一些數(shù)據(jù)轉(zhuǎn)換的靜態(tài)方法动猬,如圖:


MyFunc

ComAssistantActivity的大致截圖 770行


ComAssistantActivity

ComAssistantActivity中 數(shù)據(jù)比較多啤斗,但是也不難捋順,從左側(cè)概要中可以看出來(lái)主要是一些事件處理和兩個(gè)繼承類:串口控制類SerialControl 繼承SerialHelper和刷新顯示線程DispQueueThread
如圖是Activity onCreate()是實(shí)例化四個(gè)串口控制SerialControl 對(duì)象以及刷新線程并啟動(dòng)赁咙。

image.png
    //----------------------------------------------------串口控制類
private class SerialControl extends SerialHelper{
      

    public SerialControl(){
    }

    @Override
    protected void onDataReceived(final ComBean ComRecData)
    {
        //數(shù)據(jù)接收量大或接收時(shí)彈出軟鍵盤(pán)钮莲,界面會(huì)卡頓,可能和6410的顯示性能有關(guān)
        //直接刷新顯示免钻,接收數(shù)據(jù)量大時(shí),卡頓明顯崔拥,但接收與顯示同步极舔。
        //用線程定時(shí)刷新顯示可以獲得較流暢的顯示效果,但是接收數(shù)據(jù)速度快于顯示速度時(shí)链瓦,顯示會(huì)滯后拆魏。
        //最終效果差不多-_-,線程定時(shí)刷新稍好一些慈俯。
        DispQueue.AddQueue(ComRecData);//線程定時(shí)刷新顯示(推薦)
        
        
        Log.e("TAG", MyFunc.ByteArrToHex(ComRecData.bRec));
        /*
        runOnUiThread(new Runnable()//直接刷新顯示
        {
            public void run()
            {
                DispRecData(ComRecData);
            }
        });*/
    }
}

SerialControl 繼承SerialHelper渤刃,那么它的實(shí)例就可以對(duì)串口進(jìn)行讀寫(xiě)操作 并且 在onDataReceived()中實(shí)現(xiàn)對(duì)接收到的數(shù)據(jù)進(jìn)行處理。即添加到 刷新線程的 數(shù)據(jù)源隊(duì)列中:DispQueue是DispQueueThread 的實(shí)例贴膘。

    //----------------------------------------------------刷新顯示線程
private class DispQueueThread extends Thread{
    private Queue<ComBean> QueueList = new LinkedList<ComBean>(); 
    @Override
    public void run() {
        super.run();
        while(!isInterrupted()) {
            final ComBean ComData;
            while((ComData=QueueList.poll())!=null)
            {
                runOnUiThread(new Runnable()
                {
                    public void run()
                    {
                        DispRecData(ComData);//更新界面
                    }
                });
                try
                {
                    Thread.sleep(100);//顯示性能高的話卖子,可以把此數(shù)值調(diào)小。
                } catch (Exception e)
                {
                    e.printStackTrace();
                }
                break;
            }
        }
    }

    public synchronized void AddQueue(ComBean ComData){
        QueueList.add(ComData);
    }
}

其中QueueList做為接收到的數(shù)據(jù)存放隊(duì)列刑峡,LinkedList是有序的洋闽,為什么AddQueue要同步加鎖呢

public synchronized void AddQueue(ComBean ComData){
    QueueList.add(ComData);
}

因?yàn)長(zhǎng)inkedList是線程不安全的,開(kāi)啟了四個(gè)串口控制對(duì)象如果同時(shí)add()會(huì)拋出ConcurrentModificationException異常氛琢。

while語(yǔ)句執(zhí)行的條件LinkedList.poll()方法的含義:找到并刪除表頭喊递,返回null或隊(duì)列中第一個(gè)對(duì)象随闪,還是用源碼來(lái)分析LinkedList

 public E poll() {
    return size == 0 ? null : removeFirst();
}

 /**
 * Removes the first object from this {@code LinkedList}.
 *
 * @return the removed object.
 * @throws NoSuchElementException
 *             if this {@code LinkedList} is empty.
 */
public E removeFirst() {
    return removeFirstImpl();
}

private E removeFirstImpl() {
    Link<E> first = voidLink.next;
    if (first != voidLink) {
        Link<E> next = first.next;
        voidLink.next = next;
        next.previous = voidLink;
        size--;
        modCount++;
        return first.data;
    }
    throw new NoSuchElementException();
}

3.項(xiàng)目實(shí)現(xiàn)

用該eclipse項(xiàng)目源碼 做嘗試移植了一份Android Studio 3.0 的項(xiàng)目阳似,幾番測(cè)試通過(guò)打的包也能用,同比可以遷移到自己項(xiàng)目铐伴。代碼分享文末撮奏;

Android Studio移植實(shí)現(xiàn)

在main 目錄下創(chuàng)建 jni 和jniLibs ,
0.把原Eclipse項(xiàng)目的android_serialport_api包復(fù)制到在main/java下当宴。
1.把原eclipse中的libs路徑下的三個(gè)平臺(tái)的serial_port.so同目錄復(fù)制到j(luò)niLibs下畜吊。
2.把原eclipse中的c .h 文件復(fù)制到j(luò)ni并重命名為android_serialport_api_SerialPort,或者使用Terminal命令生成C的頭文件自己在把代碼復(fù)制進(jìn)去(注意路徑對(duì)應(yīng)方法名户矢,這個(gè)1應(yīng)該是區(qū)分包名和下劃線:Java_android_1serialport_1api_SerialPort_open)
Terminal命令

①輸入cd app\src\main\java進(jìn)入源碼所在目錄
②輸入javah -jni android_serialport_api.SerialPort生成頭文件
③把生成的android_serialport_api_SerialPort.h復(fù)制到j(luò)ni下邊(沒(méi)有該目錄就右鍵 Moudle玲献,右鍵菜單中選擇 New -> Folder -> JNI Folder)
④右鍵 jni 文件夾,右鍵菜單中選擇New -> C/C++ Source File創(chuàng)建與 .h 文件同名的 .c 文件梯浪。
⑤把原Eclipse 的jni下對(duì)應(yīng)的.c .h文件代碼復(fù)制進(jìn)去

3.在build.gradle 的android節(jié)點(diǎn)中添加

    sourceSets.main {
    jniLibs.srcDir 'src/main/jniLibs'
    jni.srcDirs = []  
}

上圖的右側(cè)標(biāo)紅部分捌年,否則會(huì)提示

 Flag android.useDeprecatedNdk is no longer supported and will be removed in the next version of Android Studio.  Please switch to a supported build system.

這樣就直接可以用原項(xiàng)目編譯好的.so 注意前提是要在在 local.properties 添加 ndk 路徑:

#Sat Jan 20 10:09:24 CST 2018
ndk.dir=F\:\\sdk\\ndk-bundle
sdk.dir=F\:\\sdk

其下目錄有Eclipse 項(xiàng)目源碼和Android Studio 源碼 以及自己使用本機(jī)debug 密鑰打包的 Android調(diào)試工具和 PC 端調(diào)試工具,

github 地址 https://github.com/silencefun/ComTest

百度云鏈接: https://pan.baidu.com/s/1nw37xu5 密碼: qscc

如果覺(jué)得有幫助挂洛,請(qǐng)點(diǎn)個(gè)贊? ★礼预,謝謝。

Android 串口通信開(kāi)發(fā)筆記01

Android 串口通信筆記2 調(diào)試工具分析 工具類實(shí)現(xiàn)分析虏劲、項(xiàng)目實(shí)現(xiàn)
Android 串口通信開(kāi)發(fā)筆記3:CMake 方式實(shí)現(xiàn)和 多對(duì)多的實(shí)現(xiàn)邏輯
Android 串口開(kāi)發(fā) 支持N-8-1(數(shù)據(jù)位停止位校驗(yàn)方式) 設(shè)定

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末托酸,一起剝皮案震驚了整個(gè)濱河市褒颈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌励堡,老刑警劉巖谷丸,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異念秧,居然都是意外死亡淤井,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)摊趾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)币狠,“玉大人,你說(shuō)我怎么就攤上這事砾层′雒啵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵肛炮,是天一觀的道長(zhǎng)止吐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)侨糟,這世上最難降的妖魔是什么碍扔? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮秕重,結(jié)果婚禮上不同,老公的妹妹穿的比我還像新娘。我一直安慰自己溶耘,他們只是感情好二拐,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著凳兵,像睡著了一般百新。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庐扫,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天饭望,我揣著相機(jī)與錄音,去河邊找鬼形庭。 笑死铅辞,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碘勉。 我是一名探鬼主播巷挥,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼验靡!你這毒婦竟也來(lái)了倍宾?” 一聲冷哼從身側(cè)響起雏节,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎高职,沒(méi)想到半個(gè)月后钩乍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怔锌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年寥粹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片埃元。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涝涤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岛杀,到底是詐尸還是另有隱情阔拳,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布类嗤,位于F島的核電站糊肠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏遗锣。R本人自食惡果不足惜货裹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望精偿。 院中可真熱鬧弧圆,春花似錦、人聲如沸还最。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拓轻。三九已至,卻和暖如春经伙,著一層夾襖步出監(jiān)牢的瞬間扶叉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工帕膜, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枣氧,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓垮刹,卻偏偏與公主長(zhǎng)得像达吞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子荒典,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容