浙江大学远程实验平台

浙江大学远程实验平台

基础知识

引脚和串口

引脚和串口在计算机和电子领域中是两个不同的概念,它们各自具有不同的作用和功能。以下是对引脚和串口的详细解释:

一、引脚(Pin)

内部电路信号通路

  1. 定义:引脚,也叫管脚,英文叫Pin。它是从集成电路(芯片)内部电路引出与外围电路的接线,所有的引脚就构成了这块芯片的接口。
  2. 功能:引脚用于连接和传输电信号,实现数据的传输和控制。引脚的功能和用途取决于所连接的设备或电路的设计。
  3. 分类:按照功能,引脚可以分为主电源、外接晶振或振荡器、多功能I/O口,以及控制、选通和复位等类型。例如,在AT89S52芯片中,引脚被划分为多种功能,包括图像识别、自动相位控制、视频信号输出等。
  4. 结构:引脚可划分为脚跟(bottom)、脚趾(toe)、脚侧(side)等部分。通过软钎焊使引脚末端与印制板上的焊盘共同形成焊点。

二、串口(Serial Port)

侧重与外部通信

  1. 定义:串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。它用于在计算机和外部设备之间进行数据传输。
  2. 特点:串口通信线路简单,只需一对传输线即可实现双向通信,从而降低了成本。它特别适用于远距离通信,但传送速度较慢。
  3. 组成:串口通常具有多个引脚,包括数据线(如TX、RX)、控制线(如RTS、CTS)和地线(GND)。这些引脚共同构成了一个完整的串口接口。
  4. 应用:串口可以用于与各种外部设备进行通信,如调制解调器、打印机、传感器等。通过串口,计算机可以与其他设备进行数据传输和控制。

总结来说,引脚是电子设备上的接点,用于连接和传输电信号;而串口是一种用于计算机和外部设备之间进行数据传输的接口。两者在计算机和电子领域中各自发挥着重要作用。

H265视频编码

用于保存视频数据,视频数据大小牵扯到压缩,H265作为一种更高级的视频压缩标准存储视频数据用于网络传输,比H264节省一半的空间,相同宽带下可以传输更加高分辨率的视频,传输后将在流服务器解析成我们所看到的视频,由于其算法相对H264复杂,对硬件要求更高,尽管如此,随着硬件技术的不断进步,HEVC正在变得越来越普及和可行!

H.265,也称为高效视频编码(High Efficiency Video Coding, HEVC),是一种视频压缩标准,其目标是提供比其前身H.264/AVC更高的数据压缩率,同时保持相同或更好的视频质量。HEVC被视为H.264/AVC的继任者,并被广泛用于各种视频传输和存储应用中,如高清电视广播、视频会议、视频流服务(如YouTube和Netflix)以及视频监控系统等。

以下是H.265(HEVC)的一些主要特点:

  1. 更高的压缩效率:与H.264/AVC相比,HEVC在相同的视频质量下可以提供大约50%的更高压缩率,这意味着在相同的带宽下可以传输更高质量的视频,或者在相同的存储空间下可以存储更多的视频内容。
  2. 更好的视频质量:HEVC提供了更先进的编码工具和技术,可以在低比特率下保持更好的视频质量,这对于带宽受限的应用特别有用。
  3. 灵活性和可扩展性:HEVC支持广泛的视频分辨率和帧率,从低分辨率的监控视频到高分辨率的4K和8K电视广播。此外,HEVC还设计用于支持未来的视频格式和技术。
  4. 并行处理能力:HEVC的编码和解码算法经过优化,以利用现代多核处理器和图形处理器的并行处理能力,从而提高性能。
  5. 向后兼容性:虽然HEVC是一个全新的标准,但它也考虑到了与现有系统和设备的兼容性。例如,HEVC编码器可以生成与H.264/AVC兼容的视频流,以便在旧版本的解码器上播放。

