实训台接线:串口要选对。接线做成数据(BoardState.wiring),默认按教材布局图 COM2 ADAM-4017+、COM3 ADAM-4150、COM4 ZigBee 协调器、COM5 LED 屏,实验可在 meta.json 的 wiring 里改:项目 7 的书上代码在一个串口上打开两个模块,这三个实验把 4150 并到 COM2 的 485 总线上。选错串口时返回值照反编译的真机版 dll(4017 全 null、4150 全 false,4150 的 OnOff 和 LED 推送返回成功但设备不动作,ZigBee 返回 false);串口没打开时照教材仿真版。Open / Close / 没打开就读写报告给服务器,实训台最上方用红字提示("程序在 COM2 上打开 ADAM-4150,它接在 COM3""ADAM-4150 的串口没有打开,程序却在读写它"),评分不通过时也附上。只读接线展示:鼠标移到实训台设备上,标题栏写出线路,对应通道、模块、串口高亮;串口服务器按接线列出设备,程序打开的口亮绿点;程序里串口下拉框的灰字按当前接线。打开实验时浏览器发 experiment 消息换接线,老师看学生提交时按作业的实验接线(提交接口返回 sampleId)。"4017"统一写成 ADAM-4017+
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

上课前逐个检查 23 个实验后的修改:补上三处评分漏洞(项目 3 任务 1、项目 4 任务 3 的关闭和项目 7 任务 2 的熄灯,程序什么都不做也能通过);评分说明的空格;任务说明按实测补充(项目 2 任务 1–3、项目 3 任务 2、项目 4 任务 2、项目 7 任务 1–3、项目 8 任务 5)。使用说明、设计方案同步,设计方案记下以后可能扩展"物联网设备安装与调试"课

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-28 17:40:57 +08:00
1 parent 163e553de4
commit 46e0f40cce
35 files changed
+637 -98

No files matched your search

