MCPcopy
hub / github.com/cpaczek/skylight / ViscaCamera

Class ViscaCamera

tracker/src/camera/visca.ts:157–716  ·  view source on GitHub ↗

Source from the content-addressed store, hash-verified

155 [...b].map((x) => x.toString(16).padStart(2, "0")).join(" ");
156
157export class ViscaCamera implements CameraDriver {
158 readonly kind = "visca" as const;
159
160 private socket: dgram.Socket | null = null;
161 private seq = 0;
162 private inFlight = 0;
163 private diag: CameraDiagnostics = {
164 kind: "visca",
165 connected: false,
166 lastSeq: 0,
167 inFlight: 0,
168 };
169
170 private pose: CameraPose | null = null;
171 private lastInquiryReply = 0;
172 /** When the last PAN/TILT position reply landed (zoom replies excluded). */
173 private lastPanTiltReplyAt = 0;
174 /** Signed commanded rates, deg/s — dead-reckons the pose between replies
175 * (position inquiries stall outright around drives on this firmware). */
176 private cmdPanDps = 0;
177 private cmdTiltDps = 0;
178
179 private inquiryTimer: ReturnType<typeof setInterval> | null = null;
180 private commandTimer: ReturnType<typeof setInterval> | null = null;
181 /** Latest requested absolute pose (latest-wins throttle). */
182 private wanted: CameraPose | null = null;
183 /** Last pan/tilt actually transmitted (zoom tracked separately). */
184 private lastSent: { panDeg: number; tiltDeg: number } | null = null;
185 /** When that absolute was sent + its expected travel time (gate expiry). */
186 private lastSentAt = 0;
187 private lastSentDurMs = 0;
188 private lastSentZoom: number | null = null;
189 /** Last velocity-drive command key (dedupe). */
190 private lastDrive: string | null = null;
191 /** Speed bytes for the pending absolute move. */
192 private wantedPanVV = 0x18;
193 private wantedTiltWW = 0x14;
194 /** Pending absolute move is speed-matched (carrot) — bypass the gate. */
195 private wantedMatched = false;
196
197 constructor(private o: ViscaOptions) {}
198
199 start(): void {
200 if (this.socket) return;
201 this.openSocket();
202
203 // 10 Hz: closed-loop pursuit corrects on pose error — at 5 Hz the error
204 // term was up to 200 ms stale, which reads as hunting.
205 const inquiryMs = 1000 / (this.o.inquiryHz ?? 10);
206 this.inquiryTimer = setInterval(() => {
207 // Reply-stall recovery: the firmware addresses replies to the most
208 // recent CLIENT and that state machine occasionally wedges (observed
209 // during calibration sweeps). A brand-new source port forces it to
210 // re-register us; sequence resets alone do not.
211 if (this.lastInquiryReply && Date.now() - this.lastInquiryReply > 4000) {
212 this.diag.lastError = "inquiry stall — rebinding socket";
213 this.openSocket();
214 this.lastInquiryReply = Date.now(); // back off one stall window

Callers

nothing calls this directly

Calls

no outgoing calls

Tested by

no test coverage detected