然而,需要注意的是,由于HEVC的编码和解码算法比H.264/AVC更复杂,因此它需要更高的计算资源来执行。这意味着在一些资源受限的设备上,使用HEVC可能会导致更高的能耗或更长的处理时间。尽管如此,随着硬件技术的不断进步,HEVC正在变得越来越普及和可行。

流媒体服务器

流媒体服务器是一种将连续的音频和视频信息压缩后放到网络服务器上的系统,使用户能够边下载边观看,而无需等待整个文件下载完毕

流媒体服务器是流媒体应用的核心系统,其关键作用在于提供音视频内容的传输和存储服务。以下是关于流媒体服务器的详细解释:

  1. 定义与原理

    • 流媒体服务器是一种将连续的音频和视频信息压缩后放到网络服务器上的系统,使用户能够边下载边观看,而无需等待整个文件下载完毕。
  2. 主要功能

    • 采集与缓存:流媒体服务器可以从各种视频设备(如摄像机、DVD等)采集视频信号,并将其缓存以供后续处理。
    • 调度与传输:服务器根据用户需求,将音视频内容进行调度和传输,确保用户能够流畅地观看。
    • 播放:流媒体服务器支持多种播放协议(如RTP/RTSP、MMS、RTMP等),以适应不同客户端的播放需求。
  3. 应用场景

    • 视频点播:用户可以根据自己的需求选择并播放音视频文件。
    • 视频会议:支持多人实时音视频通信,广泛应用于企业会议、在线教育等场景。
    • 远程教育:提供音视频内容的在线传输,支持远程教学和学习。
    • 远程医疗:支持医生与患者之间的远程视频诊断和交流。
    • 在线直播:支持各类直播活动,如体育赛事、音乐会、游戏直播等。
  4. 关键技术

    • 音视频压缩技术:通过高效的音视频压缩算法,减少音视频数据的传输带宽和存储空间需求。
    • 流媒体传输协议:支持多种流媒体传输协议,确保音视频内容的实时传输和播放。
    • 加密与安全保护:对音视频数据进行加密处理,保护内容的私密性和版权,防止非法传播和盗用。
  5. 典型产品

    • Windows Media Service(WMS):微软的流媒体服务器产品,采用MMS协议接收、传输视频,并使用Windows Media Player作为前端播放器。
    • Helix Server:RealNetworks公司的流媒体服务器产品,采用RTP/RTSP协议接收、传输视频,并使用Real Player作为播放前端。
    • Adobe Flash Media Server:采用RTMP(RTMPT/RTMPE/RTMPS)协议接收、传输视频,并使用Flash Player作为播放前端。
  6. 未来发展

    • 随着流媒体技术的不断发展,流媒体服务器的功能和性能也在不断提升,为用户提供更多元化的媒体服务。同时,随着5G、AI等技术的融合应用,流媒体服务器将在超高清视频、虚拟现实、增强现实等领域发挥更大作用。

RTMP

RTMP协议从属于应用层,它是流媒体协议,被设计用来在适合的传输协议(如TCP)上复用和打包多媒体传输流(如音频、视频和互动内容)。RTMP提供了一套全双工的可靠的多路复用消息服务,类似于TCP协议[RFC0793],用来在一对结点之间并行传输带时间戳的音频流,视频流,数据流。通常情况下,不同类型的消息会被分配不同的优先级,当网络传输能力受限时,优先级用来控制消息在网络底层的排队顺序。

  • 全双工Full-Duplex):意味着通信双方可以同时进行发送和接收操作。就像打电话一样,你可以一边听对方说话,一边向对方讲话。这种特性使得RTMP非常适合实时应用,如视频会议或在线游戏。
  • 可靠的Reliable):这通常指的是该协议能确保数据从一端到另一端的准确传输。如果数据在传输过程中丢失,协议会重传这些数据以保证其完整性。可靠性是很多实时应用所必需的特性之一。
  • 多路复用Multiplexing):这意味着通过同一个连接可以同时传输多种类型的数据流。例如,在一个RTMP连接中,可以同时传输视频流、音频流以及控制命令等不同类型的信息。这样做的好处是可以减少建立多个单独连接的需求,从而简化网络通信,并提高效率。