+18 -2
View File
@@ -106,7 +106,20 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
<h3>5. 虚拟实训台</h3>
<ul>
<li>运行区下面是虚拟实训台,代替书上的实物设备。程序打开的串口:COM2 → ADAM-4017+,COM3 → ADAM-4150,COM4 → ZigBee 协调器,COM5 → LED 屏。程序里的串口下拉框展开时,每个串口后面也会用灰字标出接的设备。</li>
<li>
运行区下面是虚拟实训台,代替书上的实物设备。设备接在串口服务器上:COM2 → ADAM-4017+,COM3 → ADAM-4150,COM4 → ZigBee 协调器,COM5 → LED 屏(项目 7
的 ADAM-4150 和 ADAM-4017+ 一起接在 COM2
上,书上的代码就是在一个串口上打开两个模块)。程序里的串口下拉框展开时,每个串口后面也会用灰字标出接的设备。
</li>
<li>
<b>串口要选对</b
>:和真机一样,在没接这台设备的串口上打开它不会报错,只是设备没有反应(读到的数据是空的、灯不亮)。忘了点“打开”就读数据、开灯也一样。这时实训台最上方会用红字提示,比如“程序在
COM2 上打开 ADAM-4150,它接在 COM3”。
</li>
<li>
鼠标移到实训台的设备上,标题栏会写出它的接线,比如“人体红外 → ADAM-4150 DI0 → 串口服务器
COM3”,接的通道和串口也会高亮。串口前面的绿点表示程序打开了这个串口。
</li>
<li>拖动滑块(或点一下滑块再按方向键)调温度、湿度、光照;点人体红外、烟雾、火焰、红外对射切换有无。程序控制的灯、警示灯、LED 屏会跟着变化。</li>
<li>
两盏照明灯:<b>照明灯 1</b> 接 ZigBee 继电器节点 1(项目 3 的路灯),<b>照明灯 2</b> 接 ADAM-4150 的 DO1(项目 7 的教室、寝室、会议室);警示灯接
@@ -142,7 +155,10 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
<h3>7. 数据库(项目 6、8)</h3>
<ul>
<li>点右上角的<b>数据库</b>(在新标签页打开),代替书上的 SSMS:左边点“+ 新建库”“+ 新建表”,点表名编辑表里的数据,表右边的 ⋯(或右键)里能查询、设计表结构、删除;<b>新建查询</b>写 SQL(输入时会提示关键字、表名和列名,写"表名."或"别名."列出这张表的列),按 F5 执行。</li>
<li>
点右上角的<b>数据库</b>(在新标签页打开),代替书上的 SSMS:左边点“+ 新建库”“+ 新建表”,点表名编辑表里的数据,表右边的
⋯(或右键)里能查询、设计表结构、删除;<b>新建查询</b>写 SQL(输入时会提示关键字、表名和列名,写"表名."或"别名."列出这张表的列),按 F5 执行。
</li>
<li>每个人有自己的一套数据库,只有自己能看到,一直保留。程序里的连接字符串只看数据库名,服务器名、账号密码照书上写就行。</li>
<li>数据弄乱了:点<b>恢复实验初始数据</b>,选实验(默认是刚才做的实验),把这个实验用到的库换回初始数据(其他库不动)。</li>
<li>点<b>← 返回实验</b>回到刚才做的实验。</li>
+7 -3
View File
@@ -222,7 +222,7 @@ async function loadAssignment(discardWork = false) {
}
// ---------- 老师运行学生提交的程序 ----------
const viewing = ref<{ studentName: string; username: string; assignmentTitle: string } | null>(null)
const viewing = ref<{ studentName: string; username: string; assignmentTitle: string; sampleId: string } | null>(null)
// 第一次打开一个实验(或作业)时自动打开任务面板;记在浏览器里,之后不再自动打开
const TASK_SEEN_KEY = 'iot-sim:task-seen'
@@ -243,7 +243,7 @@ watch(
async function loadSubmission() {
try {
const result = await api<{ files: SourceFile[]; studentName: string; username: string; assignmentTitle: string }>(
const result = await api<{ files: SourceFile[]; studentName: string; username: string; assignmentTitle: string; sampleId: string }>(
'GET',
`/api/teacher/submissions/${props.submissionId}`,
)
@@ -513,10 +513,14 @@ function start() {
rerunBlocked.value = false
session.run(
files.value.map((f) => ({ ...f })),
props.assignmentId ? assignment.value?.sampleId : sampleId.value,
experimentId.value,
)
}
/** 当前实验:做作业、看学生提交时是作业基于的实验。实训台按它接线(项目 7 的 4150 并在 COM2 上) */
const experimentId = computed(() => (props.submissionId ? viewing.value?.sampleId : props.assignmentId ? assignment.value?.sampleId : sampleId.value))
watch(experimentId, (id) => session.setExperiment(id), { immediate: true })
// ---------- 运行中改了代码或窗体:自动重新运行,不用再按 F5 ----------
/** 停止修改多久后重新运行 */
const RERUN_DELAY_MS = 1500
+5
View File
@@ -7,6 +7,7 @@ import TrainingBoard from '../scene/TrainingBoard.vue'
import type { SizeValue } from '../winforms/format'
import FormWindow, { WINDOW_MARGIN, WINDOW_STEP } from '../winforms/FormWindow.vue'
import { RUN_IMAGES } from '../winforms/images'
import { PORT_LABELS, portLabels } from '../scene/wiring'
import MessageBoxDialog from '../winforms/MessageBoxDialog.vue'
import type { RunSession } from './session'
@@ -17,6 +18,10 @@ const emit = defineEmits<{
const state = props.session.state
provide(RUN_IMAGES, props.session.images)
provide(
PORT_LABELS,
computed(() => portLabels(props.session.state.board?.wiring)),
)
// 素材库清单:认出素材图片,在高分辨率屏幕上换成两倍图
void loadGallery()
+12 -1
View File
@@ -61,6 +61,8 @@ export function createRunSession() {
/** 离开页面时主动断开,不再重连 */
let closed = false
let reconnectTimer: ReturnType<typeof setTimeout> | undefined
/** 当前实验(服务器按它给实训台接线);重连后重新告诉服务器 */
let experiment: string | undefined
/** 连续重连失败的次数,等待时间随之加长(最长 RECONNECT_MAX_MS) */
let reconnectFailures = 0
const RECONNECT_MAX_MS = 10_000
@@ -105,6 +107,7 @@ export function createRunSession() {
ws.onopen = () => {
state.connection = state.connection === 'lost' ? 'restored' : 'open'
reconnectFailures = 0
send({ type: 'experiment', sampleId: experiment })
resolve()
}
ws.onclose = () => {
@@ -155,6 +158,14 @@ export function createRunSession() {
send({ type: 'run', files, sampleId })
}
/** 打开了哪个实验:实训台换成它的接线 */
function setExperiment(sampleId: string | undefined) {
if (sampleId === experiment) return
experiment = sampleId
// 还在连接时不能发,连上后 onopen 会发
if (socket?.readyState === WebSocket.OPEN) send({ type: 'experiment', sampleId })
}
/** 调节实训台环境(滑块、传感器开关) */
function setBoard(input: Omit<Extract<ClientMessage, { type: 'boardInput' }>, 'type'>) {
send({ type: 'boardInput', ...input })
@@ -292,7 +303,7 @@ export function createRunSession() {
socket?.close()
}
return { state, images, connect, run, stop, act, replyMessageBox, setBoard, resetBoard, close }
return { state, images, connect, run, stop, act, replyMessageBox, setExperiment, setBoard, resetBoard, close }
}
export type RunSession = ReturnType<typeof createRunSession>
+192 -20
View File
@@ -2,7 +2,21 @@
// 虚拟实训台:按教材实训台布局图(顶棚 → LED 屏 → 传感器 → 采集模块)排列。
// 传感器卡片上的滑块和开关调节环境(经服务器推给学生程序),灯和继电器的状态由学生程序控制。
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { ANALOG_INFO, clockWall, type AnalogKey, type BoardState, type ClientMessage, type DigitalKey } from '@iot/shared'
import {
ANALOG_INFO,
BOARD_PORTS,
clockWall,
DEFAULT_WIRING,
describePortProblem,
PORT_DEVICE_NAMES,
portProblems,
type AnalogKey,
type BoardState,
type ClientMessage,
type DigitalKey,
type PortDevice,
} from '@iot/shared'
import { devicesOn } from './wiring'
type BoardInput = Omit<Extract<ClientMessage, { type: 'boardInput' }>, 'type'>
@@ -145,6 +159,57 @@ function setSpeed(speed: number) {
emit('set', { clock: { speed } })
}
// ---------- 接线:鼠标移到设备上,高亮它接的通道、模块和串口,并写出整条线路(只看,不能改) ----------
interface Link {
label: string
module: PortDevice | null
/** 接在模块的哪个通道上 */
channels?: string[]
/** 没有模块通道时的线路说明 */
route?: string
}
const LINKS: Record<string, Link> = {
infrared: { label: '红外对射', module: 'adam4150', channels: ['DI3'] },
alarm: { label: '警示灯', module: 'adam4150', channels: ['DO0'] },
camera: { label: '网络摄像头', module: null, route: '网线 → 路由器(按 IP 地址访问,不经过串口)' },
led: { label: 'LED 显示屏', module: 'led' },
streetLamp: { label: '照明灯 1', module: 'zigbee', route: 'ZigBee 继电器节点 1 → 无线 → ZigBee 协调器' },
bodyInfrared: { label: '人体红外', module: 'adam4150', channels: ['DI0'] },
humiture: { label: '温湿度传感器', module: 'adam4017', channels: ['Vin0', 'Vin1'] },
light: { label: '光照传感器', module: 'adam4017', channels: ['Vin2'] },
speaker: { label: '广播', module: null, route: '电脑的声卡(Windows Media Player 播放)' },
smoke: { label: '烟雾传感器', module: 'adam4150', channels: ['DI2'] },
fire: { label: '火焰传感器', module: 'adam4150', channels: ['DI1'] },
roomLamp: { label: '照明灯 2', module: 'adam4150', channels: ['DO1'] },
}
const hovered = ref<string | null>(null)
const wiring = computed(() => props.board?.wiring ?? DEFAULT_WIRING)
const link = computed(() => (hovered.value ? LINKS[hovered.value] : undefined))
/** 如"人体红外 → ADAM-4150 DI0 → 串口服务器 COM3" */
const linkPath = computed(() => {
const l = link.value
if (!l) return ''
const steps = [l.label]
if (l.route) steps.push(l.route)
if (l.module && l.channels) steps.push(`${PORT_DEVICE_NAMES[l.module]} ${l.channels.join('、')}`)
if (l.module) steps.push(`串口服务器 ${wiring.value[l.module]}`)
return steps.join(' → ')
})
const linkedChannel = (module: PortDevice, channel: string) => link.value?.module === module && link.value.channels?.includes(channel) === true
const linkedModule = (module: PortDevice) => link.value?.module === module
const linkedPort = (port: string) => !!link.value?.module && wiring.value[link.value.module] === port
/** 学生程序打开了这个串口(不管上面接的是不是它要的设备) */
const portOpen = (port: string) => Object.values(props.board?.opened ?? {}).some((opened) => opened?.trim().toUpperCase() === port)
const portDevices = (port: string) =>
devicesOn(wiring.value, port)
.map((device) => PORT_DEVICE_NAMES[device])
.join('、') || '空'
/** 串口打开错了、或没打开就读写的设备:设备没有反应,提示学生 */
const problems = computed(() => (props.board ? portProblems(props.board) : []))
const cameraTitle = computed(() =>
camera.value.on ? `网络摄像头:正在传送画面(云台左右 ${camera.value.x + 1}/7,上下 ${camera.value.y + 1}/3)` : '网络摄像头(项目 4 使用):未开启',
)
@@ -175,6 +240,7 @@ const cameraTitle = computed(() =>
</button>
<span v-if="ledText" class="summary-led" :title="ledText">LED:{{ ledText }}</span>
<span v-if="clock" class="summary-clock" title="虚拟时钟:学生程序里的 DateTime.Now">🕒 {{ wall.slice(11) }}({{ clock.speed }}×)</span>
<span v-for="p in problems" :key="p.device" class="port-warning">⚠ {{ describePortProblem(p) }}</span>
<button class="link expand" @click="setCollapsed(false)">展开 ▾</button>
</div>
@@ -186,39 +252,72 @@ const cameraTitle = computed(() =>
>
<div class="board-header">
<span class="board-name">虚拟实训台</span>
<span class="wiring-path" :class="{ active: link }" :title="linkPath">{{ linkPath || '鼠标移到设备上,看它接在哪个通道、哪个串口' }}</span>
<button class="link" title="收起后只显示一行状态,给窗体留出更多地方" @click="setCollapsed(true)">收起 ▴</button>
</div>
<!-- 串口问题放在最上面,不用往下滚就能看到 -->
<div v-if="problems.length" class="port-warnings">
<div v-for="p in problems" :key="p.device" class="port-warning">⚠ {{ describePortProblem(p) }},设备收不到命令</div>
</div>
<!-- 顶棚:红外对射、警示灯(DO0)、摄像头 -->
<div class="ceiling">
<button class="device beam-end" :class="{ active: digital.infrared }" title="点击遮挡/恢复光束" @click="toggle('infrared')">红外对射</button>
<button
class="device beam-end"
:class="{ active: digital.infrared }"
title="点击遮挡/恢复光束"
@click="toggle('infrared')"
@mouseenter="hovered = 'infrared'"
@mouseleave="hovered = null"
>
红外对射
</button>
<div class="beam" :class="{ broken: digital.infrared }" @click="toggle('infrared')">
<span v-if="digital.infrared" class="beam-tag">有人闯入</span>
</div>
<div class="device warning-light" :class="{ on: alarmOn }" title="警示灯(ADAM-4150 DO0)">警示灯</div>
<div class="device warning-light" :class="{ on: alarmOn }" title="警示灯(ADAM-4150 DO0)" @mouseenter="hovered = 'alarm'" @mouseleave="hovered = null">
警示灯
</div>
<div class="beam" :class="{ broken: digital.infrared }" @click="toggle('infrared')" />
<div class="device camera" :class="{ on: camera.on }" :title="cameraTitle">
<div class="device camera" :class="{ on: camera.on }" :title="cameraTitle" @mouseenter="hovered = 'camera'" @mouseleave="hovered = null">
<svg viewBox="0 0 24 24" width="22" height="22">
<path fill="currentColor" d="M4 7h11a2 2 0 0 1 2 2v1.5l3.5-2.5v8L17 13.5V15a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2z" />
</svg>
</div>
<button class="device beam-end" :class="{ active: digital.infrared }" title="点击遮挡/恢复光束" @click="toggle('infrared')">红外对射</button>
<button
class="device beam-end"
:class="{ active: digital.infrared }"
title="点击遮挡/恢复光束"
@click="toggle('infrared')"
@mouseenter="hovered = 'infrared'"
@mouseleave="hovered = null"
>
红外对射
</button>
</div>
<!-- LED 显示屏 -->
<div class="led-screen" title="LED 显示屏">
<div class="led-screen" title="LED 显示屏" @mouseenter="hovered = 'led'" @mouseleave="hovered = null">
<span v-if="ledText" :key="ledText" class="led-text">{{ ledText }}</span>
</div>
<!-- 传感器与执行器 -->
<div class="sensors">
<div class="card" title="照明灯 1:由 ZigBee 继电器节点 1 控制(教材项目 3 的路灯)">
<div class="card" title="照明灯 1:由 ZigBee 继电器节点 1 控制(教材项目 3 的路灯)" @mouseenter="hovered = 'streetLamp'" @mouseleave="hovered = null">
<div class="bulb" :class="{ on: streetLampOn }" />
<div class="card-title">照明灯 1</div>
<div class="card-note">ZigBee 节点 1</div>
</div>
<button class="card toggle" :class="{ active: digital.bodyInfrared }" title="点击切换:有人 / 无人" @click="toggle('bodyInfrared')">
<button
class="card toggle"
:class="{ active: digital.bodyInfrared }"
title="点击切换:有人 / 无人"
@click="toggle('bodyInfrared')"
@mouseenter="hovered = 'bodyInfrared'"
@mouseleave="hovered = null"
>
<svg viewBox="0 0 24 24" width="30" height="30" class="icon">
<circle cx="12" cy="5" r="2.6" fill="currentColor" />
<path fill="currentColor" d="M9 9h6l1.5 6h-2l-.8 7h-3.4l-.8-7h-2z" />
@@ -227,7 +326,7 @@ const cameraTitle = computed(() =>
<div class="card-note">{{ digital.bodyInfrared ? '有人' : '无人' }}</div>
</button>
<div class="card wide">
<div class="card wide" @mouseenter="hovered = 'humiture'" @mouseleave="hovered = null">
<div class="card-title">温湿度传感器</div>
<label v-for="key in ['temperature', 'humidity'] as AnalogKey[]" :key="key" class="slider">
<span>{{ ANALOG_INFO[key].label }}</span>
@@ -244,7 +343,7 @@ const cameraTitle = computed(() =>
</label>
</div>
<div class="card wide">
<div class="card wide" @mouseenter="hovered = 'light'" @mouseleave="hovered = null">
<div class="card-title">光照传感器</div>
<label class="slider">
<span>光照</span>
@@ -262,7 +361,12 @@ const cameraTitle = computed(() =>
</label>
</div>
<div class="card" :title="playing ? `广播正在播放 ${playing.name}` : '广播(教材项目 7 用 Windows Media Player 播放铃声)'">
<div
class="card"
:title="playing ? `广播正在播放 ${playing.name}` : '广播(教材项目 7 用 Windows Media Player 播放铃声)'"
@mouseenter="hovered = 'speaker'"
@mouseleave="hovered = null"
>
<svg viewBox="0 0 24 24" width="30" height="30" class="icon speaker" :class="{ on: playing }">
<path fill="currentColor" d="M4 9h4l5-4v14l-5-4H4z" />
<path
@@ -278,13 +382,27 @@ const cameraTitle = computed(() =>
<div class="card-note">{{ playing ? `播放 ${playing.name}` : '静音' }}</div>
</div>
<button class="card toggle" :class="{ active: digital.smoke }" title="点击切换:有烟雾 / 正常" @click="toggle('smoke')">
<button
class="card toggle"
:class="{ active: digital.smoke }"
title="点击切换:有烟雾 / 正常"
@click="toggle('smoke')"
@mouseenter="hovered = 'smoke'"
@mouseleave="hovered = null"
>
<div class="smoke" :class="{ on: digital.smoke }"><i /><i /><i /></div>
<div class="card-title">烟雾传感器</div>
<div class="card-note">{{ digital.smoke ? '有烟雾' : '正常' }}</div>
</button>
<button class="card toggle" :class="{ active: digital.fire }" title="点击切换:有火焰 / 正常" @click="toggle('fire')">
<button
class="card toggle"
:class="{ active: digital.fire }"
title="点击切换:有火焰 / 正常"
@click="toggle('fire')"
@mouseenter="hovered = 'fire'"
@mouseleave="hovered = null"
>
<svg viewBox="0 0 24 24" width="30" height="30" class="icon flame" :class="{ on: digital.fire }">
<path fill="currentColor" d="M12 2c1 4 5 5.5 5 11a5 5 0 0 1-10 0c0-3 1.5-4.5 2.5-6 .5 2 1.5 3 2.5 3-1-3 0-6 0-8z" />
</svg>
@@ -292,7 +410,12 @@ const cameraTitle = computed(() =>
<div class="card-note">{{ digital.fire ? '有火焰' : '正常' }}</div>
</button>
<div class="card" title="照明灯 2:由 ADAM-4150 的 DO1 控制(教材项目 7 的教室、寝室、会议室照明)">
<div
class="card"
title="照明灯 2:由 ADAM-4150 的 DO1 控制(教材项目 7 的教室、寝室、会议室照明)"
@mouseenter="hovered = 'roomLamp'"
@mouseleave="hovered = null"
>
<div class="bulb" :class="{ on: roomLampOn }" />
<div class="card-title">照明灯 2</div>
<div class="card-note">ADAM-4150 DO1</div>
@@ -301,10 +424,10 @@ const cameraTitle = computed(() =>
<!-- 采集模块与串口 -->
<div class="modules">
<div class="module">
<div class="module" :class="{ linked: linkedModule('adam4017') }">
<div class="module-title">ADAM-4017+ 模拟量(地址 2)</div>
<div class="readings">
<span v-for="(key, i) in ['temperature', 'humidity', 'light'] as AnalogKey[]" :key="key">
<span v-for="(key, i) in ['temperature', 'humidity', 'light'] as AnalogKey[]" :key="key" :class="{ linked: linkedChannel('adam4017', `Vin${i}`) }">
Vin{{ i }} {{ ANALOG_INFO[key].label }} <b>{{ board?.analog[key] ?? '-' }}</b>
</span>
<button class="link" @click="showMore = !showMore">{{ showMore ? '收起' : 'Vin3–6 更多传感器' }}</button>
@@ -326,19 +449,32 @@ const cameraTitle = computed(() =>
</div>
</div>
<div class="module">
<div class="module" :class="{ linked: linkedModule('adam4150') }">
<div class="module-title">ADAM-4150 数字量(地址 1)</div>
<div class="io-row">
<span v-for="(input, i) in DIGITAL_INPUTS" :key="input.key" class="io" :class="{ lit: digital[input.key] }"> <i />DI{{ i }} {{ input.label }} </span>
<span
v-for="(input, i) in DIGITAL_INPUTS"
:key="input.key"
class="io"
:class="{ lit: digital[input.key], linked: linkedChannel('adam4150', `DI${i}`) }"
>
<i />DI{{ i }} {{ input.label }}
</span>
</div>
<div class="io-row">
<span v-for="(label, i) in RELAYS" :key="label" class="io out" :class="{ lit: relays[i] }"> <i />DO{{ i }} {{ label }} </span>
<span v-for="(label, i) in RELAYS" :key="label" class="io out" :class="{ lit: relays[i], linked: linkedChannel('adam4150', `DO${i}`) }">
<i />DO{{ i }} {{ label }}
</span>
</div>
</div>
<div class="module ports">
<div class="module-title">串口服务器</div>
<div class="port-list"><span>COM2 → 4017</span><span>COM3 → 4150</span><span>COM4 → ZigBee 协调器</span><span>COM5 → LED 屏</span></div>
<div class="port-list" title="绿点:程序打开了这个串口">
<span v-for="port in BOARD_PORTS" :key="port" class="io out" :class="{ lit: portOpen(port), linked: linkedPort(port) }">
<i />{{ port }} → {{ portDevices(port) }}
</span>
</div>
<button class="reset" :disabled="!board" @click="emit('reset')">实训台复位</button>
</div>
</div>
@@ -860,6 +996,42 @@ button {
color: var(--muted);
}
.port-warning {
color: #ff8a80;
}
.port-warnings {
padding: 4px 8px;
background: #4a2f35;
border-radius: 4px;
}
/* 接线:鼠标所在设备接的模块、通道和串口 */
.wiring-path {
flex: 1;
min-width: 0;
margin: 0 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #6c7889;
}
.wiring-path.active {
color: #9fd3ff;
}
.module.linked {
box-shadow: inset 0 0 0 1px var(--accent);
}
.readings .linked,
.io.linked {
color: #9fd3ff;
text-decoration: underline;
text-underline-offset: 3px;
}
.link {
padding: 0;
background: none;
+21
View File
@@ -0,0 +1,21 @@
// 实训台接线在网页上的显示:串口服务器上每个口接了哪些设备。
// 串口下拉框展开时也用灰字标出来:运行区 provide,ControlView inject;设计器里没有实训台,用默认接线。
import type { InjectionKey, Ref } from 'vue'
import { BOARD_PORTS, DEFAULT_WIRING, PORT_DEVICE_NAMES, PORT_DEVICES, type PortDevice, type Wiring } from '@iot/shared'
/** 接在这个串口上的设备(项目 7 的 COM2 上并着 4017+ 和 4150) */
export function devicesOn(wiring: Wiring, port: string): PortDevice[] {
return PORT_DEVICES.filter((device) => wiring[device] === port)
}
/** 串口 → 设备名,如 { COM2: 'ADAM-4017+、ADAM-4150' };没接设备的口不列出 */
export function portLabels(wiring: Wiring = DEFAULT_WIRING): Record<string, string> {
const labels: Record<string, string> = {}
for (const port of BOARD_PORTS) {
const names = devicesOn(wiring, port).map((device) => PORT_DEVICE_NAMES[device])
if (names.length) labels[port] = names.join('、')
}
return labels
}
export const PORT_LABELS: InjectionKey<Ref<Record<string, string>>> = Symbol('portLabels')
+3 -2
View File
@@ -10,6 +10,7 @@ import DataGridViewControl from './DataGridViewControl.vue'
import DateTimePickerControl from './DateTimePickerControl.vue'
import LedControl from './LedControl.vue'
import TrackBarControl from './TrackBarControl.vue'
import { PORT_LABELS, portLabels } from '../scene/wiring'
const props = defineProps<{
node: ControlNode
@@ -61,8 +62,8 @@ const items = computed(() => (p.value.Items as string[] | undefined) ?? [])
const editable = computed(() => p.value.DropDownStyle !== 'DropDownList')
const combo = ref<HTMLElement>()
const listStyle = ref<CSSProperties>({})
/** 串口列表里每个口后面用灰字标出实训台上接的设备,只是提示,下拉框的值还是 COM2 这样的串口名 */
const portDevices: Record<string, string> = { COM2: 'ADAM-4017+', COM3: 'ADAM-4150', COM4: 'ZigBee 协调器', COM5: 'LED 屏' }
/** 串口列表里每个口后面用灰字标出实训台上接的设备(按当前实验的接线),只是提示,下拉框的值还是 COM2 这样的串口名 */
const portDevices = inject(PORT_LABELS, ref(portLabels()))
function openDropDown() {
if (!enabled.value) return