Skip to content

Commit 6db279a

Browse files
feat: Add support for dollar-prefixed special commands ($B and $Q)
Add CommandFamily::Backlash and CommandFamily::SmartDrive to support LX200GPS special commands that use two-character designators starting with '$'. Protocol additions: - $B family: Active backlash compensation (:$BAdd#, :$BZdd#) - $Q family: Smart Drive PEC control (:$Q#, :$QA+#, :$QZ-#, etc.) Implementation changes: - Updated CommandFamily enum with Backlash('$') and SmartDrive('@') - Modified identify_family() to accept string_view instead of char - Added special handling for $-prefixed commands (two-char lookup) - Updated parse_command_parts() to handle $XY command pattern - Commands like :$BA15# now parse as name="$BA", params="15" Tests: - Added test_backlash_commands with 2 test cases - Added test_smartdrive_commands with 3 test cases - All 103 tests passing (25 command tests, 51 coordinate, 17 integration, 10 parser) This completes full LX200 protocol support including all standard and special command families from the Meade specification.
1 parent 8f47131 commit 6db279a

3 files changed

Lines changed: 131 additions & 9 deletions

File tree

include/lx200/lx200.hpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,19 @@ namespace lx200
4343
/**
4444
* @brief LX200 command family classification
4545
*
46-
* Commands are grouped by their first character after the ':' prefix.
46+
* Commands are grouped by their first character(s) after the ':' prefix.
4747
* This enables fast lookup and categorization.
4848
*
49+
* Special cases:
50+
* - Most commands: Single character designator (e.g., :Gxx#, :Mxx#)
51+
* - Dollar commands: Two-character designator starting with '$' (e.g., :$Bxx#, :$Qxx#)
52+
*
4953
* Note: Some command families have semantic groupings across multiple
5054
* prefixes (e.g., date/time commands span C, G, H, S families).
5155
*/
5256
enum class CommandFamily : uint8_t {
5357
Alignment = 'A', ///< Telescope alignment commands
58+
Backlash = '$', ///< Active backlash compensation (:$B commands, LX200GPS)
5459
Reticle = 'B', ///< Reticle brightness and accessory control
5560
Sync = 'C', ///< Sync control (telescope position synchronization)
5661
Distance = 'D', ///< Distance bars
@@ -65,6 +70,7 @@ enum class CommandFamily : uint8_t {
6570
Movement = 'M', ///< Slew and movement control
6671
Precision = 'P', ///< Toggle precision mode
6772
Quit = 'Q', ///< Stop/quit movement
73+
SmartDrive = '@', ///< Smart Drive PEC control (:$Q commands, LX200GPS/LX16")
6874
Derotator = 'r', ///< Field de-rotator control (lowercase r, LX16")
6975
Rate = 'R', ///< Slew rate control
7076
SetInfo = 'S', ///< Set telescope information
@@ -483,8 +489,8 @@ class ParserState
483489
bool command_complete_{false}; ///< Command complete flag
484490
PrecisionMode precision_{PrecisionMode::High}; ///< Current precision mode
485491

486-
/// Identify command family from first character
487-
CommandFamily identify_family(char first_char) const noexcept;
492+
/// Identify command family from command name (handles both single-char and $-prefixed)
493+
CommandFamily identify_family(std::string_view name) const noexcept;
488494

489495
/// Split command name from parameters
490496
void parse_command_parts(std::string_view &name, std::string_view &params) const noexcept;

lib/lx200/src/lx200.cpp

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ std::optional<Command> ParserState::get_command() noexcept
8686
std::string_view name, params;
8787
parse_command_parts(name, params);
8888

89-
// Identify command family by first character
90-
// Case-sensitive mapping (e.g., 'g' = GPS, 'G' = GetInfo)
91-
CommandFamily family = identify_family(name.empty() ? '\0' : name[0]);
89+
// Identify command family
90+
// Most commands: by first character (e.g., 'G' = GetInfo, 'g' = GPS)
91+
// Dollar commands: by first two characters (e.g., '$B' = Backlash, '$Q' = SmartDrive)
92+
CommandFamily family = identify_family(name);
9293

9394
// Create command
9495
Command cmd{
@@ -106,10 +107,24 @@ std::optional<Command> ParserState::get_command() noexcept
106107
return cmd;
107108
}
108109

109-
CommandFamily ParserState::identify_family(char first_char) const noexcept
110+
CommandFamily ParserState::identify_family(std::string_view name) const noexcept
110111
{
111-
// Direct character-to-family mapping
112-
switch (first_char) {
112+
if (name.empty()) {
113+
return CommandFamily::Unknown;
114+
}
115+
116+
// Special handling for dollar-prefixed commands (two-character designators)
117+
if (name[0] == '$' && name.length() >= 2) {
118+
switch (name[1]) {
119+
case 'B': return CommandFamily::Backlash; // :$BAdd#, :$BZdd#
120+
case 'Q': return CommandFamily::SmartDrive; // :$Q#, :$QA+#, :$QZ-#
121+
default: return CommandFamily::Unknown;
122+
}
123+
}
124+
125+
// Standard single-character designators
126+
// Case-sensitive mapping (e.g., 'g' = GPS, 'G' = GetInfo)
127+
switch (name[0]) {
113128
case 'A': return CommandFamily::Alignment;
114129
case 'B': return CommandFamily::Reticle;
115130
case 'C': return CommandFamily::Sync;
@@ -148,6 +163,23 @@ void ParserState::parse_command_parts(std::string_view& name, std::string_view&
148163
return;
149164
}
150165

166+
// Special handling for dollar-prefixed commands
167+
// Pattern: :$BAdd# -> name="$BA", params="dd"
168+
// :$Q# -> name="$Q", params=""
169+
if (full_command[0] == '$' && full_command.length() >= 2) {
170+
if (full_command.length() > 3) {
171+
// $X + second char + third char + params
172+
// Examples: :$BAdd# -> "$BA" + "dd", :$QZ+# -> "$QZ" + "+"
173+
name = full_command.substr(0, 3);
174+
params = full_command.substr(3);
175+
} else {
176+
// Just :$XY# with no parameters
177+
name = full_command;
178+
params = std::string_view{};
179+
}
180+
return;
181+
}
182+
151183
// Lookup table: command families that take parameters after 2-char name
152184
// This replaces the if-else chain for better maintainability
153185
static constexpr char PARAMETER_FAMILIES[] = {

tests/lib/lx200/src/test_commands.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,90 @@ ZTEST(lx200, test_reticle_commands)
7878
zassert_equal(cmd->family, CommandFamily::Reticle, "B3 should be Reticle family");
7979
}
8080

81+
/**
82+
* @brief Test Backlash command family ($B)
83+
*
84+
* According to LX200CommandSet.md Section $B:
85+
* - :$BAdd# - Set Altitude/Dec Antibacklash [LX200GPS]
86+
* - :$BZdd# - Set Azimuth/RA Antibacklash [LX200GPS]
87+
*
88+
* These are special two-character designator commands starting with '$'.
89+
*/
90+
ZTEST(lx200, test_backlash_commands)
91+
{
92+
ParserState parser;
93+
94+
// :$BA15# - Set Altitude/Dec antibacklash to 15
95+
for (const char c : std::string_view(":$BA15#")) {
96+
parser.feed_character(c);
97+
}
98+
auto cmd = parser.get_command();
99+
zassert_true(cmd.has_value(), "Should parse $BA command");
100+
zassert_equal(cmd->family, CommandFamily::Backlash, "$BA should be Backlash family");
101+
zassert_equal(std::string_view(cmd->name), "$BA", "Command name should be $BA");
102+
zassert_equal(std::string_view(cmd->parameters), "15", "Parameters should be 15");
103+
104+
// :$BZ20# - Set Azimuth/RA antibacklash to 20
105+
parser.reset();
106+
for (const char c : std::string_view(":$BZ20#")) {
107+
parser.feed_character(c);
108+
}
109+
cmd = parser.get_command();
110+
zassert_true(cmd.has_value(), "Should parse $BZ command");
111+
zassert_equal(cmd->family, CommandFamily::Backlash, "$BZ should be Backlash family");
112+
zassert_equal(std::string_view(cmd->name), "$BZ", "Command name should be $BZ");
113+
zassert_equal(std::string_view(cmd->parameters), "20", "Parameters should be 20");
114+
}
115+
116+
/**
117+
* @brief Test SmartDrive command family ($Q)
118+
*
119+
* According to LX200CommandSet.md Section $Q:
120+
* - :$Q# - Toggle Smart Drive PEC on/off for both axes
121+
* - :$QA+# - Enable Dec/Alt PEC [LX200GPS]
122+
* - :$QA-# - Disable Dec/Alt PEC [LX200GPS]
123+
* - :$QZ+# - Enable RA/AZ PEC [LX200GPS]
124+
* - :$QZ-# - Disable RA/AZ PEC [LX200GPS]
125+
*
126+
* These are special two-character designator commands starting with '$'.
127+
*/
128+
ZTEST(lx200, test_smartdrive_commands)
129+
{
130+
ParserState parser;
131+
132+
// :$Q# - Toggle Smart Drive PEC (no parameters)
133+
for (const char c : std::string_view(":$Q#")) {
134+
parser.feed_character(c);
135+
}
136+
auto cmd = parser.get_command();
137+
zassert_true(cmd.has_value(), "Should parse $Q command");
138+
zassert_equal(cmd->family, CommandFamily::SmartDrive, "$Q should be SmartDrive family");
139+
zassert_equal(std::string_view(cmd->name), "$Q", "Command name should be $Q");
140+
zassert_true(cmd->parameters.empty(), "Parameters should be empty");
141+
142+
// :$QA+# - Enable Dec PEC
143+
parser.reset();
144+
for (const char c : std::string_view(":$QA+#")) {
145+
parser.feed_character(c);
146+
}
147+
cmd = parser.get_command();
148+
zassert_true(cmd.has_value(), "Should parse $QA command");
149+
zassert_equal(cmd->family, CommandFamily::SmartDrive, "$QA should be SmartDrive family");
150+
zassert_equal(std::string_view(cmd->name), "$QA", "Command name should be $QA");
151+
zassert_equal(std::string_view(cmd->parameters), "+", "Parameters should be +");
152+
153+
// :$QZ-# - Disable RA PEC
154+
parser.reset();
155+
for (const char c : std::string_view(":$QZ-#")) {
156+
parser.feed_character(c);
157+
}
158+
cmd = parser.get_command();
159+
zassert_true(cmd.has_value(), "Should parse $QZ command");
160+
zassert_equal(cmd->family, CommandFamily::SmartDrive, "$QZ should be SmartDrive family");
161+
zassert_equal(std::string_view(cmd->name), "$QZ", "Command name should be $QZ");
162+
zassert_equal(std::string_view(cmd->parameters), "-", "Parameters should be -");
163+
}
164+
81165
/**
82166
* @brief Test Sync command family (C)
83167
*

0 commit comments

Comments
 (0)