TIP: 多路复用原来意思就是同一个连接可以实现多种数据的传输,是所谓多种数据共用一个连接

RTSP

RTSP全称实时流协议(Real Time Streaming Protocol),它是流媒体协议,设计用于娱乐、会议系统中控制流媒体服务器。RTSP用于在希望通讯的两端建立并控制媒体会话(session),客户端通过发出VCR-style命令如play、record和pause等来实时控制媒体流。

RTSP协议以客户服务器方式工作,如:暂停/继续、后退、前进等。它是一个多媒体播放控制协议,用来使用户在播放从因特网下载的实时数据时能够进行控制, 因此 RTSP 又称为“因特网录像机遥控协议”。

ffmpeg

Fast Forward Moving Picture Experts Group

FFmpeg全称为Fast Forward Moving Picture Experts Group,于2000年诞生,是一款免费,开源的音视频编解码工具及开发套件。它的功能强大,用途广泛,大量用于视频网站和商业软件(比如 Youtube 和 iTunes)

核心技术

f59406f8539d490b2f1814cde5a81b6b

image-20240618102620443

心跳包监测

视频展示其实是前端向流服务器发起请求拉取视频流,所以前端需要一直发心跳包询问用户是否在线,当心跳检测用户已下线,或者用户退出页面,前端将停止拉取视频流的操作,也是相当于释放了资源!

前端检测

用于用户在当前页面时和点击退出设备时的监测

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
/* 这个将定时器在每次查询在线设备时触发 */

/* 定义一个定时器,每隔5秒钟,调用一次sendDeviceHeartbeat方法 */
setInterval() {
if (this.currentDeviceInfo.deviceGroup == "") {
clearInterval(this.timer);
} else {
this.timer = setInterval(() => {
this.sendDeviceHeartbeat();
}, 5000);
}

},

/* 发送设备心跳包 */
sendDeviceHeartbeat() {
if (this.currentDeviceInfo.deviceGroup == "") {
clearInterval(this.timer);
}
deviceHeartBeat(this.currentDeviceInfo.deviceGroup).then((response) => {
if (response === 0) {
} else {
this.$message({
message: "设备组已经退出使用",
type: "warning",
});
clearInterval(this.timer);
}
});
},

后端实现

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
/**
* 检查设备组是否还在使用,心跳请求处理接口
* 每10秒检测一次
* 10秒内有心跳请求则刷新缓存中的设备组
* 10秒内没有心跳请求则删除缓存中的设备组
*
* @param id
* @return 0:设备组还在使用 1:设备组已经不在使用
* @throws Exception
*/
@PostMapping(value = "/deviceHeartBeat/{id}")
public String deviceHeartBeat(@PathVariable("id") Integer id) {
if (!redisCache.hasKey(CacheConstants.DEVICE_GROUP_KEY + id)) {
return "1";
} else {
redisCache.expire(CacheConstants.DEVICE_GROUP_KEY + id, 10, TimeUnit.SECONDS);
return "0";
}
}

@GetMapping("/deleteDeviceGroupKey/{id}")
public String deleteDeviceGroupKey(@PathVariable("id") Integer id) {
redisCache.deleteObject(CacheConstants.DEVICE_GROUP_KEY + id);
return redisCache.hasKey(CacheConstants.DEVICE_GROUP_KEY + id);
}

视频的获取流程

image-20240925150308577

流服务器(轮询)

ZLMediaKit/ZLMediaKit: WebRTC/RTSP/RTMP/HTTP/HLS/HTTP-FLV/WebSocket-FLV/HTTP-TS/HTTP-fMP4/WebSocket-TS/WebSocket-fMP4/GB28181/SRT server and client framework based on C++11 (github.com)

快速开始 | ZLMediaKit

视频加载流程

前端页面
1
2
3
4
5
6
7
8
9
<body>
<div class="mainContainer">
<div class="center">
<h1>视频加载中...</h1>
</div>
<video name="videoElement" style='object-fit:fill' id="videoElement" class="centeredVideo" controls muted
autoplay></video>
</div>
</body>
注册摄像头

