Skip to content

Commit d5d61b0

Browse files
committed
Add experimental move-seek command
1 parent 9769fb6 commit d5d61b0

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@
1616
short aliases are `-x` and `-y`.
1717
- Unknown command-line parameters must fail loudly. Do not silently ignore
1818
parser errors or selector options that are not valid for a verb.
19+
- DirectShow/IAMCameraControl results can differ between the agent sandbox and
20+
a normal interactive process. If camera capability reads unexpectedly report
21+
"IAMCameraControl (UVC) is not supported", re-run the same command outside the
22+
sandbox before treating it as a real device limitation.
1923
- Always create GitHub issues in English.

PTZControlConsole/Program.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class Program
3434
typeof(ZoomAbsoluteOptions),
3535
typeof(ZoomRelativeOptions),
3636
typeof(MoveAbsoluteOptions),
37+
typeof(MoveSeekOptions),
3738
typeof(MoveRelativeOptions)
3839
};
3940
private static readonly Type[] AllVerbTypes = PublicVerbTypes.Concat(new[] { typeof(DocsOptions) }).ToArray();
@@ -131,6 +132,7 @@ static int RunParsed(object parsed) =>
131132
ZoomAbsoluteOptions options => SetAbsoluteZoom(ResolveCamera(ToOptions(options)), options.Value, ParseMode(options.ModeName)),
132133
ZoomRelativeOptions options => SetRelativeZoom(ResolveCamera(ToOptions(options)), options.ValueDelta, ParseMode(options.ModeName)),
133134
MoveAbsoluteOptions options => MoveAbsolute(ResolveCamera(ToOptions(options)), ToOptions(options), ParseMode(options.ModeName)),
135+
MoveSeekOptions options => MoveSeek(ResolveCamera(ToOptions(options)), ToOptions(options), ParseMode(options.ModeName), options),
134136
MoveRelativeOptions options => MoveRelative(ResolveCamera(ToOptions(options)), ToOptions(options), ParseMode(options.ModeName)),
135137
DocsOptions options => GenerateDocs(options),
136138
_ => throw new ArgumentException("Unknown command.")
@@ -600,6 +602,42 @@ static int MoveAbsolute(string camera, Options options, ValueMode mode)
600602
return Ok();
601603
}
602604

605+
static int MoveSeek(string camera, Options options, ValueMode mode, MoveSeekOptions seekOptions)
606+
{
607+
if (options.X is null && options.Y is null) throw new ArgumentException("move-seek requires -x/--pan and/or -y/--tilt.");
608+
609+
var panTarget = options.X is null ? (int?)null : ConvertAbsoluteValue(camera, UvcCameraProperty.Pan, options.X.Value, mode);
610+
var tiltTarget = options.Y is null ? (int?)null : ConvertAbsoluteValue(camera, UvcCameraProperty.Tilt, options.Y.Value, mode);
611+
var panTolerance = panTarget is null ? 0 : ConvertSeekTolerance(camera, UvcCameraProperty.Pan, seekOptions.Tolerance, mode);
612+
var tiltTolerance = tiltTarget is null ? 0 : ConvertSeekTolerance(camera, UvcCameraProperty.Tilt, seekOptions.Tolerance, mode);
613+
var maxIterations = Math.Max(1, seekOptions.MaxIterations);
614+
var settleMs = Math.Max(0, seekOptions.SettleMilliseconds);
615+
616+
var lastDistance = GetSeekDistance(camera, panTarget, tiltTarget);
617+
for (var iteration = 1; iteration <= maxIterations; iteration++)
618+
{
619+
var panCurrent = panTarget is null ? (int?)null : CameraBackend.GetValue(camera, UvcCameraProperty.Pan);
620+
var tiltCurrent = tiltTarget is null ? (int?)null : CameraBackend.GetValue(camera, UvcCameraProperty.Tilt);
621+
var panReached = panTarget is null || Math.Abs(panTarget.Value - panCurrent!.Value) <= panTolerance;
622+
var tiltReached = tiltTarget is null || Math.Abs(tiltTarget.Value - tiltCurrent!.Value) <= tiltTolerance;
623+
if (panReached && tiltReached)
624+
return Ok();
625+
626+
var panDirection = panTarget is null ? (int?)null : Math.Sign(panTarget.Value - panCurrent!.Value);
627+
var tiltDirection = tiltTarget is null ? (int?)null : Math.Sign(tiltTarget.Value - tiltCurrent!.Value);
628+
CameraBackend.MoveRelativePanTilt(camera, panDirection, tiltDirection);
629+
if (settleMs > 0)
630+
Thread.Sleep(settleMs);
631+
632+
var distance = GetSeekDistance(camera, panTarget, tiltTarget);
633+
if (distance >= lastDistance)
634+
throw new InvalidOperationException($"move-seek did not make progress after {iteration} iteration(s). Current distance: {distance}; previous distance: {lastDistance}.");
635+
lastDistance = distance;
636+
}
637+
638+
throw new TimeoutException($"move-seek did not reach the target within {maxIterations} iteration(s). Final distance: {lastDistance}.");
639+
}
640+
603641
static int MoveRelative(string camera, Options options, ValueMode mode)
604642
{
605643
if (options.X is null && options.Y is null) throw new ArgumentException("move-relative requires -x/--pan and/or -y/--tilt.");
@@ -616,6 +654,16 @@ static int MoveRelative(string camera, Options options, ValueMode mode)
616654
return Ok();
617655
}
618656

