实训台接线:串口要选对。接线做成数据(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+
上课前逐个检查 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:
1 parent
163e553de4
commit
46e0f40cce
35 files changed
+637
-98
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user