RESTful 接口 | ZLMediaKit

1
/index/api/addStreamProxy
1
2
3
4
5
secret: Jj9JiJgR82NjRoSRYJQSsGtW2TPBEJrt //连接秘钥
vhost: 192.168.1.11 //流服务器地址
app: live //流应用名
stream: 113 //流Id标识视频
url: rtsp://admin:hzyjy123@192.168.1.170 //摄像头连接信息
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
//将摄像头注册在流服务器,输出视频流
function getStreamUrl() {
$.ajax({
url: '/dev-api/system/config/configKey/camera.request.ip', //获取camera本身的连接信息
beforeSend: function (request) {
request.setRequestHeader("Authorization", "Bearer " + getCookie("Admin-Token"));
},
success: (res) => {
// 生成唯一标识符
const streamId = `${getParams("orderNum")}${getParams("cameraNum")}`;
$.ajax({
url: `http://${res.msg}:800/index/api/addStreamProxy`,
data: {
"secret": `Jj9JiJgR82NjRoSRYJQSsGtW2TPBEJrt`,
"vhost": `${res.msg}`,
"app": "live",
"stream": streamId,
"url": "rtsp://admin:" + `${getParams("password")}@${getParams("ip")}`
},
success: (response) => {
if (response.code != 0) {
console.log(response);
//将失败的信息response.msg放进mainContainer,提示用户
document.querySelector('.center').innerHTML = `<h1>注册流代理失败,请查看流服务器中错误信息,检查摄像头IP及帐号密码是否正确</h1>`;
return;
} else {
//将.center删掉
document.querySelector('.center').style.display = 'none';
console.log(response);
//输出视频流
playStream(res.msg, streamId)
}

}
});
// playStream(res.msg, streamId)
}
});
}
输出视频流
  1. 访问流服务器获取flv视频流
  2. 将flv视频流绑定video标签
  3. 定时器重试播放
1
/live/{streamId}.flv
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
var timerId; // 全局变量,存储定时器ID
/*
输出视频流具体操作,每隔2分钟,重新获取一次视频流,
这样可以确保在视频播放过程中,视频播放器始终处于活跃状态,从而避免因视频播放器故障而导致的视频播放中断而无法再启动。
*/
function playStream(configIP, streamId) {
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: `http://${configIP}:800/live/${streamId}.flv`
});
flvPlayer.attachMediaElement(flvPlayer);
flvPlayer.load();
flvPlayer.play();

// 清理旧的定时器
if (timerId) {
clearInterval(timerId);
}

// 创建新的定时器
timerId = setInterval(() => {
// 停止播放
flvPlayer.pause();
flvPlayer.unload();
flvPlayer.detachMediaElement();
// 重新播放
playStream(configIP, streamId)
}, 120000);
}
}

负载均衡

image-20240619150216225

  1. 项目启动加载正常端口
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
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class RuoYiApplication {
@Autowired
private IRemoteService remoteService;

public static void main(String[] args) {
// System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(RuoYiApplication.class, args);
System.out.println("远程共享实验系统启动成功");

}

@PostConstruct
public void init() {
remoteService.setStreamPortKeys();
}
}
/**
* 将流服务器端口号写入缓存
*/
@Override
public void setStreamPortKeys() {
//streamPort为String类型,用逗号分隔
String[] streamPorts = streamPort.split(",");
//key为STREAM_SERVER_PORT_KEY+streamPort ,value为0
for (String streamPort : streamPorts) {
//先根据PortChecker方法检查端口启用,true为启用,false为未启用
if (PortChecker(Integer.parseInt(streamPort))) {
redisCache.setCacheObject(CacheConstants.STREAM_SERVER_PORT_KEY + streamPort, "0");
} else {
redisCache.deleteObject(CacheConstants.STREAM_SERVER_PORT_KEY + streamPort);
}
}
}