657+
static int GetSeekDistance(string camera, int? panTarget, int? tiltTarget)
658+
{
659+
var distance = 0;
660+
if (panTarget is not null)
661+
distance += Math.Abs(panTarget.Value - CameraBackend.GetValue(camera, UvcCameraProperty.Pan));
662+
if (tiltTarget is not null)
663+
distance += Math.Abs(tiltTarget.Value - CameraBackend.GetValue(camera, UvcCameraProperty.Tilt));
664+
return distance;
665+
}
666+
619667
static int Ok()
620668
{
621669
Console.WriteLine("OK");
@@ -901,6 +949,16 @@ static int ClampRawValue(string camera, UvcCameraProperty property, int value)
901949
return Math.Clamp(value, range.min, range.max);
902950
}
903951

952+
static int ConvertSeekTolerance(string camera, UvcCameraProperty property, int tolerance, ValueMode mode)
953+
{
954+
tolerance = Math.Max(0, tolerance);
955+
if (mode == ValueMode.Raw)
956+
return tolerance;
957+
958+
var range = CameraBackend.GetRange(camera, property);
959+
return Math.Max(1, (int)Math.Round((range.max - range.min) * (tolerance / 100.0)));
960+
}
961+
904962
static int AddRawDelta(string camera, UvcCameraProperty property, int delta)
905963
{
906964
var range = CameraBackend.GetRange(camera, property);
@@ -1187,6 +1245,19 @@ sealed class MoveAbsoluteOptions : MoveOptions
11871245
{
11881246
}
11891247

1248+
[Verb("move-seek", HelpText = "Experimentally seek absolute pan and/or tilt values using relative movement.")]
1249+
sealed class MoveSeekOptions : MoveOptions
1250+
{
1251+
[Option("tolerance", Default = 2, HelpText = "Allowed target distance in the selected mode.")]
1252+
public int Tolerance { get; set; }
1253+
1254+
[Option("max-iterations", Default = 30, HelpText = "Maximum relative correction attempts.")]
1255+
public int MaxIterations { get; set; }
1256+
1257+
[Option("settle-ms", Default = 250, HelpText = "Delay after each relative movement attempt.")]
1258+
public int SettleMilliseconds { get; set; }
1259+
}
1260+
11901261
[Verb("move-relative", HelpText = "Change pan and/or tilt by relative deltas.")]
11911262
sealed class MoveRelativeOptions : MoveOptions
11921263
{

docs/generated/cli-help.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ PTZControlConsole 1.0.0.0
2525
zoom-absolute Set an absolute zoom value.
2626
zoom-relative Change zoom by a relative delta.
2727
move-absolute Set absolute pan and/or tilt values.
28+
move-seek Experimentally seek absolute pan and/or tilt values using
29+
relative movement.
2830
move-relative Change pan and/or tilt by relative deltas.
2931
help Display more information on a specific command.
3032
version Display version information.
@@ -336,6 +338,37 @@ PTZControlConsole 1.0.0.0
336338
337339
```
338340

341+
## move-seek
342+
343+
```text
344+
PTZControlConsole 1.0.0.0
345+
346+
--tolerance (Default: 2) Allowed target distance in the selected
347+
mode.
348+
349+
--max-iterations (Default: 30) Maximum relative correction attempts.
350+
351+
--settle-ms (Default: 250) Delay after each relative movement
352+
attempt.
353+
354+
-x, --pan Pan axis value.
355+
356+
-y, --tilt Tilt axis value.
357+
358+
-m, --mode Required. Value mode: percent or raw.
359+
360+
-c, --camera Camera device name fragment.
361+
362+
-d, --device-path Concrete camera device path.
363+
364+
-s, --slot PTZControl camera slot 1..3.
365+
366+
--help Display this help screen.
367+
368+
--version Display version information.
369+
370+
```
371+
339372
## move-relative
340373

341374
```text

docs/syntax.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ uses device/driver raw values.
119119

120120
```text
121121
PTZControlConsole move-absolute -m|--mode percent|raw [-x|--pan VALUE] [-y|--tilt VALUE] [-c|--camera "NamePart" | -d|--device-path "DevicePath" | -s|--slot 1..3]
122+
PTZControlConsole move-seek -m|--mode percent|raw [-x|--pan VALUE] [-y|--tilt VALUE] [--tolerance VALUE] [--max-iterations COUNT] [--settle-ms MS] [-c|--camera "NamePart" | -d|--device-path "DevicePath" | -s|--slot 1..3]
122123
PTZControlConsole move-relative -m|--mode percent|raw [-x|--pan VALUE_DELTA] [-y|--tilt VALUE_DELTA] [-c|--camera "NamePart" | -d|--device-path "DevicePath" | -s|--slot 1..3]
123124
```
124125

125126
`-x`/`--pan` controls pan. `-y`/`--tilt` controls tilt.
127+
`move-seek` is experimental. It reads the current raw pan/tilt values, moves
128+
relatively toward the target, and re-checks progress until the target is reached
129+
or the iteration limit is hit.
126130

127131
## Selection options
128132

scripts/test-console-camera-linux.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ run_preparation_command "Move pan away from absolute percent position 40" move-a
240240
run_camera_command "Move pan axis to absolute percent position 40" true move-absolute --mode percent --pan 40 "${selector[@]}"
241241
run_preparation_command "Move tilt away from absolute percent position 60" move-absolute --mode percent --tilt 40 "${selector[@]}"
242242
run_camera_command "Move tilt axis to absolute percent position 60" true move-absolute --mode percent --tilt 60 "${selector[@]}"
243+
run_preparation_command "Move pan and tilt away from seek percent target 50/50" move-absolute --mode percent --pan 0 --tilt 0 "${selector[@]}"
244+
run_camera_command "Seek pan and tilt to absolute percent position 50/50 using relative movement" true move-seek --mode percent --pan 50 --tilt 50 --tolerance 5 --max-iterations 40 --settle-ms 250 "${selector[@]}"
243245
run_preparation_command "Move pan before relative percent +10" move-absolute --mode percent --pan 40 "${selector[@]}"
244246
run_camera_command "Change pan axis by relative percent value +10" true move-relative --mode percent --pan 10 "${selector[@]}"
245247
run_preparation_command "Move pan before relative percent -10" move-absolute --mode percent --pan 60 "${selector[@]}"
@@ -258,6 +260,8 @@ run_preparation_command "Move tilt away from absolute raw +$RAW_MOVE_ABSOLUTE" m
258260
run_camera_command "Move tilt axis to absolute raw position +$RAW_MOVE_ABSOLUTE" true move-absolute --mode raw --tilt "$RAW_MOVE_ABSOLUTE" "${selector[@]}"
259261
run_preparation_command "Move tilt away from absolute raw -$RAW_MOVE_ABSOLUTE" move-absolute --mode raw --tilt 0 "${selector[@]}"
260262
run_camera_command "Move tilt axis to absolute raw position -$RAW_MOVE_ABSOLUTE" true move-absolute --mode raw --tilt "-$RAW_MOVE_ABSOLUTE" "${selector[@]}"
263+
run_preparation_command "Move pan and tilt away from seek raw target 0/0" move-absolute --mode raw --pan "$RAW_MOVE_ABSOLUTE" --tilt "$RAW_MOVE_ABSOLUTE" "${selector[@]}"
264+
run_camera_command "Seek pan and tilt to absolute raw position 0/0 using relative movement" true move-seek --mode raw --pan 0 --tilt 0 --tolerance "$RAW_MOVE_DELTA" --max-iterations 40 --settle-ms 250 "${selector[@]}"
261265
run_preparation_command "Move pan before relative raw +$RAW_MOVE_DELTA" move-absolute --mode raw --pan 0 "${selector[@]}"
262266
run_camera_command "Change pan axis by relative raw value +$RAW_MOVE_DELTA" true move-relative --mode raw --pan "$RAW_MOVE_DELTA" "${selector[@]}"
263267
run_preparation_command "Move pan before relative raw -$RAW_MOVE_DELTA" move-absolute --mode raw --pan 0 "${selector[@]}"

scripts/test-console-camera-windows.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ Invoke-PreparationCommand "Move pan away from absolute percent position 40" (@("
199199
Invoke-CameraCommand "Move pan axis to absolute percent position 40" (@("move-absolute", "--mode", "percent", "--pan", "40") + $selector)
200200
Invoke-PreparationCommand "Move tilt away from absolute percent position 60" (@("move-absolute", "--mode", "percent", "--tilt", "40") + $selector)
201201
Invoke-CameraCommand "Move tilt axis to absolute percent position 60" (@("move-absolute", "--mode", "percent", "--tilt", "60") + $selector)
202+
Invoke-PreparationCommand "Move pan and tilt away from seek percent target 50/50" (@("move-absolute", "--mode", "percent", "--pan", "0", "--tilt", "0") + $selector)
203+
Invoke-CameraCommand "Seek pan and tilt to absolute percent position 50/50 using relative movement" (@("move-seek", "--mode", "percent", "--pan", "50", "--tilt", "50", "--tolerance", "5", "--max-iterations", "40", "--settle-ms", "250") + $selector)
202204
Invoke-PreparationCommand "Move pan before relative percent +10" (@("move-absolute", "--mode", "percent", "--pan", "40") + $selector)
203205
Invoke-CameraCommand "Change pan axis by relative percent value +10" (@("move-relative", "--mode", "percent", "--pan", "10") + $selector)
204206
Invoke-PreparationCommand "Move pan before relative percent -10" (@("move-absolute", "--mode", "percent", "--pan", "60") + $selector)
@@ -217,6 +219,8 @@ Invoke-PreparationCommand "Move tilt away from absolute raw +$RawMoveAbsolute" (
217219
Invoke-CameraCommand "Move tilt axis to absolute raw position +$RawMoveAbsolute" (@("move-absolute", "--mode", "raw", "--tilt", "$RawMoveAbsolute") + $selector)
218220
Invoke-PreparationCommand "Move tilt away from absolute raw -$RawMoveAbsolute" (@("move-absolute", "--mode", "raw", "--tilt", "0") + $selector)
219221
Invoke-CameraCommand "Move tilt axis to absolute raw position -$RawMoveAbsolute" (@("move-absolute", "--mode", "raw", "--tilt", "$(-1 * $RawMoveAbsolute)") + $selector)
222+
Invoke-PreparationCommand "Move pan and tilt away from seek raw target 0/0" (@("move-absolute", "--mode", "raw", "--pan", "$RawMoveAbsolute", "--tilt", "$RawMoveAbsolute") + $selector)
223+
Invoke-CameraCommand "Seek pan and tilt to absolute raw position 0/0 using relative movement" (@("move-seek", "--mode", "raw", "--pan", "0", "--tilt", "0", "--tolerance", "$RawMoveDelta", "--max-iterations", "40", "--settle-ms", "250") + $selector)
220224
Invoke-PreparationCommand "Move pan before relative raw +$RawMoveDelta" (@("move-absolute", "--mode", "raw", "--pan", "0") + $selector)
221225
Invoke-CameraCommand "Change pan axis by relative raw value +$RawMoveDelta" (@("move-relative", "--mode", "raw", "--pan", "$RawMoveDelta") + $selector)
222226
Invoke-PreparationCommand "Move pan before relative raw -$RawMoveDelta" (@("move-absolute", "--mode", "raw", "--pan", "0") + $selector)

0 commit comments

Comments
 (0)