实训台接线:串口要选对。接线做成数据(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
+35
-2
@@ -4,9 +4,13 @@ import {
|
||||
ANALOG_INFO,
|
||||
type BoardState,
|
||||
type ClientMessage,
|
||||
DEFAULT_WIRING,
|
||||
DIGITAL_CHANNELS,
|
||||
PORT_DEVICES,
|
||||
type PortDevice,
|
||||
type RunnerInput,
|
||||
type RunnerMessage,
|
||||
type Wiring,
|
||||
} from '@iot/shared'
|
||||
import { config } from './config'
|
||||
|
||||
@@ -19,9 +23,16 @@ export function defaultBoardState(): BoardState {
|
||||
ledText: '',
|
||||
camera: { on: false, x: 3, y: 1 },
|
||||
clock: null,
|
||||
wiring: { ...DEFAULT_WIRING },
|
||||
opened: closedPorts(),
|
||||
usedUnopened: [],
|
||||
}
|
||||
}
|
||||
|
||||
function closedPorts(): BoardState['opened'] {
|
||||
return { adam4017: null, adam4150: null, zigbee: null, led: null }
|
||||
}
|
||||
|
||||
/** 一个浏览器会话的虚拟实训台。多次运行之间保留状态,和真实实训台断电前继电器保持原状一样 */
|
||||
export class Board {
|
||||
state = defaultBoardState()
|
||||
@@ -33,9 +44,17 @@ export class Board {
|
||||
analog: ANALOG_CHANNELS.map((key) => this.state.analog[key]),
|
||||
digital: DIGITAL_CHANNELS.map((key) => this.state.digital[key]),
|
||||
clock: this.state.clock,
|
||||
wiring: this.state.wiring,
|
||||
}
|
||||
}
|
||||
|
||||
/** 换成某个实验的接线;返回 true 表示有变化 */
|
||||
setWiring(wiring: Wiring) {
|
||||
if (PORT_DEVICES.every((device) => this.state.wiring[device] === wiring[device])) return false
|
||||
this.state.wiring = { ...wiring }
|
||||
return true
|
||||
}
|
||||
|
||||
/** 浏览器调节环境;返回 true 表示传感器读数有变化 */
|
||||
applyInput(message: Extract<ClientMessage, { type: 'boardInput' }>) {
|
||||
let changed = false
|
||||
@@ -81,6 +100,17 @@ export class Board {
|
||||
|
||||
/** 学生程序控制了输出设备;返回 true 表示实训台状态有变化 */
|
||||
applyDevice(message: Extract<RunnerMessage, { type: 'device' }>) {
|
||||
const device = message.device as PortDevice
|
||||
if (PORT_DEVICES.includes(device) && (message.action === 'open' || message.action === 'close')) {
|
||||
this.state.opened[device] = message.action === 'open' ? String(message.data ?? '') : null
|
||||
if (message.action === 'open') this.state.usedUnopened = this.state.usedUnopened.filter((d) => d !== device)
|
||||
return true
|
||||
}
|
||||
if (PORT_DEVICES.includes(device) && message.action === 'unopened') {
|
||||
if (this.state.usedUnopened.includes(device)) return false
|
||||
this.state.usedUnopened = [...this.state.usedUnopened, device]
|
||||
return true
|
||||
}
|
||||
switch (`${message.device}/${message.action}`) {
|
||||
case 'led/text':
|
||||
this.state.ledText = String(message.data ?? '')
|
||||
@@ -111,13 +141,16 @@ export class Board {
|
||||
}
|
||||
}
|
||||
|
||||
/** 学生程序结束:摄像头不再传画面,云台回到正中(下次运行的程序从正中开始) */
|
||||
/** 学生程序结束:摄像头不再传画面,云台回到正中(下次运行的程序从正中开始);打开的串口随程序关闭 */
|
||||
programExited() {
|
||||
this.state.camera = { on: false, x: 3, y: 1 }
|
||||
this.state.opened = closedPorts()
|
||||
this.state.usedUnopened = []
|
||||
}
|
||||
|
||||
/** 复位不改接线(接线跟着实验走),也不动正在运行的程序打开的串口 */
|
||||
reset() {
|
||||
this.state = defaultBoardState()
|
||||
this.state = { ...defaultBoardState(), wiring: this.state.wiring, opened: this.state.opened, usedUnopened: this.state.usedUnopened }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// 自动评分:编译一次学生工程,每个检查项各启动一次程序,按步骤操作并核对结果。
|
||||
// 检查大部分时间在等定时器,CPU 主要花在编译和启动程序上;全站同时跑 config.gradeConcurrency 个检查,其余排队。
|
||||
import type { CheckResult, ControlNode, GradeResult } from '@iot/shared'
|
||||
import { describePortProblem, portProblems, type CheckResult, type ControlNode, type GradeResult } from '@iot/shared'
|
||||
import { rm } from 'node:fs/promises'
|
||||
import { dirname } from 'node:path'
|
||||
import { config } from '../config'
|
||||
import { compileService } from '../compile-service'
|
||||
import { gradingDatabaseDir } from '../databases'
|
||||
import { sampleWiring } from '../samples'
|
||||
import type { Check, Expectation, Step } from './checks'
|
||||
import { ProgramDriver } from './driver'
|
||||
|
||||
@@ -56,6 +57,7 @@ class Failure extends Error {}
|
||||
|
||||
async function runCheck(dir: string, check: Check, sampleId?: string): Promise<CheckResult> {
|
||||
const driver = new ProgramDriver()
|
||||
driver.board.setWiring(await sampleWiring(sampleId))
|
||||
const dbDir = sampleId ? await gradingDatabaseDir(sampleId) : undefined
|
||||
try {
|
||||
driver.start(dir, dbDir)
|
||||
@@ -237,7 +239,7 @@ async function expectStep(driver: ProgramDriver, step: Extract<Step, { kind: 'ex
|
||||
key = found
|
||||
}
|
||||
const ok = await driver.waitFor(() => satisfied(driver, e, key), within)
|
||||
if (!ok) throw new Failure(describeFailure(driver, e, step.desc, wait))
|
||||
if (!ok) throw new Failure(describeFailure(driver, e, step.desc, wait) + portNote(driver))
|
||||
}
|
||||
|
||||
/** 列表框的 Text 只是选中的那一项,检查"包含"时看所有列表项 */
|
||||
@@ -303,8 +305,15 @@ function chartSeries(node: ControlNode | undefined): { name: string; points: unk
|
||||
return (node?.props.Chart as { series?: { name: string; points: unknown[] }[] } | undefined)?.series ?? []
|
||||
}
|
||||
|
||||
/** 串口打开错了或没打开时,设备没反应的原因多半在这里,附在不通过的说明后面 */
|
||||
function portNote(driver: ProgramDriver) {
|
||||
const problems = portProblems(driver.board.state)
|
||||
return problems.length ? `。注意:${problems.map(describePortProblem).join(';')}` : ''
|
||||
}
|
||||
|
||||
function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind: 'unchanged' }>, desc: string | undefined, wait: string): string {
|
||||
const prefix = wait ? `${wait}` : ''
|
||||
// "期望 6 秒内……":数字前空一格
|
||||
const prefix = wait ? ` ${wait}` : ''
|
||||
const onOff = (on: boolean) => (on ? '打开' : '关闭')
|
||||
switch (e.kind) {
|
||||
case 'prop': {
|
||||
@@ -333,7 +342,7 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
case 'relay':
|
||||
return `期望${prefix}实训台的${RELAY_NAMES[e.index] ?? `DO${e.index}`}(ADAM-4150 DO${e.index})${desc ?? onOff(e.on)},实际是${onOff(driver.board.state.relays[e.index])}的`
|
||||
case 'zigbee':
|
||||
return `期望${prefix}实训台的${ZIGBEE_NAMES[e.index] ?? `ZigBee 继电器 ${e.index}`}${desc ?? onOff(e.on)},实际是${onOff(driver.board.state.zigbee[e.index])}的`
|
||||
return `期望${prefix}实训台的${ZIGBEE_NAMES[e.index] ?? `ZigBee 继电器 ${e.index}`} ${desc ?? onOff(e.on)},实际是${onOff(driver.board.state.zigbee[e.index])}的`
|
||||
case 'messageBox': {
|
||||
const shown = driver.messageBoxes.map((b) => `"${b.text}"`).join('、')
|
||||
return `期望${prefix}弹出${desc ?? `包含"${e.text}"的提示框`},${shown ? `实际弹出的是 ${shown}` : '实际没有弹出提示框'}`
|
||||
|
||||
+6
-3
@@ -9,7 +9,7 @@ import { config } from './config'
|
||||
import { openDatabase } from './db'
|
||||
import { createGradingQueue } from './grading/queue'
|
||||
import { restoreExperimentDatabases, userDatabaseDir } from './databases'
|
||||
import { listSamples } from './samples'
|
||||
import { knownSample, sampleWiring } from './samples'
|
||||
import { cleanupStaleRuns, RunSession } from './run-session'
|
||||
import { sqlService } from './sql-service'
|
||||
import { checkSandbox, sandboxError } from './sandbox'
|
||||
@@ -78,15 +78,18 @@ const server = Bun.serve({
|
||||
if (!session) return
|
||||
if (message.type === 'run') {
|
||||
try {
|
||||
const { sampleId } = message
|
||||
if (typeof sampleId === 'string' && (await listSamples()).some((s) => s.id === sampleId)) {
|
||||
const sampleId = await knownSample(message.sampleId)
|
||||
if (sampleId) {
|
||||
// 建库失败不影响运行(程序连不上库时会报出来)
|
||||
await restoreExperimentDatabases(ws.data.userId!, sampleId, true).catch((error) => console.error('自动建实验数据库失败', error))
|
||||
}
|
||||
session.setWiring(await sampleWiring(sampleId))
|
||||
await session.start(message.files)
|
||||
} catch (error) {
|
||||
ws.send(JSON.stringify({ type: 'serverError', message: String(error) } satisfies ServerMessage))
|
||||
}
|
||||
} else if (message.type === 'experiment') {
|
||||
session.setWiring(await sampleWiring(await knownSample(message.sampleId)))
|
||||
} else if (message.type === 'stop') {
|
||||
await session.stop()
|
||||
} else if (message.type === 'boardInput') {
|
||||
|
||||
@@ -75,8 +75,8 @@ export function studentDetail(db: Database, assignmentId: number, userId: number
|
||||
/** 一份提交的代码和它属于谁(老师在实验页里运行学生的程序时用) */
|
||||
export function submissionFiles(db: Database, submissionId: number) {
|
||||
const row = db
|
||||
.query<{ files: string; assignment_id: number; title: string; name: string; username: string }, [number]>(
|
||||
`SELECT s.files, s.assignment_id, a.title, u.name, u.username
|
||||
.query<{ files: string; assignment_id: number; sample_id: string; title: string; name: string; username: string }, [number]>(
|
||||
`SELECT s.files, s.assignment_id, a.sample_id, a.title, u.name, u.username
|
||||
FROM submissions s JOIN assignments a ON a.id = s.assignment_id JOIN users u ON u.id = s.user_id WHERE s.id = ?`,
|
||||
)
|
||||
.get(submissionId)
|
||||
@@ -84,6 +84,8 @@ export function submissionFiles(db: Database, submissionId: number) {
|
||||
return {
|
||||
files: JSON.parse(row.files) as SourceFile[],
|
||||
assignmentId: row.assignment_id,
|
||||
/** 作业基于的实验:运行时按它给实训台接线、建实验数据库 */
|
||||
sampleId: row.sample_id,
|
||||
assignmentTitle: row.title,
|
||||
studentName: row.name,
|
||||
username: row.username,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mkdir, mkdtemp, rm } from 'node:fs/promises'
|
||||
import { dirname, join, normalize, resolve } from 'node:path'
|
||||
import type { Subprocess } from 'bun'
|
||||
import type { ClientMessage, RunnerInput, RunnerMessage, ServerMessage, SourceFile } from '@iot/shared'
|
||||
import type { ClientMessage, RunnerInput, RunnerMessage, ServerMessage, SourceFile, Wiring } from '@iot/shared'
|
||||
import { Board } from './board'
|
||||
import { compileService } from './compile-service'
|
||||
import { config } from './config'
|
||||
@@ -129,6 +129,13 @@ export class RunSession {
|
||||
this.input(this.board.runnerInputs())
|
||||
}
|
||||
|
||||
/** 换成实验的接线(打开实验、运行时) */
|
||||
setWiring(wiring: Wiring) {
|
||||
if (!this.board.setWiring(wiring)) return
|
||||
this.sendBoard()
|
||||
this.input(this.board.runnerInputs())
|
||||
}
|
||||
|
||||
boardReset() {
|
||||
this.board.reset()
|
||||
this.sendBoard()
|
||||
|
||||
+25
-1
@@ -1,6 +1,6 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { join, relative, resolve } from 'node:path'
|
||||
import type { SourceFile } from '@iot/shared'
|
||||
import { BOARD_PORTS, DEFAULT_WIRING, PORT_DEVICES, type SourceFile, type Wiring } from '@iot/shared'
|
||||
import { config } from './config'
|
||||
|
||||
const SAMPLES_DIR = config.samplesDir
|
||||
@@ -22,6 +22,30 @@ export async function listSamples(): Promise<SampleInfo[]> {
|
||||
return samples.sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
|
||||
/** 浏览器传来的实验编号:是已有的示例才返回 */
|
||||
export async function knownSample(id: unknown): Promise<string | undefined> {
|
||||
return typeof id === 'string' && (await listSamples()).some((s) => s.id === id) ? id : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 实验的实训台接线:meta.json 的 wiring 只写和默认接线不同的设备,如项目 7 的 { "adam4150": "COM2" }。
|
||||
* 没有这个实验(或没给 id)时用默认接线
|
||||
*/
|
||||
export async function sampleWiring(id?: string): Promise<Wiring> {
|
||||
const wiring = { ...DEFAULT_WIRING }
|
||||
if (!id) return wiring
|
||||
const path = resolve(SAMPLES_DIR, id, 'meta.json')
|
||||
if (!path.startsWith(`${SAMPLES_DIR}/`)) return wiring
|
||||
const file = Bun.file(path)
|
||||
if (!(await file.exists())) return wiring
|
||||
const custom = ((await file.json()) as { wiring?: Partial<Wiring> }).wiring ?? {}
|
||||
for (const device of PORT_DEVICES) {
|
||||
const port = custom[device]
|
||||
if (port !== undefined && BOARD_PORTS.includes(port)) wiring[device] = port
|
||||
}
|
||||
return wiring
|
||||
}
|
||||
|
||||
export async function loadSample(id: string): Promise<SourceFile[] | null> {
|
||||
const dir = resolve(SAMPLES_DIR, id)
|
||||
if (!dir.startsWith(`${SAMPLES_DIR}/`)) return null
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { expect, test } from 'bun:test'
|
||||
import { DEFAULT_WIRING, describePortProblem, portProblems } from '@iot/shared'
|
||||
import { Board } from '../src/board'
|
||||
import { sampleWiring } from '../src/samples'
|
||||
|
||||
test('滑块输入取整并限制在范围内,开关输入只接受布尔值', () => {
|
||||
const board = new Board()
|
||||
@@ -31,3 +33,50 @@ test('学生程序控制输出设备:LED 屏、4150 继电器、ZigBee 继电
|
||||
expect(board.state.relays.every((on) => !on)).toBe(true)
|
||||
expect(board.state.ledText).toBe('')
|
||||
})
|
||||
|
||||
test('接线:记下程序打开的串口,指出打开错了的设备;复位不改接线,程序结束时串口关闭', () => {
|
||||
const board = new Board()
|
||||
board.applyDevice({ type: 'device', device: 'adam4017', action: 'open', data: 'COM2' })
|
||||
board.applyDevice({ type: 'device', device: 'adam4150', action: 'open', data: 'com2' })
|
||||
board.applyDevice({ type: 'device', device: 'led', action: 'open', data: '' })
|
||||
expect(portProblems(board.state).map(describePortProblem)).toEqual([
|
||||
'程序在 com2 上打开 ADAM-4150,它接在 COM3',
|
||||
'程序没有选串口就打开了 LED 屏,它接在 COM5',
|
||||
])
|
||||
|
||||
// 项目 7:4150 并到 COM2 的 485 总线上
|
||||
expect(board.setWiring({ ...DEFAULT_WIRING, adam4150: 'COM2' })).toBe(true)
|
||||
expect(board.setWiring({ ...DEFAULT_WIRING, adam4150: 'COM2' })).toBe(false)
|
||||
expect(board.runnerInputs().wiring?.adam4150).toBe('COM2')
|
||||
expect(portProblems(board.state).map((w) => w.device)).toEqual(['led'])
|
||||
|
||||
board.applyDevice({ type: 'device', device: 'led', action: 'close', data: null })
|
||||
expect(portProblems(board.state)).toEqual([])
|
||||
board.reset()
|
||||
expect(board.state.wiring.adam4150).toBe('COM2')
|
||||
expect(board.state.opened.adam4017).toBe('COM2')
|
||||
board.programExited()
|
||||
expect(Object.values(board.state.opened).every((port) => port === null)).toBe(true)
|
||||
})
|
||||
|
||||
test('串口没打开(或已关闭)时程序还在读写设备:提示一次,打开后、程序结束时清除', () => {
|
||||
const board = new Board()
|
||||
expect(board.applyDevice({ type: 'device', device: 'adam4150', action: 'unopened', data: null })).toBe(true)
|
||||
expect(board.applyDevice({ type: 'device', device: 'adam4150', action: 'unopened', data: null })).toBe(false)
|
||||
expect(portProblems(board.state).map(describePortProblem)).toEqual(['ADAM-4150 的串口没有打开,程序却在读写它(它接在 COM3)'])
|
||||
|
||||
board.applyDevice({ type: 'device', device: 'adam4150', action: 'open', data: 'COM3' })
|
||||
expect(portProblems(board.state)).toEqual([])
|
||||
board.applyDevice({ type: 'device', device: 'adam4150', action: 'close', data: null })
|
||||
board.applyDevice({ type: 'device', device: 'adam4150', action: 'unopened', data: null })
|
||||
expect(portProblems(board.state).map((p) => p.device)).toEqual(['adam4150'])
|
||||
board.programExited()
|
||||
expect(portProblems(board.state)).toEqual([])
|
||||
})
|
||||
|
||||
test('实验的接线:项目 7 把 4150 并到 COM2,其他实验和未知实验用默认接线', async () => {
|
||||
expect(await sampleWiring('p7-t1-智慧教室')).toEqual({ ...DEFAULT_WIRING, adam4150: 'COM2' })
|
||||
expect(await sampleWiring('p4-t3-报警灯')).toEqual(DEFAULT_WIRING)
|
||||
expect(await sampleWiring('../p7-t1-智慧教室')).toEqual(DEFAULT_WIRING)
|
||||
expect(await sampleWiring()).toEqual(DEFAULT_WIRING)
|
||||
})
|
||||
@@ -78,7 +78,7 @@ describe('改坏的代码', () => {
|
||||
const result = await grade(dir, await checksOf(id), id)
|
||||
const realtime = result.results.find((r) => r.title === '能实时显示入侵数据')!
|
||||
expect(realtime.passed).toBe(false)
|
||||
expect(realtime.message).toBe('期望6 秒内 txt_even 的文字包含"发现入侵",实际是"正常"')
|
||||
expect(realtime.message).toBe('期望 6 秒内 txt_even 的文字包含"发现入侵",实际是"正常"')
|
||||
// 开关按钮的两项不受影响
|
||||
expect(result.results.filter((r) => r.passed).length).toBe(2)
|
||||
}, 60000)
|
||||
@@ -135,7 +135,11 @@ describe('改坏的代码', () => {
|
||||
`),
|
||||
]
|
||||
const result = await grade(dir, checks, id)
|
||||
expect(result.results.at(-1)).toMatchObject({ passed: false, message: '期望窗体上的 lamp 点开关后灯亮,实际是灭的' })
|
||||
// 这一项没有先打开串口,程序控制 4150 时串口没开,说明后面附上提示
|
||||
expect(result.results.at(-1)).toMatchObject({
|
||||
passed: false,
|
||||
message: '期望窗体上的 lamp 点开关后灯亮,实际是灭的。注意:ADAM-4150 的串口没有打开,程序却在读写它(它接在 COM2)',
|
||||
})
|
||||
}, 60000)
|
||||
|
||||
test('关闭时忘了挂起线程', async () => {
|
||||
@@ -163,7 +167,7 @@ describe('改坏的代码', () => {
|
||||
})
|
||||
const result = await grade(dir, await checksOf(id), id)
|
||||
const fire = result.results.find((r) => r.title === '能自动推送火情到 LED 屏')!
|
||||
expect(fire).toMatchObject({ passed: false, message: '期望8 秒内实训台 LED 屏显示"发现火情"(勾选"自动推送火情"后有火焰),实际什么也没显示' })
|
||||
expect(fire).toMatchObject({ passed: false, message: '期望 8 秒内实训台 LED 屏显示"发现火情"(勾选"自动推送火情"后有火焰),实际什么也没显示' })
|
||||
expect(result.results.filter((r) => !r.passed)).toHaveLength(1)
|
||||
}, 60000)
|
||||
|
||||
@@ -189,6 +193,20 @@ describe('改坏的代码', () => {
|
||||
const result = await grade(dir, await checksOf(id), id)
|
||||
expect(result.results[0]).toMatchObject({ passed: false, message: '期望实训台的报警灯(ADAM-4150 DO0)打开,实际是关闭的' })
|
||||
}, 60000)
|
||||
|
||||
test('串口写死成 COM2:4150 收不到命令,说明里指出接在 COM3', async () => {
|
||||
const id = 'p4-t3-报警灯'
|
||||
const dir = await prepare(id, 'port', {
|
||||
file: 'Form1.cs',
|
||||
from: 'DeviceCtrlHelper.adam4150Helper.Open(com_port.Text);',
|
||||
to: 'DeviceCtrlHelper.adam4150Helper.Open("COM2");',
|
||||
})
|
||||
const result = await grade(dir, await checksOf(id), id)
|
||||
expect(result.results[0]).toMatchObject({
|
||||
passed: false,
|
||||
message: '期望实训台的报警灯(ADAM-4150 DO0)打开,实际是关闭的。注意:程序在 COM2 上打开 ADAM-4150,它接在 COM3',
|
||||
})
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
describe('评分规则写错时指出位置', () => {
|
||||
|
||||
@@ -160,7 +160,7 @@ test('项目2 任务1:选串口、打开、推送文字到 LED 屏', async ()
|
||||
runner.send({ type: 'action', id: id('btn_Open'), action: 'click' })
|
||||
runner.send({ type: 'action', id: id('btn_Send'), action: 'click' })
|
||||
|
||||
const device = await runner.waitFor('device', (m) => m.device === 'led')
|
||||
const device = await runner.waitFor('device', (m) => m.device === 'led' && m.action === 'text')
|
||||
expect(device.data).toBe('欢迎学习物联网')
|
||||
const led = id('led')
|
||||
const ledPatch = await runner.waitFor('patch', (m) => m.changes.some((c) => c.id === led && c.prop === 'LedText'))
|
||||
@@ -219,10 +219,11 @@ test('项目3 任务1:单选按钮切换,事件顺序与 WinForms 一致,
|
||||
const { runner, id } = await startSample('p3-t1-ZigBee路灯')
|
||||
const lamp = id('lamp1')
|
||||
|
||||
// 串口没打开时控制无效:没有继电器动作,但界面上的灯照样会亮(学生能看出两者不一致)
|
||||
// 串口没打开时控制无效:没有继电器动作(实训台提示串口没打开),但界面上的灯照样会亮(学生能看出两者不一致)
|
||||
runner.send({ type: 'action', id: id('rb_Open'), action: 'click' })
|
||||
await runner.waitFor('patch', (m) => m.changes.some((c) => c.id === lamp && c.prop === 'IsOn' && c.value === true))
|
||||
expect(runner.messages.some((m) => m.type === 'device')).toBe(false)
|
||||
await runner.waitFor('device', (m) => m.device === 'zigbee' && m.action === 'unopened')
|
||||
expect(runner.messages.some((m) => m.type === 'device' && m.action === 'relay')).toBe(false)
|
||||
|
||||
runner.send({ type: 'action', id: id('com_port'), action: 'select', index: 2 })
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
@@ -232,7 +233,7 @@ test('项目3 任务1:单选按钮切换,事件顺序与 WinForms 一致,
|
||||
runner.send({ type: 'action', id: id('rb_Close'), action: 'click' })
|
||||
const devices: unknown[] = []
|
||||
await relayOff
|
||||
for (const m of runner.messages) if (m.type === 'device') devices.push(m.data)
|
||||
for (const m of runner.messages) if (m.type === 'device' && m.action === 'relay') devices.push(m.data)
|
||||
expect(devices).toEqual([
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
@@ -329,6 +330,27 @@ test('项目4 任务1:红外对射被遮挡时显示"发现入侵"', async ()
|
||||
await even('发现入侵')
|
||||
}, 30000)
|
||||
|
||||
test('串口要选对:4150 接在 COM3,在 COM2 上打开读不到传感器;换成项目 7 的接线(并到 COM2)就能读到', async () => {
|
||||
const { runner, id } = await startSample('p4-t1-红外对射')
|
||||
const even = (value: string) => runner.next('patch', (m) => m.changes.some((c) => c.id === id('txt_even') && c.value === value), 5000)
|
||||
runner.send({ type: 'board', analog: ANALOG_DEFAULT, digital: [false, false, false, true] })
|
||||
const opened = runner.next('device', (m) => m.device === 'adam4150' && m.action === 'open')
|
||||
runner.send({ type: 'action', id: id('com_port'), action: 'select', index: 0 })
|
||||
runner.send({ type: 'action', id: id('btn_open'), action: 'click' })
|
||||
// 实训台据此提示"程序在 COM2 上打开 ADAM-4150";收不到回应,和真机一样读到的全是 false
|
||||
expect((await opened).data).toBe('COM2')
|
||||
await even('正常')
|
||||
|
||||
const intrusion = even('发现入侵')
|
||||
runner.send({
|
||||
type: 'board',
|
||||
analog: ANALOG_DEFAULT,
|
||||
digital: [false, false, false, true],
|
||||
wiring: { adam4017: 'COM2', adam4150: 'COM2', zigbee: 'COM4', led: 'COM5' },
|
||||
})
|
||||
await intrusion
|
||||
}, 30000)
|
||||
|
||||
test('项目4 任务2:打开摄像机显示画面,转动后画面变化,有人翻墙时画面变化,拍照保存到 Camera 文件夹', async () => {
|
||||
const { runner, id } = await startSample('p4-t2-网络摄像机')
|
||||
const pictures = () => runner.next('patch', (m) => m.changes.some((c) => c.id === id('pictureBox1') && c.prop === 'Image'), 5000)
|
||||
@@ -367,7 +389,7 @@ test('项目4 任务2:打开摄像机显示画面,转动后画面变化,
|
||||
test('项目4 任务3:开报警灯——实训台 DO0 动作,窗体上的报警灯亮', async () => {
|
||||
const { runner, id } = await startSample('p4-t3-报警灯')
|
||||
await openAdam4150(runner, id)
|
||||
const relay = runner.next('device', (m) => m.device === 'adam4150')
|
||||
const relay = runner.next('device', (m) => m.device === 'adam4150' && m.action === 'do')
|
||||
const lamp = runner.next('patch', (m) => m.changes.some((c) => c.id === id('alarm1') && c.prop === 'IsOn' && c.value === true))
|
||||
runner.send({ type: 'action', id: id('btn_alarmOpen'), action: 'click' })
|
||||
expect((await relay).data).toEqual([0, 1])
|
||||
@@ -730,9 +752,7 @@ test('禁用:分组框禁用后里面的控件 Enabled 读出 false、触发 E
|
||||
const output = runner.messages.flatMap((m) => (m.type === 'console' ? [m.text] : [])).join('')
|
||||
// 浏览器只收到分组框自己的 Enabled,子控件按父控件画成禁用
|
||||
expect(findByName(form, 'button').props.Enabled).toBe(true)
|
||||
expect(output).toBe(
|
||||
'button changed=False\ngroup=False button=False off=False\nbutton changed=True\ngroup=True button=True off=False\nclick\n',
|
||||
)
|
||||
expect(output).toBe('button changed=False\ngroup=False button=False off=False\nbutton changed=True\ngroup=True button=True off=False\nclick\n')
|
||||
}, 30000)
|
||||
|
||||
test('透明背景:文本框、窗体等设透明背景色和 WinForms 一样报错,标签、面板可以', async () => {
|
||||
|
||||
Reference in new issue
Block a user