/**
* 端口健康检查
*/
public boolean PortChecker(Integer port) {
String host = "localhost";
try {
Socket socket = new Socket(host, port);
log.info("流服务器端口 " + port + " 已启用");
socket.close();
return true;
} catch (IOException e) {
log.info("流服务器端口 " + port + " 未启用");
return false;
}
}
  1. 查询所有端口连接数并找出最小的
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
/**
* 获取占用最少的streamPort
* 轮询算法,去读取streamPort中的端口号
* 1.先将streamPort作为STREAM_SERVER_PORT_KEY所有的key,value默认为0
* 2.找到缓存中STREAM_SERVER_PORT_KEY的key中value最小的key
* 3.获取最小的key的value
* 4.将最小的key的value+1
* 5.返回最小的key的value
*/
@Override
public String getStreamPortByPolling() {
try {
//找到缓存中STREAM_SERVER_PORT_KEY的key中value最小的key
String minStreamPortKey = getMinStreamPortKey();
//获取最小的key的value
String minStreamPort = redisCache.getCacheObject(CacheConstants.STREAM_SERVER_PORT_KEY + minStreamPortKey);
//将最小的key的value+1
redisCache.setCacheObject(CacheConstants.STREAM_SERVER_PORT_KEY + minStreamPortKey, String.valueOf(Integer.parseInt(minStreamPort) + 1));
log.info("获取占用最少的minStreamPortKey成功: " + minStreamPortKey);
//返回最小的key的value
return minStreamPortKey;
} catch (Exception e) {
log.error("获取占用最少的minStreamPortKey失败", e);
return null;
}
}

/**
* 获取缓存中STREAM_SERVER_PORT_KEY所有的key
*/
public List<String> getStreamPortKeys() {
String[] streamPortKeys = redisCache.keys(CacheConstants.STREAM_SERVER_PORT_KEY + "*").toArray(new String[0]);
List<String> streamPortKeysList = new ArrayList<>();
for (String streamPortKey : streamPortKeys) {
streamPortKeysList.add(streamPortKey.substring(CacheConstants.STREAM_SERVER_PORT_KEY.length()));
}
streamPortKeysList.sort(Comparator.comparingInt(Integer::parseInt));
log.info("获取缓存中STREAM_SERVER_PORT_KEY所有的key: " + streamPortKeysList);
return streamPortKeysList;
}

