项目 4 任务 2 网络摄像机:PictureBox、Image / Bitmap、照片列表
- 摄像头:自绘东围墙场景(Camera/scene.svg),按云台位置切成 640×480 帧(build-frames.ts); Pan 每次转一格,红外对射被遮挡时画面出现翻墙的人;类名、委托、事件参数与教材 dll 一致, ImageRead 每 250 ms 在后台线程触发 - Image / Bitmap 保存真实图片数据:Save、FromFile、FromStream,文件夹不存在时报 GDI+ 一般性错误; 图片每张只发给浏览器一次(按内容哈希缓存) - 程序所在目录(AppDomain.CurrentDomain.BaseDirectory,编译检查单独放行)和当前目录指向可写的 bin/Debug, 沙箱里在沙箱的临时目录 - PictureBox(SizeMode、BorderStyle);设计器工具箱加"图片框";运行区显示程序保存的图片,点开看大图; 实训台摄像头图标显示开启状态 - 评分规则加图片框显示图片、摄像头开关与转向、保存图片三种检查;示例 p4-t2-网络摄像机 - 测试:摄像头端到端、图片读写与程序目录、AppDomain 编译检查、评分出错情况 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ed9450c02
commit
6b088240da
76 files changed
+1290
-14
No files matched your search
@@ -15,6 +15,7 @@ export function defaultBoardState(): BoardState {
|
||||
relays: [false, false, false, false, false],
|
||||
zigbee: [false, false],
|
||||
ledText: '',
|
||||
camera: { on: false, x: 3, y: 1 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +70,25 @@ export class Board {
|
||||
this.state.zigbee[relay] = on === 1
|
||||
return true
|
||||
}
|
||||
case 'camera/on':
|
||||
this.state.camera.on = message.data === true
|
||||
return true
|
||||
case 'camera/pan': {
|
||||
const [x, y] = message.data as [number, number]
|
||||
this.state.camera.x = x
|
||||
this.state.camera.y = y
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 学生程序结束:摄像头不再传画面,云台回到正中(下次运行的程序从正中开始) */
|
||||
programExited() {
|
||||
this.state.camera = { on: false, x: 3, y: 1 }
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.state = defaultBoardState()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ export type Expectation =
|
||||
| { kind: 'relay'; index: number; on: boolean }
|
||||
| { kind: 'zigbee'; index: number; on: boolean }
|
||||
| { kind: 'messageBox'; text: string }
|
||||
/** 图片框里显示了图片 */
|
||||
| { kind: 'image'; control: string }
|
||||
/** 摄像头在传画面(on)/ 已关闭(off),或云台比开始时朝某个方向转过 */
|
||||
| { kind: 'camera'; state: 'on' | 'off' | 'up' | 'down' | 'left' | 'right' }
|
||||
/** 程序用 Image.Save 保存了图片,路径包含这段文字(如 Camera/) */
|
||||
| { kind: 'savedImage'; path: string }
|
||||
| { kind: 'console'; text: string }
|
||||
|
||||
/** 期望默认最多等多久(毫秒);依赖定时器的检查在规则里写 within */
|
||||
@@ -59,6 +65,12 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
if ('led' in raw) return { kind: 'led', text: str(raw.led, where, ' LED 屏应显示的文字') }
|
||||
if ('messageBox' in raw) return { kind: 'messageBox', text: str(raw.messageBox, where, '提示框应包含的文字') }
|
||||
if ('console' in raw) return { kind: 'console', text: str(raw.console, where, '控制台应输出的文字') }
|
||||
if ('camera' in raw) {
|
||||
const state = String(raw.camera)
|
||||
if (!['on', 'off', 'up', 'down', 'left', 'right'].includes(state)) throw new Error(`${where}:camera 要写 on、off、up、down、left 或 right`)
|
||||
return { kind: 'camera', state: state as Extract<Expectation, { kind: 'camera' }>['state'] }
|
||||
}
|
||||
if ('savedImage' in raw) return { kind: 'savedImage', path: str(raw.savedImage, where, '保存的图片路径应包含的文字') }
|
||||
if ('relay' in raw) return { kind: 'relay', index: Number(raw.relay), on: bool(raw.on, where, 'on') }
|
||||
if ('zigbee' in raw) return { kind: 'zigbee', index: Number(raw.zigbee), on: bool(raw.on, where, 'on') }
|
||||
|
||||
@@ -69,6 +81,7 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
return { kind: 'matches', control, pattern: new RegExp(source), source }
|
||||
}
|
||||
if ('unchanged' in raw) return { kind: 'unchanged', control, ms: duration(raw.unchanged, where) }
|
||||
if ('image' in raw) return { kind: 'image', control }
|
||||
for (const [key, prop] of Object.entries(PROP_KEYS)) {
|
||||
if (key in raw) return { kind: 'prop', control, prop, value: STRING_PROPS.has(prop) ? String(raw[key]) : bool(raw[key], where, key) }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export class ProgramDriver {
|
||||
private readonly nodes = new Map<string, ControlNode>()
|
||||
private readonly parents = new Map<string, ControlNode>()
|
||||
readonly messageBoxes: { text: string; caption: string }[] = []
|
||||
/** 程序用 Image.Save 保存的图片路径 */
|
||||
readonly savedImages: string[] = []
|
||||
readonly exceptions: string[] = []
|
||||
console = ''
|
||||
exited = false
|
||||
@@ -106,6 +108,9 @@ export class ProgramDriver {
|
||||
case 'device':
|
||||
this.board.applyDevice(message)
|
||||
break
|
||||
case 'imageSaved':
|
||||
this.savedImages.push(message.path)
|
||||
break
|
||||
case 'console':
|
||||
this.console += message.text
|
||||
break
|
||||
|
||||
@@ -11,6 +11,9 @@ const START_TIMEOUT = 10_000
|
||||
|
||||
const RELAY_NAMES = ['报警灯', '照明灯', '路灯', '插座 1', '插座 2']
|
||||
const ZIGBEE_NAMES = ['ZigBee 节点 0', '照明灯 1']
|
||||
/** 摄像头云台开始时的位置(见 IPCameraHelper.cs) */
|
||||
const CAMERA_START = { x: 3, y: 1 }
|
||||
const CAMERA_WORDS = { on: '开启(传送画面)', off: '关闭', up: '向上转', down: '向下转', left: '向左转', right: '向右转' }
|
||||
|
||||
// ---------- 并发限制 ----------
|
||||
let running = 0
|
||||
@@ -160,6 +163,14 @@ function satisfied(driver: ProgramDriver, e: Exclude<Expectation, { kind: 'uncha
|
||||
return driver.messageBoxes.some((box) => box.text.includes(e.text))
|
||||
case 'console':
|
||||
return driver.console.includes(e.text)
|
||||
case 'image':
|
||||
return Boolean(props(e.control)?.Image)
|
||||
case 'camera': {
|
||||
const { on, x, y } = driver.board.state.camera
|
||||
return { on, off: !on, up: y < CAMERA_START.y, down: y > CAMERA_START.y, left: x < CAMERA_START.x, right: x > CAMERA_START.x }[e.state]
|
||||
}
|
||||
case 'savedImage':
|
||||
return driver.savedImages.some((path) => path.includes(e.path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,5 +206,13 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
}
|
||||
case 'console':
|
||||
return `期望${prefix}控制台${desc ?? `输出"${e.text}"`},实际输出${quote(driver.console.trim())}`
|
||||
case 'image':
|
||||
return `期望${prefix} ${e.control} ${desc ?? '显示图片'},实际是空的`
|
||||
case 'camera':
|
||||
return `期望${prefix}网络摄像机${desc ?? CAMERA_WORDS[e.state]},实际${driver.board.state.camera.on ? '开着' : '没有开启'}${e.state === 'on' || e.state === 'off' ? '' : '、云台没有朝这个方向转'}`
|
||||
case 'savedImage': {
|
||||
const saved = driver.savedImages.map((p) => `"${p}"`).join('、')
|
||||
return `期望${prefix}${desc ?? `保存图片到"${e.path}"`},${saved ? `实际保存的是 ${saved}` : '实际没有保存图片'}`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,8 @@ export class RunSession {
|
||||
clearTimeout(timeout)
|
||||
if (this.proc !== proc) return
|
||||
this.proc = undefined
|
||||
this.board.programExited()
|
||||
this.sendBoard()
|
||||
const cpuMessage = cpuLimitMessage(code, proc.signalCode)
|
||||
if (cpuMessage) this.send({ type: 'console', text: cpuMessage })
|
||||
this.send({ type: 'exited', code })
|
||||
@@ -159,6 +161,8 @@ export class RunSession {
|
||||
if (!proc) return
|
||||
proc.kill()
|
||||
await proc.exited
|
||||
this.board.programExited()
|
||||
this.sendBoard()
|
||||
}
|
||||
|
||||
private async cleanup() {
|
||||
|
||||
Reference in new issue
Block a user