0. 背景与目标
-
已验证: 能识别 Canon EOS R6,可拍照、可把照片下载到电脑;并观察到"相机回放里没有新照片"的现象。
-
目标:
-
薄封装 / CameraCore(相机控制内核)
-
照相亭控制层(Booth Controller:流程、UI、打印、AI、上传)
-
未来可对齐 Snappic 的"有趣 UI、多端扩展"的形态(你给的机器人形态图:大屏 + 上方相机头)。
-
1. 总体技术路线(工程分层)
核心思想要"工程化":
UI 不直接调用 SDK;UI 只发"命令事件"。
相机模型(CameraModel)统一缓存状态,并用 Observer 广播变化。
CommandProcessor 串行执行命令,处理 DEVICE_BUSY 重试。
可以把它分成 4 层:
1.1 EDSDK P/Invoke 层(最低层)
-
文件:
EDSDK.cs -
作用:声明 EDSDK 的常量、结构体、枚举、DllImport 的函数签名(EdsInitializeSDK / EdsOpenSession / EdsSetPropertyData / EdsDownload... 等)。
这就是未来要的"薄封装"的最底层:纯签名 + 常量定义,不带业务。
1.2 Model + Event 层(相机状态中心)
-
文件:
CameraModel.cs,CameraEvent.cs,Observer.cs,CameraEventListener.cs -
作用:
-
CameraModel保存"当前相机状态"(属性值、属性描述、EVF 状态、下载状态...) -
通过
NotifyObservers(...)向所有观察者广播事件(UI 控件、窗口、进度条等)
-
这层的价值:把"相机状态"从 UI/命令里抽离,形成单一事实来源(Single Source of Truth)。
1.3 Command 层(业务原子操作)
-
文件夹:
Command/*以及Command.cs -
作用:每个命令封装一次 EDSDK 操作:
-
会话:
OpenSessionCommand,CloseSessionCommand -
拍照:
PressShutterCommand,(TakePictureCommand文件里有省略...) -
下载:
DownloadCommand,DownloadAllFilesCommand,DeleteAllFilesCommand -
EVF:
StartEvfCommand,DownloadEvfCommand,EndEvfCommand,DriveLensCommand,DoEvfAFCommand,ClickAFCommand...
-
典型特征 :命令执行失败时,统一上报模型事件;遇到 DEVICE_BUSY 则发 DEVICE_BUSY 事件以便重试(见 PressShutterCommand.cs 的逻辑:EdsSendCommand 失败且为 Busy 时发事件)。PressShutterCommand.cs 里能看到这一套路。
1.4 Action / Controller / UI 适配层(把 UI 变成命令)
-
文件:
ActionSource.cs,ActionListener.cs,ActionEvent.cs,CameraController.cs,MainWindow.cs -
作用:
-
ActionSource.FireEvent(Command, Arg):UI 触发命令的统一入口 -
ActionListener:监听动作事件 -
CameraController:把动作翻译成"投递命令到 CommandProcessor" -
UI 控件(Button/ComboBox/Label/ProgressBar/EvfPictureBox)普遍实现 IObserver:订阅模型事件、刷新自己
-
既有的"属性控件就是插件",就是这一层的体现。
2. 关键链路解析
2.1 启动与连接相机
典型流程(从 Program.cs → MainWindow / CameraController 推断):
-
EdsInitializeSDK() -
获取相机列表、选中相机
-
注册回调(ObjectEvent / PropertyEvent / StateEvent)→
CameraEventListener -
EdsOpenSession() -
同步属性与可选范围(PropDesc),驱动 UI 初始化
这套结构对"照相亭"非常关键:我们要做的并不是 UI,而是 稳定的状态机 + 回调/事件闭环。
2.2 为什么"电脑能看到新拍的,但 SD 卡回放看不到"?
OpenSessionCommand.cs 中,对 PropID_SaveTo 做了设置:
-
当某个条件满足(代码里判断了一个
PropID_FixedMovie的值)时,把SaveTo设为EdsSaveTo.Host,并调用EdsSetCapacity伪装"电脑存储空间很大"。(项目里在OpenSessionCommand.cs60~68 行附近能看到:设置SaveTo.Host+EdsSetCapacity) -
否则把
SaveTo设为EdsSaveTo.Camera(约 72 行附近)。
结论:
-
SaveTo=Host:照片"只到电脑",相机卡里不出现,所以相机回放看不到。
-
SaveTo=Camera:照片进卡,相机回放可见;如果你还想电脑也拿到,需要监听对象事件或再额外下载。
这与观察完全一致:你能"立刻在电脑看到刚拍的",同时相机回放看不到(典型 Host 模式)。
2.3 SD 卡"文件下载"下载到哪里?
DownloadAllFilesCommand.cs 里写死了默认路径:
-
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)(约 115 行) -
然后拼接文件名
dirItemInfo.szFileName(约 117 行) -
再
EdsCreateFileStream(...)+EdsDownload(...)(约 119~129 行)
所以下载到:当前 Windows 用户的"图片(My Pictures)"目录。
后面做照相亭必改:这必须变成可配置路径(比如 D:\Booth\Shots\ 或按订单号/会话号分目录)。
3. LiveView(EVF)到底是什么?StartEvf / DownloadEvf / EndEvf 的意义
3.1 EVF(LiveView)定义
-
相机进入"电子取景器实时流"状态,把每一帧(通常是 JPEG)通过 EDSDK 以 stream 的形式送到 PC。
-
PC 端不断拉帧并显示,同时可以在 LiveView 上做:
-
对焦框信息(FocusInfo)
-
触摸/点击对焦(ClickAF)
-
执行 AF(DoEvfAF)
-
变焦(Zoom)
-
驱动镜头对焦(DriveLens:类似手动对焦步进)
-
3.2 你这个 Demo 的"拉帧方式"是事件驱动链,不是 Timer
EvfPictureBox.cs 中的核心逻辑非常关键:
-
当收到
EVFDATA_CHANGED:-
先触发
GET_PROPERTY(FocusInfo) -
立刻再次触发
DOWNLOAD_EVF以获取下一帧(你代码里在 67~71 行附近能看到:GET_PROPERTY(FocusInfo) 然后 FireEvent(DOWNLOAD_EVF))
-
-
当检测到
PropID_Evf_OutputDevice包含 PC 时:-
认为 PC LiveView 开始,置
_active=true并触发第一次DOWNLOAD_EVF(约 82~88 行附近)
-
这意味着:
-
StartEvf负责把相机切进"输出到 PC"的状态(通常是设置 EvfOutputDevice) -
DownloadEvf是"拉一帧" -
EvfPictureBox把"拉帧循环"串起来(拉到一帧 → 显示 → 继续拉下一帧) -
EndEvf负责退出 LiveView
这段机制你未来做照相亭特别有用:
你可以把"实时预览"完全抽成一个独立模块(EVF Pipeline),UI 只订阅"最新帧图像"。
4. 属性系统(Property Layer)是怎么设计的?为什么这么多 ComboBox/Label/TrackBar?
4.1 核心模板
很多 PropertyComboBox.cs、PropertyTrackBar.cs 等"基类"。整个属性系统大概遵循这个模式:
-
map:把相机的枚举值 → 人类可读文本
- 例如
TvComboBox、WhiteBalanceComboBox、IsoComboBox、PictureStyleComboBox等都内置map.Add(...)。
- 例如
-
PropDesc 驱动:下拉框的可选项来自相机返回的"可选范围"
- 这点非常关键:不同机型/模式支持值不同,不能写死。
-
控件实现 IObserver:
-
收到
PROPERTY_CHANGED→ 刷当前值 -
收到
PROPERTY_DESC_CHANGED→ 刷可选列表 + 再刷当前值
-
-
用户操作不改相机,只发命令事件
- 例如 ActionButton/RadioButton:点击直接
FireEvent(Command, Arg)(你前面传的 ActionButton/ActionRadioButton 就是这个套路)
- 例如 ActionButton/RadioButton:点击直接
4.2 这对照相亭的直接价值
照相亭常见需求:
-
强制固定:ISO、白平衡、画质、构图比例、曝光补偿上限等
-
UI 上允许用户可调(可选):美颜强度、模板、倒计时、连拍张数
-
幕后动态变化:存储剩余张数、电量、温度、连接状态
这套属性层让你能做到:
-
UI 是 Web/Flutter/Qt 都无所谓:只要消费"属性状态 JSON"即可
-
相机兼容性更强:R6/R100/R8 等都可以靠 PropDesc 自适应
5. 线程与稳定性:为什么它能"跑得稳"?
5.1 CommandProcessor 串行化
-
命令是被排队执行的(串行),避免并发调用 EDSDK 造成状态混乱。
-
Busy 时通过事件提示重试(
EDS_ERR_DEVICE_BUSY是 EDSDK 常见状态)。
5.2 事件回调统一进 Model,再广播给 UI
这是 WinForms 稳定运行的重要原因:
-
回调里不直接碰 UI
-
UI 只在 Observer 通知后刷新(通常还会做线程切换)
未来做照相亭:这套"回调→模型→广播"结构必须保留,否则你会被线程/UI 崩溃折磨。
6. 如何 演进成"照相亭控制层 + 薄封装"?
下面是 MVP 技术路线(结合你"对齐 Snappic 多端 + MVP 要快"的现实):
6.1 拆分
拆成 3 个工程:
-
CameraCore(.NET Class Library)
-
含:
EDSDK.cs+ Model + Command + EventListener + CommandProcessor -
对外暴露:
CameraService类(Open/Close/Capture/StartLiveView/GetFrame/SetProperty...)
-
-
CameraHost(Windows 常驻进程 / Service)
-
运行在机器人底座的工控机/miniPC 上(最现实)
-
对外提供 HTTP/WebSocket:
/capture、/status、/liveview/frame、/property/set
-
负责:下载存储、文件命名、上传、打印、异常自恢复
-
-
BoothUI
-
MVP :Web(React/Vue)全屏 或 Flutter
-
UI 不需要知道 EDSDK,只调用 CameraHost API
-
未来要 iPad/手机联动(对齐 Snappic)也更容易:同一套 Web/Flutter UI 直接复用
-
6.2 为什么我不建议 MVP 直接上 Qt "全家桶"
Qt 做大屏 kiosk UI 很强,但:
-
你仍然要写 EDSDK 封装(C++/PInvoke)
-
多端(iPad/手机)复用差(除非再做一套)
-
MVP 的最大风险不是 UI,而是相机链路稳定性、拍照下载、断线恢复
所以 MVP 更优先:先稳相机内核 + API 服务化,UI 可以快速迭代。
7. 机器人照相亭形态

-
一体机大屏(竖屏)+ 头部摄像头模组(像机器人头)
-
"固定点位 + 触摸交互 + 自动拍照/引导"
这类硬件非常适合:
-
底座内置 Windows mini PC(跑 CameraHost)
-
屏幕跑 Web 全屏(Chromium kiosk)或 Flutter Windows
-
远程手机/平板做"遥控/预览/扫码取片"------直接复用 API
这条路能最快对齐 Snappic 的"体验感",同时不被多端技术债拖死。
8. 下一步:
-
把 SaveTo 策略产品化
-
你要明确:照相亭"照片最终落点"是什么?
-
常见做法:
SaveTo=Host(更快拿到)+ 需要时再同步/备份到卡(可选)
-
-
把下载路径、命名规则、会话目录抽出来
- 现在写死 MyPictures(
DownloadAllFilesCommand.cs115 行附近)
- 现在写死 MyPictures(
-
做"拍照一次的完整闭环"状态机
- StartEVF(可选)→ 倒计时 → PressShutter → 等 ObjectEvent → 下载 → 生成预览 → 上传/打印
-
把 CameraCore 做成一个 Class Library
- WinForms 只是 Demo UI,你要把逻辑从 UI 剥离出来
-
做 CameraHost(HTTP/WebSocket)
-
先让外部能调用:拍照 / 获取最新照片路径 / 获取 liveview 帧
-
UI 先用最简单页面验证(别先卷动画)
-
。