/**
* 找到缓存中STREAM_SERVER_PORT_KEY的key中value最小的key
*/
public String getMinStreamPortKey() {
List<String> streamPortKeys = getStreamPortKeys();

if (streamPortKeys.isEmpty()) {
log.error("缓存中STREAM_SERVER_PORT_KEY的key为空");
return null;
}

String minStreamPortKey = streamPortKeys.get(0);

for (String streamPortKey : streamPortKeys) {
if (Integer.parseInt(redisCache.getCacheObject(CacheConstants.STREAM_SERVER_PORT_KEY + streamPortKey)) < Integer.parseInt(redisCache.getCacheObject(CacheConstants.STREAM_SERVER_PORT_KEY + minStreamPortKey))) {
minStreamPortKey = streamPortKey;
}
}
log.info("缓存中占用最少的STREAM_SERVER_PORT_KEY的key为: " + minStreamPortKey);
return minStreamPortKey;
}

  1. 注册流代理
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
/**
* 注册流代理
*/
@Override
public AjaxResult registerStreamProxy(String cameraIp, String vhost, String app, String stream, String url) {
log.info("为" + cameraIp + "注册流代理");
// 流代理密钥
String secret = "Jj9JiJgR82NjRoSRYJQSsGtW2TPBEJrt";

// 留代理请求参数,param: secret=Jj9JiJgR82NjRoSRYJQSsGtW2TPBEJrt&vhost=192.168.0.120&app=live&stream=113&url=rtsp://admin:hzyjy123@192.168.0.170
String param = "secret=" + secret + "&vhost=" + vhost + "&app=" + app + "&stream=" + stream + "&url=" + url;

// 判断缓存中是否有该cameraIp的key
if (redisCache.hasKey((CacheConstants.CAMERA_REGISTER_KEY + cameraIp))) {
String result = redisCache.getCacheObject(CacheConstants.CAMERA_REGISTER_KEY + cameraIp);
log.info("缓存中有该cameraIp的keyresult: " + result);
return AjaxResult.success(result);
} else {
log.info("缓存中没有该cameraIp的key,开始注册流代理");
//获取轮询算法得到的streamPort
String streamPort = getStreamPortByPolling();
if (streamPort == null) {
return AjaxResult.error("获取视频流服务器端口失败,请检测配置文件中流服务器端口是否启用");
}
//注册流代理的url,postUrl = "http://192.168.0.120:800/index/api/addStreamProxy";
//将vhost和streamPort拼接到url中http://vhost:streamPort/index/api/addStreamProxy
String postUrl = "http://" + vhost + ":" + streamPort + "/index/api/addStreamProxy";
try {
String response;
//发送post请求,response: { "code" : 0, "data" : { "key" : "192.168.0.120/live/113" }}
try {
response = HttpUtils.sendPost(postUrl, param);
} catch (Exception e) {
log.error("注册流代理失败,请检查参数配置中的IP是否正确");
return AjaxResult.error("注册流代理失败,请检查参数配置中的IP是否正确");
}

// 根据返回的response中的code判断是否注册成功
JSONObject responseJson = JSONObject.parseObject(response);
Integer code = responseJson.getInteger("code");
if (code == -1) {
log.error("注册流代理失败,请查看流服务器中错误信息,检查摄像头IP及帐号密码是否正确");
return AjaxResult.error("注册流代理失败,请查看流服务器中错误信息,检查摄像头IP及帐号密码是否正确");
}

//解析response,获取streamData
String streamData = responseJson.getJSONObject("data").getString("key");

//将streamData拼接成http://vhost:streamPort/live/113
String[] split = streamData.split("/");
String streamUrl = split[0] + ":" + streamPort + "/" + split[1] + "/" + split[2];

//将cameraIp和streamUrl写入缓存
redisCache.setCacheObject(CacheConstants.CAMERA_REGISTER_KEY + cameraIp, streamUrl, 30, TimeUnit.MINUTES);

log.info("注册流代理成功streamUrl: " + streamUrl);
return AjaxResult.success(streamUrl);
} catch (Exception e) {
log.error("注册流代理失败,请检查参数配置中的IP是否正确");
return AjaxResult.error("注册流代理失败,请检查参数配置中的IP是否正确");
}

}
}

flv.js

不依托flash的视频播放js组件,支持H256x

bilibili/flv.js: HTML5 FLV Player (github.com)

远程烧录

image-20240703105627021

业务实现:

1
2
String[] my_args = new String[]{"python", scriptPath, port, message};
Process proc = Runtime.getRuntime().exec(my_args);

Java调用Python代码

Python

1
2
3
4
5
6
7
8
9
10
if __name__ == '__main__':
name = ''
appellation = ''
if len(sys.argv) >1:
# 参数传入
name = sys.argv[1]
appellation = sys.argv[2]
print_hi('PyCharm')
sleep(5)
print('你好 '+name + appellation)
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
# 参数传递的另外一种方式,采取-p形式指定参数
def main():

if sys.platform == "win32":
port = "COM10"
elif sys.platform == "darwin":
port = "/dev/tty.usbserial"
else:
port = "/dev/ttyUSB0"

