服务器网页版上位机设计 03 上位机 (完结)

本设计主要涉及三个方面: 服务器,网页版,上位机.

书接上回,介绍完网页页面的设计,现在来说说上位机的功能设计.

也就是js文件的内容编写.

1.获取html对象

  • 在网页设计时,设置了4个按钮,现在为它们添加回调函数.

  • 首先需要在js中获取html的元素.有多种方法,我选择类似css类选择器的方法,这是为了和我的css筛选方式相同.

参考: << 【JavaScript】 JS中获取HTML元素值的三种方法_L5Butterfly的博客-CSDN博客_js 获取元素值 >>

  • 1.js文件中添加如下代码:
// 通过选择器获取一个元素
const f1 =  document.querySelector('div.d11 > form');
const f2 =  document.querySelector('div.d12 > div.d21 > form');
const f3 =  document.querySelector('div.d12 > div.d22 > form');
  • 然后就可以通过这3个对象获取元素了.想打印看看是什么内容?可以直接在浏览器控制台中输入代码,这个控制台其实就是js环境,类似python,逐行编译.
  • 输入f1就能看到提示,第一行代表有创建这个变量.第二和第三行代表输入历史值,我之前测试时有输入过,现在快捷显示.然后左边的页面能看到<form>标签元素高亮了,发现并没有包裹所有表单元素,因为我们没有设置大小,也不需要设置.

  • 当你按下回车,会发现打印的就是html内的文本内容(没想到居然就是文本内容),再把鼠标移动到个别元素上,会再次高亮个别元素.

  • 那么怎么方便明了的获取<form>内的元素呢?可以直接通过html中设置的name元素,获取到个别元素.不需要再通过选择器等方法获取了.而且很关键的是,这个name元素是字符串的形式,是支持中文的.用起来也很像访问字典数据.
  • 例如,输入f1["波特率"]就能获取到波特率的元素.非常方便.接下来就可以通过这个方法获取按钮元素,然后添加回调函数了.

2.添加回调函数

  • js中对html进行操作,这一行为有个专业术语:DOM,(Document Object Model) 译为文档对象模型.

参考: << HTML DOM 教程 | 菜鸟教程 (runoob.com) >>

  • 想介绍的东西都有多,不过不跑题了,就说添加回调函数,也就是按下按键时,触发的函数.关键词是onclick .有两种方法,一种是直接为onclick属性赋值,另一个是指定为onclick赋值.(我一开始用后者,回来才知道前者,所以都介绍一下.)

参考: << HTML DOM 事件 | 菜鸟教程 (runoob.com) >>

<< HTML DOM addEventListener() 方法 | 菜鸟教程 (runoob.com) >>

// 直接赋值,简单方便
f1["端口号"].onclick = function(){};
// 指定赋值,灵活
f1["端口号"].addEventListener("click", function(){});
  • 这里选择使用前者,方便快捷.添加代码前,先规划好每个按钮的不同状态,一开始要先将框架定下来,之后再一点点添加内容.这样代码写起来才不会乱.

f1[“开关键”] -> 打开 / 关闭

f2[“按钮”] -> 开始读取 / 停止读取

f3[“按钮”] -> 开始发送 / 停止发送

f1[“端口号”] -> 显示当前端口ID

(本来想做个选择端口的功能的,但是打开端口时就已经包含了选择功能,虽然可以分开,但是因为无法识别端口名字,只能获取到端口id,所以选择端口只能看着一堆数字也分辨不出来.所以干脆就不分开了.然后这个端口按钮就只是用来查看已经连接的端口id)

  • 设计好大概框架后,就可以写代码了,在1.js中继续添加如下代码:
