1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! CHDK-specific methods, added to `PtpSession` via additional inherent impl blocks.
use super::opcode::{self, Sub, PTP_OC_CHDK};
use super::script::{ErrorCategory, ScriptId, ScriptMsg, ScriptStatus, ScriptValue};
use crate::ptp::{DataPhase, PtpSession};
use crate::{Error, Result};
/// CHDK PTP protocol version reported by the camera.
#[derive(Debug, Clone, Copy)]
pub struct ChdkVersion {
pub major: u32,
pub minor: u32,
}
impl std::fmt::Display for ChdkVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
impl PtpSession {
// ---------- Version ----------
/// Query the CHDK PTP protocol version (sub-command 0, no data phase).
pub async fn chdk_version(&mut self) -> Result<ChdkVersion> {
let resp = self
.command(PTP_OC_CHDK, &[Sub::Version.as_u32()], DataPhase::None)
.await?;
resp.ok()?;
if resp.params.len() < 2 {
return Err(Error::Codec(format!(
"chdk_version: expected ≥2 response params, got {}",
resp.params.len()
)));
}
Ok(ChdkVersion {
major: resp.params[0],
minor: resp.params[1],
})
}
// ---------- Script execution ----------
/// Send Lua source to the camera's on-board interpreter and return the
/// assigned `ScriptId`. Returns immediately — the script runs asynchronously
/// on the camera; use `script_status()` and `read_script_msg()` to drive it.
pub async fn execute_script_lua(&mut self, source: &str) -> Result<ScriptId> {
let param2 = opcode::SCRIPT_LANG_LUA as u32
| opcode::SCRIPT_FLAG_FLUSH_CAM_MSGS
| opcode::SCRIPT_FLAG_FLUSH_HOST_MSGS;
// CHDK loads scripts via luaL_loadstring (NUL-terminated). Without the
// trailing NUL the parser walks into stale buffer contents from prior runs.
let mut payload = Vec::with_capacity(source.len() + 1);
payload.extend_from_slice(source.as_bytes());
payload.push(0);
let resp = self
.command(
PTP_OC_CHDK,
&[Sub::ExecuteScript.as_u32(), param2],
DataPhase::Out(&payload),
)
.await?;
resp.ok()?;
if resp.params.is_empty() {
return Err(Error::Codec(
"execute_script_lua: response missing script_id".into(),
));
}
Ok(resp.params[0])
}
/// Poll the running script's status (RUN bit, MSG-pending bit).
pub async fn script_status(&mut self) -> Result<ScriptStatus> {
let resp = self
.command(PTP_OC_CHDK, &[Sub::ScriptStatus.as_u32()], DataPhase::None)
.await?;
resp.ok()?;
let raw = resp.params.first().copied().unwrap_or(0);
Ok(ScriptStatus::from_raw(raw))
}
/// Drain one message from the camera's outbound queue.
/// Returns `ScriptMsg::None` if the queue is empty.
pub async fn read_script_msg(&mut self) -> Result<ScriptMsg> {
let resp = self
.command(PTP_OC_CHDK, &[Sub::ReadScriptMsg.as_u32()], DataPhase::In)
.await?;
resp.ok()?;
let msg_type = resp.params.first().copied().unwrap_or(opcode::MSGTYPE_NONE);
let subtype = resp.params.get(1).copied().unwrap_or(0);
let script_id = resp.params.get(2).copied().unwrap_or(0);
// params[3] would be length; resp.data already carries the payload.
Ok(match msg_type {
opcode::MSGTYPE_NONE => ScriptMsg::None,
opcode::MSGTYPE_ERR => ScriptMsg::Error {
script_id,
category: ErrorCategory::from_raw(subtype),
text: String::from_utf8_lossy(&resp.data).into_owned(),
},
opcode::MSGTYPE_RET => ScriptMsg::Return {
script_id,
value: ScriptValue::decode(subtype, &resp.data),
},
opcode::MSGTYPE_USER => ScriptMsg::User {
script_id,
value: ScriptValue::decode(subtype, &resp.data),
},
other => {
return Err(Error::Codec(format!(
"read_script_msg: unknown msg_type {other}"
)))
}
})
}
/// Convenience: execute Lua source, poll until the script finishes
/// (or `timeout_ms` elapses), drain all messages, return them in order.
///
/// Errors in the returned vec do NOT abort — callers can inspect the full
/// transcript. Returns `Err(...)` only on transport / timeout failures.
pub async fn execute_script_wait(
&mut self,
source: &str,
timeout_ms: u64,
) -> Result<Vec<ScriptMsg>> {
let _script_id = self.execute_script_lua(source).await?;
let mut msgs = Vec::new();
let start = std::time::Instant::now();
// 20ms keeps probe RTTs tight (matters for clock-sync offset estimation
// and any other timing-sensitive use). For a typical 1–3s shoot()
// the extra polling cost is negligible (~50–150 idle queries vs 7–20).
let poll_interval = std::time::Duration::from_millis(20);
loop {
// Drain any pending messages first.
loop {
let m = self.read_script_msg().await?;
if matches!(m, ScriptMsg::None) {
break;
}
msgs.push(m);
}
let status = self.script_status().await?;
if !status.running() {
// Final drain after the script ends (RET / late ERR).
loop {
let m = self.read_script_msg().await?;
if matches!(m, ScriptMsg::None) {
break;
}
msgs.push(m);
}
return Ok(msgs);
}
if start.elapsed() > std::time::Duration::from_millis(timeout_ms) {
return Err(Error::Usb(format!(
"script timed out after {timeout_ms} ms"
)));
}
std::thread::sleep(poll_interval);
}
}
/// Take a picture. Sends `shoot()` to the camera, waits for completion,
/// returns the full script transcript (errors included).
///
/// If the camera is in playback mode, switches it to record first
/// (this adds ~2 s of warm-up time).
pub async fn shoot(&mut self) -> Result<Vec<ScriptMsg>> {
let src = "\
if not get_mode() then \
switch_mode_usb(1) \
sleep(2000) \
end \
shoot()";
self.execute_script_wait(src, 20_000).await
}
}