parser = argparse.ArgumentParser(
description=("Stcflash, a command line programmer for "
+ "STC 8051 microcontroller.\n"
+ "https://github.com/laborer/stcflash"))
parser.add_argument("-b","--bin",
help="code image (bin/hex)",
type=argparse.FileType("rb"), nargs='?')
parser.add_argument("-p", "--port",
help="serial port device (default: %s)" % port,
default=port)
parser.add_argument("-l", "--lowbaud",
help="initial baud rate (default: 2400)",
type=int,
default=2400)
parser.add_argument("-hb", "--highbaud",
help="initial baud rate (default: 115200)",
type=int,
default=115200)
parser.add_argument("-r", "--protocol",
help="protocol to use for programming",
choices=["89", "12c5a", "12c52", "12cx052", "8", "15", "auto"],
default="auto")
parser.add_argument("-a", "--aispbaud",
help="baud rate for AutoISP (default: 4800)",
type=int,
default=4800)
parser.add_argument("-m", "--aispmagic",
help="magic word for AutoISP")
parser.add_argument("-v", "--verbose",
help="be verbose",
default=0,
action="count")
parser.add_argument("-e", "--erase_eeprom",
help=("erase data eeprom during next download"
+"(experimental)"),
action="store_true")
parser.add_argument("-ne", "--not_erase_eeprom",
help=("do not erase data eeprom next download"
+"(experimental)"),
action="store_true")

Java

调用Python安全起见会将py代码拷贝一份再进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 创建临时文件
* 用于存放上传的文件
*/
private File createTempFile(String fileName, String resourcePath) {
File file = new File(System.getProperty("java.io.tmpdir"), fileName);
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath)) {
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
log.error("创建临时文件{}失败", fileName, e);
return null;
}
return file;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws IOException, InterruptedException {
// JRE包含Java类库,Java类加载器和Java虚拟机
// String[] my_args = new String[]{"python", "E:/pythonProject/openCV/test/信息录入.py"}; python + .py路径 + 参数
String[] my_args = new String[]{"python", "E:/pythonProject/main.py","张三","老头"};
// 对应上面第一种参数传递方式,如同在命令行执行:python mian.py 张三 老头
Process proc = Runtime.getRuntime().exec(my_args);
String line;
StringBuilder lines = new StringBuilder();
// 创建一个缓冲区用于缓存输入的字符流,可以方便、高效读取文本
BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream(), "GBK"));
while ((line = reader.readLine()) != null) {
lines.append(line).append("\n");
}
reader.close();
proc.waitFor();
System.out.println(lines.toString());
}

输出

1
2
Hi, PyCharm
你好 张三老头
1
2
// 对应上述第二种传参方式
Process pro = RuntimeUtil.exec("python ", scriptPath1, "--protocol=" + protocol, "--port=" + port51, "--bin=" + bin);

Java调用bin文件

1
2
3
4
5
6
7
8
// hex2binPath bin文件path,hexFilePath是hex文件(bin文件执行参数)
ProcessBuilder processBuilder = new ProcessBuilder(hex2binPath, hexFilePath);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
log.info("hexToBin转化失败,无进程等待,请联系管理员查看异常日志");
return null;
}

Java监听串口

image-20240704164405183

打开串口

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
serialPort = SerialPortTool.openComPort(portName, baudRate, 8, 1, 0);