// 按钮的回调函数
f1["端口号"].onclick = function()
{if (f1["开关键"].value == "打开"){alert(`未打开串口`); // 弹窗:提示没有打开串口}else if (f1["开关键"].value == "关闭") {alert(`已打开串口\n${2048}`); // 弹窗:反馈已打开串口的信息}else{new Error(this); // 意料之外,报错}
};
f1["开关键"].onclick = function()
{if (f1["开关键"].value == "打开"){console.log(`开启串口`); // 以下调用 开启串口 函数f1["开关键"].value = "关闭"; // 执行完毕,最后修改按钮文本}else if (f1["开关键"].value == "关闭") {console.log(`关闭串口`); // 以下调用 关闭串口 函数f1["开关键"].value = "打开"; // 执行完毕,最后修改按钮文本}else{new Error(this); // 意料之外,报错}
};
f2["按钮"].onclick = function()
{if (f2["按钮"].value == "开始接收"){console.log(`开始接收`); // 以下调用 开始接收 函数f2["按钮"].value = "停止接收"; // 执行完毕,最后修改按钮文本}else if (f2["按钮"].value == "停止接收") {console.log(`停止接收`); // 以下调用 停止接收 函数f2["按钮"].value = "开始接收"; // 执行完毕,最后修改按钮文本}else{new Error(this); // 意料之外,报错}
};
f3["按钮"].onclick = function()
{if (f3["按钮"].value == "开始发送"){console.log(`开始发送`); // 以下调用 开始发送 函数f3["按钮"].value = "停止发送"; // 执行完毕,最后修改按钮文本}else if (f3["按钮"].value == "停止发送") {console.log(`停止发送`); // 以下调用 停止发送 函数f3["按钮"].value = "开始发送"; // 执行完毕,最后修改按钮文本}else{new Error(this); // 意料之外,报错}
};
  • 上面代码中还用到了new Error(this),这个是反馈行号用的,可以自行在控制台输入查看效果.

参考: << js获取当前代码行号_60rzvvbj的博客-CSDN博客_js获取当前行数 >>

(其实在浏览器中,随便打印输出点东西就可以了,因为控制台会显示执行的行号,还能快捷跳转查看)

3.Web Serial API

  • 一哟一哟,终于开始正片内容了.我们将开始网页访问串口的功能实现.以下内容写在2.js文件中.在上一篇讲网页设计时已经完成脚本导入的步骤了.所以现在2个文件可以互相访问.没错,并不是只有1.js能访问2.js,而是二者可以互相访问,这也是使用requirejs的一大好处.
  • 推荐一系列教程,大部分其实都是源自官方的手册翻译.大同小异.

推荐参考:

<< Web Serial API,web端通过串口与硬件通信 - 掘金 (juejin.cn) >>

<< 什么,网页也能直接与硬件通信?Web Serial API!|8月更文挑战 - 掘金 (juejin.cn) >>

<< Web Serial API - Web APIs | MDN (mozilla.org) >>

<< Web Serial API (wicg.github.io) >>

  • 在开始前还需要先准备一下串口工具,因为手头可能没有能提供串口连接的硬件设备.使用虚拟串口做实验更加方便.使用VSPD创建虚拟串口,然后网页版上位机连接一个,串口调试助手连接一个.

推荐参考:

<< Virtual Serial Port Driver 10 破解版 - 星光的博客 (starxg.com) >> (半天找不到免安装,就安装一下吧)

<< 友善串口调试助手下载与安装 - 知乎 (zhihu.com) >> (找了个可以免安装的)

3.1.异步&延时

  • 这里介绍一个重要概念,异步.为了防止程序堵塞,浏览器的串口模块是需要异步使用的.使用关键词asyncawait ,使用的时候只需要注意两点即可,1)不能用返回值,2)await必须在async函数内使用.

参考: << JavaScript 入门笔记 - 下 - 函数语法_L建豪 忄YH的博客-CSDN博客 >>

  • 在串口通讯时,也很难避免会用到延时等待.在js中很特殊,没有直接的sleep等待函数.只有一个创建异步的setTimeout函数,能创建一个x毫秒后执行的异步函数.不过可以自行嵌套创建.

参考: << javascript里的sleep()方法_clschen的博客-CSDN博客_js sleep() >>

  • 2.js中添加如下内容:
// 延时函数,异步中调用
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
// 调用时要加上 await,代表只能在异步函数 async 中使用.
// await sleep(1000);
  • 如果是需要多段延时等待的话,sleep使用会很方便,如果只是一段的话,还是直接使用setTimeout会更加直接,也不需要使用await关键词.

3.2.打开串口

推荐参考: << Web Serial API (wicg.github.io) >> 4.4 open() method ;

  • 2.js中添加如下内容:
/* 说明: 全局变量
*/
let port_g = null; // 储存端口对象
let reader_g = null; // 储存读对象
let writer_g = null; // 储存写对象/* 说明: 选择端口,并打开
*  参数: 波特率
*/
async function open(baudRate)
{if (!("serial" in navigator)) // 保护措施{console.log(`本浏览器不支持串口功能.`);return; // 跳过剩余内容}console.log(`选择端口`);try{console.log(`选择成功`);port_g = await navigator.serial.requestPort();}catch{console.log(`选择失败`);port_g = null;}if (port_g != null) // 保护措施{console.log(`打开端口`);await port_g.open({baudRate: baudRate,      // 波特率dataBits: 8,              // 数据位stopBits: 1,              // 停止位parity: 'none',             // 奇偶校验bufferSize = 255,           // 缓冲区大小flowControl: 'none'           // 流控模式});}
}
  • 需要注意的有3点: 1)选择端口和打开端口都是带有await关键词的异步函数,所以要使用async函数.2)选择端口时需要使用try关键词,因为如果没有在弹窗中选择端口时,会引发错误.3)open()函数的传参是字典,其中波特率是没有默认值的,其他参数都有默认值.不写也可以.
  • 然后在1.js文件内的按钮回调函数中调用.
