浙江大学淋巴医疗系统架构设计
浙江大学淋巴医疗系统架构设计
FANSEA淋巴测量软件设计
项目介绍
本项目旨在支持浙江大学医学教授在淋巴检测设备上的研发与实现
工期
架构设计
整体采用单体架构,多模块开发;基于ruoyi框架用户模块,系统模块,通用模块以及最核心的诊断模块
诊断模块是项目的核心实现,其又可以分为诊断模块、设备模块、患者信息模块、扫描详情模块
系统设计
设计参考
重点词语:
qualitative part
定性的部分;volume vs. time diagram
体积与时间关系图
1 | 定义java方法监听串口,串口会每20ms发送modbus格式的数据并以\n对每个指令进行分隔,要求对每个完整的指令进行处理计算,当读取到的数据末尾不是\n则留到下次读取,确保发过来的每条指令都能完整收到并处理 |
数据库设计
字典表(ruoyi自带)
系统参数表(ruoyi自带)
医生表(ruoyi自带)
采用若依自带的
user
表,医生表要求登录可以看到自己负责的病人并做诊断,需要包含账号密码用于登录
检查结果表(dgn_result)
字段 | 含义 | 数据类型 | 默认值 | 是否可空 | 约束 |
---|---|---|---|---|---|
id | 诊断结果编号 | bigint(20) | 否 | 主键 | |
patient_code | 病人id | bigint(20) | |||
doctor_id | 负责的医生id | bigint(20) | 否 | ||
diagnostic_status | 诊断状态(对应字典:异常/正常等) | bigint(20) | |||
diagnostic_describe | 诊断描述 | varchar(512) | |||
diagnostic_part | 诊断部位(对应字典:左右臂等) | bigint(20) | |||
figure_id | 扫描图形id | bigint(20) | |||
create_time | 创建时间 | datetime | |||
update_time | 更新时间 | datetime | |||
frequency | 采集次数 | int | |||
create_by | 创建人 | varchar(64) | |||
update_by | 更新人 | varchar(64) | |||
patient_name | 病人name | varchar(10) | |||
device | 设备号 | int |
扫描详情(dgn_detail)
不确定前端数据渲染方案,听甲方需求是后端返回长宽信息交给前端做渲染,可能会使用influxDB/mongoDB,具体方案待明确
字段 | 含义 | ||||
---|---|---|---|---|---|
figure_id | 图形id | ||||
volume | 体积 | ||||
total_length | 总长度 | ||||
img | 采集模型的图像 | ||||
create_time | 创建时间 | datetime | |||
update_time | 更新时间 | datetime | |||
create_by | 创建人 | ||||
update_by | 更新人 |
使用时间序列数据库(如InfluxDB或TimescaleDB)
优点:
- 高性能写入:时间序列数据库特别优化了时间序列数据的写入和查询,非常适合高频率的数据记录。
- 压缩与存储优化:这类数据库通常内置数据压缩机制,可以有效减少存储空间需求。
- 预聚合功能:对于历史数据的统计分析,时间序列数据库能够预先计算聚合结果,加速查询速度。
患者表(patient_info)
字段 | 含义 | 数据类型 | 默认值 | 是否可空 | 约束 |
---|---|---|---|---|---|
id | 诊断结果编号 | bigint(20) | 否 | 主键 | |
patient_code | 病人id | bigint(20) | |||
doctor_id | 负责的医生id | bigint(20) | 否 | ||
diagnostic_status | 诊断状态(对应字典:异常/正常等) | bigint(20) | |||
diagnostic_describe | 诊断描述 | varchar(512) | |||
diagnostic_part | 诊断部位(对应字典:左右臂等) | bigint(20) | |||
figure_id | 扫描图形id | bigint(20) | |||
create_time | 创建时间 | datetime | |||
update_time | 更新时间 | datetime | |||
frequency | 采集次数 | int | |||
create_by | 创建人 | varchar(64) | |||
update_by | 更新人 | varchar(64) | |||
patient_name | 病人name | varchar(10) | |||
device | 设备号 | int |
设备详情 (dgn_devcie)
字段 | 含义 | 数据类型 | 默认值 | 是否可空 | 约束 |
---|---|---|---|---|---|
id | 诊断结果编号 | bigint(20) | 否 | 主键 | |
patient_code | 病人id | bigint(20) | |||
doctor_id | 负责的医生id | bigint(20) | 否 | ||
diagnostic_status | 诊断状态(对应字典:异常/正常等) | bigint(20) | |||
diagnostic_describe | 诊断描述 | varchar(512) | |||
diagnostic_part | 诊断部位(对应字典:左右臂等) | bigint(20) | |||
figure_id | 扫描图形id | bigint(20) | |||
create_time | 创建时间 | datetime | |||
update_time | 更新时间 | datetime | |||
frequency | 采集次数 | int | |||
create_by | 创建人 | varchar(64) | |||
update_by | 更新人 | varchar(64) | |||
patient_name | 病人name | varchar(10) | |||
device | 设备号 | int |
设计模式选择
工厂+策略模式
获取光栅数据有两种方案:上报、轮询。他们分别都有优缺点,适应不同的场景,医生通过选项来指定对应的模式来执行最终的扫描操作
- 上报:数据处理快,但是无法保障数据的完整性,精度一般(适用于简单的形体扫描)
- 轮询:数据处理慢,能保障数据的完整性,能自主设定轮询频率,精度相对就高(适用于更加精细的使用场景)
使用该模式的好处:
- 工厂模式:可以避免if-else,优化代码结构
- 策略模式:用于对应两种数据获取方案
- 定义策略
1 | public interface RasterDataProcessStrategy { |
策略的两个实现:
- 轮询策略
1 |
|
- 上报策略
1 |
|
- 策略工厂类,用于生成策略
1 |
|
- 实际使用
1 | if (serialPort != null){ |
命令模式
- 应用场景:当你的系统需要将请求封装成对象,以便使用不同的请求、队列请求或者记录请求日志时,可以使用命令模式。特别适用于有固定指令集的上下位机通信。
- 实现思路:定义一个命令接口,所有的具体指令类都继承该接口。这样,每个指令都是一个对象,可以被单独处理和执行。这使得添加新的指令变得容易,只需实现命令接口即可。
观察者模式
- 应用场景:当上位机需要监听下位机的状态变化时,可以使用观察者模式。
- 实现思路:下位机作为主题,上位机作为观察者。当下位机状态发生改变时,主动通知所有注册的观察者,这样可以实现松耦合的通信机制。
适配器模式
多个方法解决同一个问题
- 目标接口(
RasterDataListenerAdapter,RasterDataPollAdapter
):需要适配的标准接口。 - 源对象(source):需要被适配的不兼容对象,这里是serialPort的数据。
- 适配器对象(
RasterDataProcessAdapter
):充当中间转换角色,该对象将源对象转换成目标接口。
单例模式
- 应用场景:当需要在整个系统中共享唯一的通信实例时,可以使用单例模式。
- 实现思路:确保一个类只有一个实例,并提供一个全局访问点。
结合以上设计模式,可以构建出既灵活又强大的上下位机通信框架。在实际开发中,根据具体需求选择合适的模式组合,可以使代码结构更加清晰,易于理解和扩展。
技术选型
mybatis-plus
1 | <!-- MyBatis Plus 核心库 --> |
jSerialComm
一个很好的Java串口通信工具
com.fazecast.jSerialComm (jSerialComm 2.11.0 API)
Spring WebSocket
后端传输给前端的长宽数据用于实时展示,其基数之大上报速率之快,http通信每一次响应需要四次招手三次挥手无法满足要求性能要求,故采用全双工协议WebSocket做长宽数据上报
JNA
采用Java调用C语言来计算ModBus指令的CRC16校验码
模块设计
信息录入
预处理
串口参数配置
波特率,串口号,校验位,数据位,停止位
设备参数
采集次数,步进长度,轮询频率,最大运行距离,原点位置
扫描
数据处理
- 数据保存在MySQL中
- 数据实时返回给前端
基于WebSocket,每条数据上报,前端保存在容器,在数据上报完成之后,或者数据达一半时,执行渲染操作
诊断
基础知识
ModBus
学习使用Modbus RTU
模式,首要就是要知道报文格式。其中:
地址位(Address):一个8位的二进制数,用于标识从设备的地址。主设备通过地址位来选择要通信的从设备。
功能码(Function Code):一个8位的二进制数,用于指示主设备要执行的操作类型,如读取数据、写入数据等。
数据位(Data):根据功能码的不同,数据位可以包含读取或写入的数据。
校验位(Checksum):一个16位的循环冗余校验(CRC)码,用于检测数据在传输过程中是否发生错误。
ModBus调试
如果是从机:
- Rx为接收到的主机消息帧
- Tx为发送到的主机数据
RS485
RS485
是一种物理层通信标准,用于在串行通信中传输数据。它定义了电气特性、信号传输方式和连接方式等。RS485通信可以支持多个设备通过同一总线进行通信,其中一个设备作为主设备发送指令,其他设备作为从设备接收指令。
指令发送与响应
寄存器
寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。其实寄存器就是一种常用的时序逻辑电路,但这种时序逻辑电路只包含存储电路。寄存器的存储电路是由锁存器或触发器构成的,因为一个锁存器或触发器能存储1位二进制数,所以由N个锁存器或触发器可以构成N位寄存器。寄存器是中央处理器内的组成部分。寄存器是有限存储容量的高速存储部件,它们可用来暂存指令、数据和位址。
CRC校验
CRC即循环冗余校验码:是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。
若依
新建自己的模块
若依框架-(一)自定义模块添加 - 晨光静默 - 博客园 (cnblogs.com)
- 在父目录下新建新模块
- 在父类项目中添加自己的模块
1 | <!-- 诊断模块--> |
1 | <modules> |
- 在admin模块添加自己的模块,并在自己模块引入common
新建菜单
若依分离版手把手教你新建菜单,配置路由,自动生成前后端代码_若依菜单配置-CSDN博客
集成Mybatis-Plus
若依集成MybatisPlus步骤_若依引入mybatis-plus-CSDN博客
Java
JNA
-CSDN博客 - image.png)
JNA(Java Native Access):提供一组Java工具类用于在运行期间动态访问系统本地库(native library:如Window的dll)而不需要编写任何Native/JNI代码。开发人员只要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射。
java调用c
- 写一个c程序使用MinGw工具打包成dll或so动态链接文件
windows
- gcc -m64 -shared -o
//该指令只适合C语言
linux
- gcc -m64 -fPIC -shared -o
1 | extern "C" { |
- 引入依赖,实现
Library
接口
1 | <dependency> |
1 | public interface Sum extends Library { |
- java调用cpp程序
1 | Sum.INSTANCE.sum(1,2) |
前端
vue项目引入外部js客服页
在public/index.html
中加入
1 | <script |
指令截取
响应:
- 读指令(7字节):返回数据载荷(数据位为1字节)
- 写指令(8字节):返回指令对比是否与发送指令相同来判断是否写操作成功
CompletableFuture
CompletableFuture提交任务whenComplete
难点
实验表明:
对与同一个串口来说,SerialPort无论多个对象只是指定一个串口资源,当被某个SerialPort对象开启占用端口时,其他对象无法开启,除非执行:serialPort.closePort();
这就表明,在连接过期之时,必须执行close关闭资源操作,才能保证下一个连接能进行
使用心跳检测+Redis过期回调技术,保障串口通信资源正常关闭以及刷新设备在线状态
联调问题
java发送串口指令的方案可行性
- 读指令是否成功
- 写指令是否截取数据成功
- 错误的原因