if (serialPort != null) {
// 创建新线程来处理串口的打开操作
SerialPort finalSerialPort = serialPort;

Thread thread = new Thread(() -> {
try {
MySerialPortEventListener listener = new MySerialPortEventListener(finalSerialPort, 1);
listenerMap.put(portName, listener);
log.info("添加串口监听器成功");
} catch (Exception e) {
listenerMap.remove(portName);
log.error("添加串口监听器失败", e);
}
});
thread.start();

// openComPort方法实现
log.info("开始打开串口: portName = " + portName + ",baudrate = " + b + ",datebits = " + d + ",stopbits = " + s + ",parity = " + p);

// 将获取端口标识符的步骤放入try-catch块中
CommPortIdentifier portIdentifier;
// 通过端口名称识别指定 COM 端口
try {
portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
} catch (NoSuchPortException e) {
log.error("没有找到串口:" + portName);
return null;
}
/**
* open(String TheOwner, int i):打开端口
* TheOwner 自定义一个端口名称,随便自定义即可
* i:打开的端口的超时时间,单位毫秒,超时则抛出异常:PortInUseException if in use.
* 如果此时串口已经被占用,则抛出异常:gnu.io.PortInUseException: Unknown Application
*/
commPort = portIdentifier.open(portName, 5000);
/**
* 判断端口是不是串口
* public abstract class SerialPort extends CommPort
*/
if (commPort instanceof SerialPort) {
SerialPort serialPort = (SerialPort) commPort;
/**
* 设置串口参数:setSerialPortParams( int b, int d, int s, int p )
* b:波特率(baudrate)
* d:数据位(datebits),SerialPort 支持 5,6,7,8
* s:停止位(stopbits),SerialPort 支持 1,2,3
* p:校验位 (parity),SerialPort 支持 0,1,2,3,4
* 如果参数设置错误,则抛出异常:gnu.io.UnsupportedCommOperationException: Invalid Parameter
* 此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用
*/
serialPort.setSerialPortParams(b, d, s, p);
log.info("打开串口 " + portName + " 成功");
return serialPort;
}

注册串口监听

实现SerialPortEventListener类,重写监听方法

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
public class MySerialPortEventListener implements SerialPortEventListener {

private String latestData = "";

// 事件源
private SerialPort serialPort;

private int eventType;


/**
* 构造串口监听器
*
* @param serialPort
* @param eventType
* @throws TooManyListenersException
*/
public MySerialPortEventListener(SerialPort serialPort, int eventType) throws TooManyListenersException {
this.serialPort = serialPort;
this.eventType = eventType;
//注册监听
serialPort.addEventListener(this);
//串口有数据监听
serialPort.notifyOnDataAvailable(true);
//中断事件监听
serialPort.notifyOnBreakInterrupt(true);
}

/**
* 串口监听事件
*
* @param arg0
*/

@Override
public void serialEvent(SerialPortEvent arg0) {
// 代表监听可用数据
if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
// 数据通知
byte[] bytes = SerialPortTool.getDataFromPort(serialPort);
String str = new String(bytes);
latestData = str;
eventType = arg0.getEventType(); // 将事件类型赋值给eventType
log.info("收到的数据长度:" + bytes.length);
log.info("收到的数据:" + str);
}
}


}


// getDataFromPort具体实现
inputStream = serialPort.getInputStream();

// 等待数据接收完成
Thread.sleep(500);

// 获取可读取的字节数
int availableBytes = inputStream.available();
if (availableBytes > 0) {
data = new byte[availableBytes];
int readBytes = inputStream.read(data);
log.info("从串口 " + serialPort.getName() + " 接收到数据:" + Arrays.toString(data) + " 完成");
} else {
log.info("从串口 " + serialPort.getName() + " 接收到空数据");
}

SerialPortListener事件类型详解

在SerialPortEvent中,这些参数代表了串行端口的不同事件。它们的意义如下:

  1. DATA_AVAILABLE:数据可用。当串行端口接收到数据时,这个事件将被触发。
  2. OUTPUT_BUFFER_EMPTY:输出缓冲区空。当串行端口的输出缓冲区为空时,这个事件将被触发。
  3. CTS:清除传输请求。当串行端口的清除传输请求(CTS)信号变为低电平(0)时,这个事件将被触发。
  4. DSR:数据服务请求。当串行端口的数据服务请求(DSR)信号变为低电平(0)时,这个事件将被触发。
  5. RI:远程中断请求。当串行端口的远程中断请求(RI)信号变为低电平(0)时,这个事件将被触发。
  6. CD:数据位中断。当串行端口的数据位中断(CD)信号变为低电平(0)时,这个事件将被触发。
  7. OE:溢出错误。当串行端口的溢出错误(OE)信号变为低电平(0)时,这个事件将被触发。
  8. PE:Parity错误。当串行端口的Parity错误(PE)信号变为低电平(0)时,这个事件将被触发。
  9. FE:帧错误。当串行端口的帧错误(FE)信号变为低电平(0)时,这个事件将被触发。
  10. BI:Break中断。当串行端口的Break中断(BI)信号变为低电平(0)时,这个事件将被触发。

前端知识

html内嵌页面

iframe(内嵌框架)是 HTML 中一种用于将一个网页嵌入到另一个网页中的标签,它可以在一个页面中显示来自其他页面的内容。在网页中,使用