...
f1["开关键"].onclick = function()
{if (f1["开关键"].value == "打开"){console.log(`开启串口`); // 以下调用 开启串口 函数open(f1["波特率"].options[f1["波特率"].selectedIndex].value); // 取出波特率,然后打开串口f1["开关键"].value = "关闭"; // 执行完毕,最后修改按钮文本}
...
  • 这个时候发现一个bug,虽然不选择端口后控制台不会报错,但是按钮的文本却发生了改变.如果打开失败,应该是不改变文本.

  • 如果是以前c语言的思维,应该有个返回值,成功返回真,失败返回假.但是因为异步函数的原因,返回值并不好用.结合js的特点,用传入函数的方式解决.
  • 2.js文件修改成如下:
async function open(baudRate, fun)
{...if (port_g != null) // 保护措施{console.log(`打开端口`);await port_g.open({baudRate: baudRate,        // 波特率dataBits: 8,              // 数据位parity: 'none',             // 奇偶校验stopBits: 1,                 // 停止位flowControl: 'none'         // 流控模式});}if (port_g != null) // 保护措施{fun(); // 执行完毕,最后附带执行}
}
  • 1.js文件修改成如下:
...
f1["开关键"].onclick = function()
{if (f1["开关键"].value == "打开"){console.log(`开启串口`); // 以下调用 开启串口 函数open(f1["波特率"].options[f1["波特率"].selectedIndex].value, ()=>{f1["开关键"].value = "关闭"; // 执行完毕,最后修改按钮文本}); // 取出波特率,然后打开串口}
...
  • 这样就完美许多了,而且很灵活,fun()函数的调用甚至可以根据需要而变动.

3.3.端口信息

推荐参考: << Web Serial API (wicg.github.io) >> 4.3 getInfo() method ;

<< JavaScript中判断一个对象是否为一个类的实例_oldjwu的博客-CSDN博客_js 判断是否是实例 >>

  • 2.js中添加如下内容:
/* 说明: 获取端口信息
*  参数: 无
*/
function getInfo(fun)
{if (port_g.constructor == SerialPort) // 保护措施,判断对象是否为类的实例{let usbVendorId = port_g.getInfo()["usbVendorId"]; // 供应商 IDif (usbVendorId == undefined){usbVendorId = "未定义"; // 虚拟串口或部分杂牌usb设备可能没有设置这个参数}let usbProductId = port_g.getInfo()["usbProductId"]; // 产品 IDif (usbProductId == undefined){usbProductId = "未定义";}fun(usbVendorId, usbProductId);}
}
  • 获取usb设备id的主要作用其实是用在打开端口上.目前我的设计是选择授权哪个端口然后就打开哪个端口.官方推荐的设计是,可以授权多个端口,只要网页不关闭授权就有效.然后记录ID,在打开端口时可以通过ID过滤器选择打开哪个端口.(我个人感觉这样好麻烦啊,还是整一对一比较方便,一般人使用应该不会有一拖多的情况吧.)
  • 这2个usb设备id都可以在pc的设备管理器-端口(COM和LPT)中查看得到.不过因为我的是虚拟串口,且我忘记是哪个选项了,所以没找到.如果你有硬件设备的话可以找找.验证验证.

  • 另外注意,getInfo()是不需要await关键字的,所以可以不需要async.如果要加上也可以.这里我还是继续采用fun()传参的方式.
  • 1.js文件修改成如下:(箭头函数真方便)
...
f1["端口号"].onclick = function()
{if (f1["开关键"].value == "打开"){alert(`未打开串口`); // 提示没有打开串口}else if (f1["开关键"].value == "关闭") {getInfo((usbVendorId, usbProductId)=>{alert(`已打开串口:供应商 ID : ${usbVendorId}产品 ID   : ${usbProductId}`); // 反馈已打开串口的信息});}
...

3.4.写数据

推荐参考: << Web Serial API (wicg.github.io) >> 4.6 writable attribute ;

  • 2.js中添加如下内容:
/* 说明: 写数据
*  参数: 数据
*/
async function write(data, fun)
{if (port_g.constructor == SerialPort) // 保护措施{const encoder = new TextEncoder(); // 创建实例writer_g = port_g.writable.getWriter(); // 创建写对象writer_g.write(encoder.encode(data)); // 开始发送// writer_g.close(); // 写终止,停止发送内容,这里用不到,循环发送时才可能用到,后面讲解writer_g.releaseLock(); // 写完毕,允许稍后关闭端口writer_g = null; // 清空变量console.log(data);fun(); // 执行完毕,额外执行}
}
  • 1.js文件修改成如下:
...
f3["按钮"].onclick = function()
{if (f3["按钮"].value == "开始发送"){console.log(`开始发送`); // 以下调用 开始发送 函数f3["按钮"].value = "停止发送"; // 开始执行,修改按钮文本write(f3["发送"].value, ()=>{console.log(`停止发送`);f3["按钮"].value = "开始发送"; // 执行完毕,重置文本});}else if (f3["按钮"].value == "停止发送") {console.log(`停止发送`); // 以下调用 停止发送 函数// 暂不写手动关闭发送的功能,所以不可以手动将按钮从"停止发送"改为"开始发送".// f3["按钮"].value = "开始发送";  // 使用 disabled 属性禁用按钮,或是直接屏蔽这一行,也能起到同样效果}
...
  • 注意,记得调用writer_g.releaseLock(),如果没有调用这个函数,将无法关闭端口.而调用这个之前写任务必须结束,如果没有结束,需要调用writer_g.close()函数来手动结束写任务.不过如果只是发送一段数据,一般不会用得到.应该是在循环的 写数据流 中会用到.大概.
  • 总得来说简单的写数据实现挺容易的,以下是效果图.网页端选择COM1.

3.5.读数据

推荐参考: << Web Serial API (wicg.github.io) >> 4.5 readable attribute

  • 2.js中添加如下内容:
/* 说明: 读数据
*  参数: 最大读取时间
*/
async function read(time, fun)
{let data = []; // 储存接收数据if (port_g.constructor == SerialPort) // 保护措施{if (port_g.readable) // 判断是否可用,只执行一次,不使用 while{reader_g = port_g.readable.getReader(); // 创建读对象setTimeout(()=>{if (reader_g != null){reader_g.cancel(); // 读终止,停止读任务,}},time); // 达到最大读取时间后手动关闭console.log(`读取开始`);try {while (true) {const { value, done } = await reader_g.read(); // 执行读任务if (done) // 读取成功或失败时会返回,不然一直阻塞在死循环{console.log(`读取失败,一般是手动关闭了读任务`);break;}else{for(let i=0; i<value.length; i++) // 连续读取到的数据可能是二维数组或一维数组,解包成一维后再组合数组{data = data.concat(value[i]); // 读取成功,保存读取内容,组合数组}}}}catch (error) {console.log(`读取失败,一般是端口断开连接`);} finally {console.log(`读取结束`);reader_g.releaseLock(); // 允许稍后关闭端口reader_g = null; // 清空变量}}console.log(data);fun(data); // 执行完毕,额外执行}
}
  • 注意注意注意,和写数据类似,读数据,同样需要调用releaseLock(),以便之后能关闭端口.而调用前提是读任务已经结束.和写任务不一样,读任务是在while中一直循环的,因为经常不知道要读取的长度,( 不过在一些情况下是固定长度读取的,比如自定义协议之类的 )为此,必须要手动调用cancel()关闭读任务,使得其能执行break退出while.

注意注意注意,关闭任务使用的是close(),关闭读任务使用的是cancel();

  • 自定义情况下手动关闭读任务,最常见的就是设定时间,超时关闭.这时用到前面所说的setTimeout()函数了.

我之前学习时时一直没找到cancel(),导致我一直无法正常关闭端口,在这个上面栽了大跟头.可能是我教程没看清,所以特意重点说明.

  • 另一点需要注意的,读取到的数据是数组,而且每次读取到的长度不一定,且可能是二维数组,所以遍历一维后使用concat()组合数组的方式保存数据.再另外,读取的内容是字符ASCII格式的十进制形式.

  • 1.js文件修改成如下:

...
f2["按钮"].onclick = function()
{if (f2["按钮"].value == "开始接收"){console.log(`开始接收`); // 以下调用 开始接收 函数f2["按钮"].value = "停止接收"; // 开始执行,修改按钮文本read(3000, (data)=>{f2["接收"].value = data; // 将接收内容显示在多行文本框f2["按钮"].value = "开始接收"; // 执行完毕,最后修改按钮文本});}else if (f2["按钮"].value == "停止接收") {console.log(`停止接收`); // 以下调用 停止接收 函数if (reader_g != null) // 手动关闭{reader_g.cancel(); // 读终止,停止读任务,}// f2["按钮"].value = "开始接收"; // 不需要这里执行,上面执行}
...
  • 现在,点击读取按钮后,就会读取3秒,超时会自动关闭读取,改变文本.也可以提前手动关闭读取.
  • 以下是试验效果,我读取了2次,第一次读取等待超时,第二次读取提前手动关闭,49,50,51是123的ASCII值,13是换行符.

3.6.关闭串口

推荐参考: << Web Serial API (wicg.github.io) >> 4.9 close() method ;

  • 2.js中添加如下内容:
/* 说明: 关闭串口
*  参数: 无
*/
async function close(fun)
{if (port_g.constructor == SerialPort) // 保护措施{if (reader_g != null) // 如果没有清空对象{reader_g.cancel(); // 就关闭任务}if (writer_g != null) // 同上{writer_g.close();}await sleep(200); // 等待一会,确保读写任务关闭,await port_g.close(); // 关闭串口fun(); // 执行完毕}
}
  • 注意,关闭端口之前一定要先关闭读写任务.为了确保异步函数执行结束,就调用延时等待,也是上面介绍过的.起到阻塞性的延时等待.

注意注意注意,关闭任务使用的是close(),关闭读任务使用的是cancel();而关闭串口使用的又是close();逆天,为什么要这样区别命名啊?

  • 1.js文件修改成如下:
...else if (f1["开关键"].value == "关闭") {console.log(`关闭串口`); // 以下调用 关闭串口 函数close(()=>{f1["开关键"].value = "打开"; // 执行完毕,最后修改按钮文本}); // 关闭端口}
...
  • 最后效果如下图,开始接收后就立刻关闭串口,接收理想的提前结束.

4.总结

  • 好了,以上就算是这个系列的全部内容了.从零开始,完整的介绍了一个web上位机小项目.希望能帮到你.

也算是填了一个我以前的坑.之前也写过一个python+qt的上位机,用于图像接受的,参加智能车比赛时用.本来打算也写个教程的.不过后来嫌弃太简单就没写了(就是懒).那个项目唯一折腾我的就是不知道怎么在qt里显示连贯图片,也就是视频.找了许久才找对方向.其他东西都没什么特别的.

  • 现在工作后愈发不想奋斗,只想摸鱼.下班就想玩游戏和睡觉.如果工作中有什么有趣的项目,而且有闲情逸致的话,再记录.

服务器网页版上位机设计 - 03 - 上位机 (完结)相关推荐

  1. 模型机设计(VERILOG)-模型机结构与Verilog语言

    前言 模型机是本学期电子电路课程的综合设计实验作业,主要利用数字电路逻辑部分的知识完成一个能实现多个指令的模型机,使用Verilog语言实现各个部件并完成最终的部件连接及验证.         在实现 ...

  2. r语言 服务器网页版ide RStudio Server 简介

    目录 介绍 安装R 安装RStudio Server 创建账号 开始使用 注意事项 无法打开登录页 多用户使用 服务卡死 端口占用 外网使用 终端工具推荐 RStudio Server是网页版的RSt ...

  3. ftp服务器网页版登陆,Serv-U

    Serv-U是Windows平台和Linux平台的安全FTP服务器(FTPS, SFTP, HTTPS),是一个优秀的.安全的文件管理.文件传输和文件共享的解决方案.同时也是应用最广泛的FTP服务器软 ...

  4. 10款实用高效的网页版PS插件推荐!

    PS是设计界适用范围广.应用次数多的基础性设计工具,PS的功能之强大,常常令新手望而却步.其实,为了对PS的功能进行补充和优化,这些年来陆续开发出了许许多多的功能性PS插件,在网页版 PS --即时设 ...

  5. 基于STM32C8T6、ESP8266-01S、JavaWeb、JSP、Html、JavaScript、Android、服务器和客户端设计、上位机和下位机设计等技术融合的物联网智能监控系统设计与实现

    系列文章目录 第一章ESP8266的java软件仿真测试 第二章ESP8266硬件与软件测试 第三章ESP8266客户端与Java后台服务器联调 第四章ESP8266客户端与JavaWeb服务器联调 ...

  6. ROV采集与通信系统之上位机设计

    前言 时间一晃,我已经是一名即将步入研三的老学长,趁着这个假期抓紧时间把毕业设计的大体框架完成,后续细节的优化工作再慢慢处理.毕设的课题是ROV采集与通信系统,简单来说就是ROV水下实时采集高清图像信 ...

  7. 最简单DIY基于ESP8266的物联网智能小车①(webserver服务器网页简单遥控版)

    ESP8266和ESP32物联网智能小车开发系列文章目录 第一篇:最简单DIY基于ESP8266的物联网智能小车①(webserver服务器网页简单遥控版) 文章目录 ESP8266和ESP32物联网 ...

  8. java web聊天室论文_基于Java网页版聊天室的设计与实现毕业论文含开题报告及文献综述(样例3)...

    <基于Java网页版聊天室的设计与实现毕业论文含开题报告及文献综述.doc>由会员分享,可免费在线阅读全文,更多与<基于Java网页版聊天室的设计与实现毕业论文含开题报告及文献综述& ...

  9. 红警自建服务器,有大神做了个网页版的红警2,方便打工人上班摸鱼

    mumu丨文 前段时间愚人节的时候看到个游戏新闻,说是国外有个叫Chrono Divide的项目,作者用Java重写了红警2的核心程序,复刻了一个红警2页游出来. 是的,你没看错,你可以在网页上与别人 ...

最新文章

  1. Matlab学习笔记——二进制文件的读写
  2. Java中的数据结构
  3. 欲了解Android Studio,必先知道Gradle
  4. 【转】汇编语言学习笔记一:CS和IP寄存器
  5. Java基于socket服务实现UDP协议的方法
  6. 对Faster R-CNN的理解(1)
  7. 使用Fastjson提示No serializer found for class
  8. 运行Java web时遇到的错误
  9. 07-windows下Elasticsearch安装-elasticsearch-service服务
  10. linux go 连接oracle,Ubuntu14下golang连接oracle11g (OCI12.1方式)
  11. 小学计算机兴趣小组计划书,兴趣小组计划
  12. UBS缘何突然抛弃智能投顾?全球财富管理霸主的数字化转型启示(上)
  13. docker 搭建nginx php mysql_docker搭建nginx+mysql+php
  14. vue:监听浏览器地址栏变化
  15. 基于树结构的机器学习模型
  16. 区块链公司依靠电信主网颠覆汇款行业
  17. 23.打印由*号组成的三角形图案
  18. 主存、辅存、内存、外存、存储器:名词解释
  19. 网络游戏引入人工智能:游戏玩家并非真人
  20. Kolmogorov–Smirnov test

热门文章

  1. sha256可以解密?用网上的sha256在线解密平台能解出来吗
  2. Science:内侧前额叶皮层解决利用-探索困境的神经机制
  3. springboot中使用socket对接第三方接口
  4. Fiddler笔记(一)
  5. [ 笔记 ] 计算机网络安全_6_入侵检测系统
  6. 与C语言“无关”的C语言
  7. CentOS7安装SQLServer2017
  8. Capella产品格式与产品定义
  9. [UML] --- 类图
  10. HDOJ 1811 Rank of Tetris