diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..940f229 --- /dev/null +++ b/.clang-format @@ -0,0 +1,114 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Note: The list of ForEachMacros can be obtained using: +# +# git grep -h '^#define [^[:space:]]*FOR_EACH[^[:space:]]*(' include/ \ +# | sed "s,^#define \([^[:space:]]*FOR_EACH[^[:space:]]*\)(.*$, - '\1'," \ +# | sort | uniq +# +# References: +# - https://clang.llvm.org/docs/ClangFormatStyleOptions.html + +--- +BasedOnStyle: LLVM +AlignConsecutiveMacros: AcrossComments +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +AttributeMacros: + - __aligned + - __deprecated + - __packed + - __printf_like + - __syscall + - __syscall_always_inline + - __subsystem +BitFieldColonSpacing: After +BreakBeforeBraces: Linux +ColumnLimit: 100 +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +ForEachMacros: + - 'ARRAY_FOR_EACH' + - 'ARRAY_FOR_EACH_PTR' + - 'FOR_EACH' + - 'FOR_EACH_FIXED_ARG' + - 'FOR_EACH_IDX' + - 'FOR_EACH_IDX_FIXED_ARG' + - 'FOR_EACH_NONEMPTY_TERM' + - 'FOR_EACH_FIXED_ARG_NONEMPTY_TERM' + - 'RB_FOR_EACH' + - 'RB_FOR_EACH_CONTAINER' + - 'SYS_DLIST_FOR_EACH_CONTAINER' + - 'SYS_DLIST_FOR_EACH_CONTAINER_SAFE' + - 'SYS_DLIST_FOR_EACH_NODE' + - 'SYS_DLIST_FOR_EACH_NODE_SAFE' + - 'SYS_SEM_LOCK' + - 'SYS_SFLIST_FOR_EACH_CONTAINER' + - 'SYS_SFLIST_FOR_EACH_CONTAINER_SAFE' + - 'SYS_SFLIST_FOR_EACH_NODE' + - 'SYS_SFLIST_FOR_EACH_NODE_SAFE' + - 'SYS_SLIST_FOR_EACH_CONTAINER' + - 'SYS_SLIST_FOR_EACH_CONTAINER_SAFE' + - 'SYS_SLIST_FOR_EACH_NODE' + - 'SYS_SLIST_FOR_EACH_NODE_SAFE' + - '_WAIT_Q_FOR_EACH' + - 'Z_FOR_EACH' + - 'Z_FOR_EACH_ENGINE' + - 'Z_FOR_EACH_EXEC' + - 'Z_FOR_EACH_FIXED_ARG' + - 'Z_FOR_EACH_FIXED_ARG_EXEC' + - 'Z_FOR_EACH_IDX' + - 'Z_FOR_EACH_IDX_EXEC' + - 'Z_FOR_EACH_IDX_FIXED_ARG' + - 'Z_FOR_EACH_IDX_FIXED_ARG_EXEC' + - 'Z_GENLIST_FOR_EACH_CONTAINER' + - 'Z_GENLIST_FOR_EACH_CONTAINER_SAFE' + - 'Z_GENLIST_FOR_EACH_NODE' + - 'Z_GENLIST_FOR_EACH_NODE_SAFE' + - 'STRUCT_SECTION_FOREACH' + - 'STRUCT_SECTION_FOREACH_ALTERNATE' + - 'TYPE_SECTION_FOREACH' + - 'K_SPINLOCK' + - 'COAP_RESOURCE_FOREACH' + - 'COAP_SERVICE_FOREACH' + - 'COAP_SERVICE_FOREACH_RESOURCE' + - 'HTTP_RESOURCE_FOREACH' + - 'HTTP_SERVER_CONTENT_TYPE_FOREACH' + - 'HTTP_SERVICE_FOREACH' + - 'HTTP_SERVICE_FOREACH_RESOURCE' + - 'I3C_BUS_FOR_EACH_I3CDEV' + - 'I3C_BUS_FOR_EACH_I2CDEV' +IfMacros: + - 'CHECKIF' +# Disabled for now, see bug https://github.com/zephyrproject-rtos/zephyr/issues/48520 +#IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^".*\.h"$' + Priority: 0 + - Regex: '^<(assert|complex|ctype|errno|fenv|float|inttypes|limits|locale|math|setjmp|signal|stdarg|stdbool|stddef|stdint|stdio|stdlib|string|tgmath|time|wchar|wctype)\.h>$' + Priority: 1 + - Regex: '^\$' + Priority: 2 + - Regex: '.*' + Priority: 3 +IndentCaseLabels: false +IndentGotoLabels: false +IndentWidth: 8 +InsertBraces: true +SpaceBeforeInheritanceColon: False +SpaceBeforeParens: ControlStatementsExceptControlMacros +SortIncludes: Never +UseTab: ForContinuationAndIndentation +WhitespaceSensitiveMacros: + - COND_CODE_0 + - COND_CODE_1 + - IF_DISABLED + - IF_ENABLED + - LISTIFY + - STRINGIFY + - Z_STRINGIFY + - DT_FOREACH_PROP_ELEM_SEP diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1e5da51 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,124 @@ +````instructions +# OpenAstroFirmware AI Agent Instructions + +This is firmware for DIY astronomical telescope mounts, built on Zephyr RTOS. It's a modern C++20 rewrite of the successful OpenAstroTracker v1 firmware, emphasizing maintainability and advanced features. + +## Architecture Overview + +**Build System**: Zephyr RTOS v4.1.0 with west build tool +**Target Hardware**: MKS Robin Nano (STM32F407-based) boards +**Languages**: C++20 primary, with C for low-level protocol implementations +**Structure**: Dual-app architecture - `app/` (main firmware) + `lib/` (reusable components) + +Key directories: +- `app/` - Main Zephyr application with mount control logic +- `lib/lx200/` - LX200 telescope protocol implementation (C) +- `boards/mks/robin_nano/` - Custom board definition and device tree +- `tests/` - Comprehensive test suites using Zephyr Twister +- `zephyr/module.yml` - Zephyr module configuration + +## Development Workflow + +### Building & Testing +```bash +west build -b robin_nano # Standard build +west build -b robin_nano -- -DEXTRA_CONF_FILE=debug.conf # Debug build +west twister -T . -p robin_nano # Run all tests +west twister -T tests/lib/lx200/ # Run specific test suite +``` + +### Configuration Layers +- **Kconfig**: `prj.conf` (base) + `debug.conf` (debug overlay) +- **Device Tree**: Custom STM32F407 board in `boards/mks/robin_nano/` +- **Build Configs**: Use `-DEXTRA_CONF_FILE=debug.conf` for development + +## Code Patterns & Conventions + +### Language Split Pattern +**C++20 Application Layer** (`app/src/`): +```cpp +#include +Mount mount; // Main astronomical mount controller +``` + +**C Protocol Layer** (`lib/lx200/`): +```c +lx200_parser_state_t parser_state; +lx200_parse_command_string(cmd_string, &command); +``` + +### Logging Infrastructure +Every module uses Zephyr structured logging: +```cpp +LOG_MODULE_REGISTER(ModuleName, CONFIG_APP_LOG_LEVEL); +// Available: LOG_DBG(), LOG_INF(), LOG_WRN(), LOG_ERR() +``` + +### LX200 Protocol Implementation +Commands follow `:COMMAND[params]#` format. Current implementation status: +- ✅ **Parser Framework**: Command parsing, family classification +- ⏳ **Coordinate Functions**: All `lx200_parse_*_coordinate()` are stubs +- ⏳ **Formatting Functions**: All `lx200_format_*()` are stubs + +Example of incomplete implementation pattern: +```c +lx200_parse_result_t lx200_parse_ra_coordinate(const char *str, lx200_coordinate_t *coord) { + LOG_WRN("Function not implemented yet"); + return LX200_PARSE_ERROR; // All coordinate functions are stubs +} +``` + +## Testing Strategy + +### Test-Driven Development Pattern +Tests serve as specifications for unimplemented features in `tests/lib/lx200/`: + +**Working Tests** (verify current functionality): +- Parser state management +- Command family identification (A,B,C,D,F,G,g,H,I,L,M,P,Q,R,S,T,U) +- Basic command/parameter separation + +**Specification Tests** (currently failing, define requirements): +- Coordinate parsing (RA: `HH:MM:SS`, Dec: `sDD*MM:SS`) +- Time/date parsing (`HH:MM:SS`, `MM/DD/YY`) +- Response formatting functions + +Run with: `west twister -T tests/lib/lx200/` + +### Configuration Testing +Two build configurations tested via `sample.yaml`: +- `app.default`: Production configuration +- `app.debug`: Debug configuration with `CONFIG_APP_LOG_LEVEL_DBG=y` + +## Project-Specific Context + +### Astronomical Domain Knowledge +- **Coordinate Systems**: RA/Dec (celestial), Alt/Az (horizontal) +- **Precision Modes**: High (`HH:MM:SS`) vs Low (`HH:MM.T`) coordinate formats +- **LX200 Compatibility**: Industry standard for telescope communication +- **Mount Types**: Equatorial tracking, Alt-azimuth positioning + +### Critical Implementation Details +**Parameter Detection Logic** (needs improvement): +```c +// Current: oversimplified - only checks 'S' commands +bool lx200_command_has_parameter(const char *command) { + return (command[0] == 'S'); // Missing many parameter commands +} +``` + +**Command Parsing Edge Cases**: +- Special commands: `B+`, `B-`, `F+`, `F-`, `T+`, `T-` +- Rate commands: `R0` through `R9` +- Library commands and GPS commands also take parameters + +### Development Status +Early development phase. Focus on: +1. Implementing coordinate parsing stubs in `lib/lx200/` +2. Following existing test specifications in `tests/lib/lx200/` +3. Using C for protocol layer, C++20 for application layer +4. Maintaining Zephyr module structure for reusability + +When implementing new features, always check corresponding tests first to understand expected behavior and formats. + +```` diff --git a/README.md b/README.md index 18d528f..736cfe0 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,10 @@ It was a very long and educational time developing, testing and improving [OpenA ### Built With -* [Zephyr]([https://platformio.org/](https://docs.zephyrproject.org/3.7.0/)) +* [Zephyr RTOS](https://docs.zephyrproject.org/) - Real-time operating system for connected, resource-constrained devices +* [West](https://docs.zephyrproject.org/latest/guides/west/index.html) - Zephyr's meta-tool for managing project repositories +* [CMake](https://cmake.org/) - Cross-platform build system +* C++ - Modern C++ for telescope mount control implementation

(back to top)

@@ -102,28 +105,94 @@ It was a very long and educational time developing, testing and improving [OpenA ## Getting Started -This is an example of how you may give instructions on setting up your project locally. -To get a local copy up and running follow these simple example steps. +To get started with OpenAstroFirmware development or to build and deploy the firmware for your telescope mount, follow these steps. The project supports both hardware deployment (MKS Robin Nano) and native simulation for development and testing. ### Supported hardware -*TBD* +- **MKS Robin Nano** (STM32F407xx-based controller board) - Production target +- **native_sim** - Development and testing platform +- STM32F407xx microcontroller family support +- Stepper motor drivers: + - TMC stepper motor controllers + - GPIO-based stepper motor drivers + - Testing/simulation drivers for development ### Prerequisites -*TBD* +- **Zephyr SDK** - Required for cross-compilation and board support +- **West** - Zephyr's meta-tool for project management and building +- **CMake** (version 3.20.0 or higher) - Build system +- **Ninja** - Fast build tool +- **Python 3** (with pip) - For build scripts and utilities +- **Git** - For version control and West manifest management + +#### Installation + +1. Install the Zephyr SDK following the [official Zephyr getting started guide](https://docs.zephyrproject.org/latest/getting_started/index.html) +2. Set up the Python virtual environment and install West: + ```bash + python3 -m venv ~/.venv + source ~/.venv/bin/activate + pip install west + ``` ### Configuration -*TBD* +The firmware uses Zephyr's Kconfig system for configuration. Key configuration files: + +- `app/prj.conf` - Main project configuration +- `app/boards/native_sim.conf` - Native simulator specific settings +- `app/boards/native_sim.overlay` - Device tree overlay for simulation +- Board-specific configurations in `boards/` directory + +Configuration can be modified using: +```bash +west build -t menuconfig # Interactive configuration menu +west build -t guiconfig # GUI configuration tool +``` ### Build -*TBD* +#### For Native Simulation (Development/Testing) +```bash +cd OpenAstroFirmware/app +west build -b native_sim +``` + +#### For MKS Robin Nano (Production Hardware) +```bash +cd OpenAstroFirmware/app +west build -b mks_robin_nano +``` + +#### Clean Build +```bash +west build -t pristine # Clean all build artifacts +west build -b # Rebuild from scratch +``` + +The build system supports: +- Cross-compilation for STM32F407xx targets +- Native compilation for PC-based testing +- Comprehensive testing with automated test suites ### Upload -*TBD* +#### Native Simulation +```bash +west build -t run # Run the firmware in simulation +``` + +#### Hardware Targets (MKS Robin Nano) +```bash +west flash # Flash firmware to connected hardware +west debug # Start debugging session +west attach # Attach debugger to running target +``` + +Additional upload options: +- `west build -t flash` - Alternative flash command +- Custom flash runners supported via Zephyr's runner system

(back to top)

@@ -132,7 +201,32 @@ To get a local copy up and running follow these simple example steps. ## Usage -*TBD* +OpenAstroFirmware provides telescope mount control with the following capabilities: + +### Core Features +- **LX200 Protocol Support** - Compatible with Meade LX200 command set for telescope control software +- **Stepper Motor Control** - Precise control of RA and DEC axes with multiple driver support +- **Mount Coordinate Management** - Right Ascension and Declination positioning +- **Cross-Platform Development** - Native simulation for testing without hardware + +### LX200 Command Interface +The firmware implements the standard LX200 protocol for communication with planetarium software like: +- SkySafari +- Stellarium +- TheSkyX +- Cartes du Ciel +- Any ASCOM-compatible software + +### Hardware Control +- Stepper motor drivers (TMC, GPIO-based) +- STM32F407xx microcontroller support +- Extensible driver architecture for additional hardware components + +### Development and Testing +- Native simulation support for PC-based development +- Comprehensive logging system +- Automated testing framework +- In-hardware debugging capabilities

(back to top)

@@ -141,10 +235,29 @@ To get a local copy up and running follow these simple example steps. ## Roadmap -- [ ] Build environment setup - - [ ] *TBD* -- [ ] MVP - - [ ] *TBD* +- [x] Build environment setup + - [x] Zephyr RTOS integration + - [x] West workspace configuration + - [x] Cross-compilation support + - [x] Native simulation target +- [x] Core Foundation + - [x] LX200 protocol library implementation + - [x] Mount coordinate management (RA/DEC) + - [x] Stepper motor driver framework + - [x] Board support for MKS Robin Nano + - [x] Logging and debugging infrastructure +- [ ] MVP (In Progress) + - [x] Basic mount control interface + - [ ] Complete LX200 command set implementation + - [ ] Motor calibration and homing + - [ ] Real-time telescope tracking + - [ ] Hardware-in-the-loop testing +- [ ] Future Enhancements + - [ ] Touch display interface + - [ ] Mobile app connectivity + - [ ] Custom object tracking (Sun, Moon, ISS, Comets) + - [ ] Advanced calibration procedures + - [ ] Extended mount type support See the [open issues](https://github.com/OpenAstroTech/OpenAstroFirmware/issues) for a full list of proposed features (and known issues). diff --git a/app/prj.conf b/app/prj.conf index 5ba2ef3..7326509 100644 --- a/app/prj.conf +++ b/app/prj.conf @@ -10,7 +10,7 @@ CONFIG_REQUIRES_FULL_LIBCPP=y CONFIG_GPIO=y # CONFIG_RING_BUFFER=y -# CONFIG_LX200=y +CONFIG_LX200=y # CONFIG_TIMING_FUNCTIONS=y CONFIG_MAIN_THREAD_PRIORITY=5 diff --git a/include/lx200/lx200.h b/include/lx200/lx200.h index 3df7372..277414a 100644 --- a/include/lx200/lx200.h +++ b/include/lx200/lx200.h @@ -30,7 +30,7 @@ * - Time: HH:MM:SS * - Date: MM/DD/YY * - * COMMAND CATEGORIES: + * COMMAND FAMILIES: * ================== * * A - ALIGNMENT COMMANDS @@ -216,7 +216,8 @@ * :Me# - Start slewing east at current slew rate * :Mw# - Start slewing west at current slew rate * :MS# - Slew to target coordinates - * Returns: 0# (slew possible) or 1# (object below horizon) or 2# (object below higher limit) + * Returns: 0# (slew possible) or 1# (object below horizon) or 2# (object below higher + * limit) * * APPENDIX A: LX200GPS COMMAND EXTENSIONS * ====================================== @@ -286,3 +287,516 @@ */ #pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @defgroup lx200_parser LX200 Protocol Parser + * @brief Parser and handler functions for the Meade LX200 telescope protocol + * @{ + */ + +/* ============================================================================ + * CONSTANTS AND DEFINITIONS + * ============================================================================ */ + +/** Maximum length of an LX200 command including terminator */ +#define LX200_MAX_COMMAND_LENGTH 32 + +/** Maximum length of an LX200 response including terminator */ +#define LX200_MAX_RESPONSE_LENGTH 64 + +/** LX200 command prefix character */ +#define LX200_COMMAND_PREFIX ':' + +/** LX200 command terminator character */ +#define LX200_COMMAND_TERMINATOR '#' + +/** LX200 response terminator character */ +#define LX200_RESPONSE_TERMINATOR '#' + +/* ============================================================================ + * ENUMERATIONS + * ============================================================================ */ + +/** + * @brief LX200 command parsing result codes + */ +typedef enum { + /** Command parsed successfully */ + LX200_PARSE_OK = 0, + /** Command is incomplete, need more data */ + LX200_PARSE_INCOMPLETE, + /** Invalid command prefix */ + LX200_PARSE_INVALID_PREFIX, + /** Invalid or missing terminator */ + LX200_PARSE_INVALID_TERMINATOR, + /** Unknown or malformed command */ + LX200_PARSE_INVALID_COMMAND, + /** Invalid parameter format */ + LX200_PARSE_INVALID_PARAMETER, + /** Command too long for buffer */ + LX200_PARSE_BUFFER_OVERFLOW, + /** General parsing error */ + LX200_PARSE_ERROR +} lx200_parse_result_t; + +/** + * @brief LX200 command families + */ +typedef enum { + /** Alignment commands (A) */ + LX200_CMD_ALIGNMENT, + /** Reticle/accessory control (B) */ + LX200_CMD_RETICLE, + /** Sync control (C) */ + LX200_CMD_SYNC, + /** Distance bars (D) */ + LX200_CMD_DISTANCE, + /** Focuser control (F) */ + LX200_CMD_FOCUSER, + /** Get telescope information (G) */ + LX200_CMD_GET, + /** GPS/magnetometer commands (g) */ + LX200_CMD_GPS, + /** Time format command (H) */ + LX200_CMD_TIME_FORMAT, + /** Initialize telescope (I) */ + LX200_CMD_INITIALIZE, + /** Object library commands (L) */ + LX200_CMD_LIBRARY, + /** Movement commands (M) */ + LX200_CMD_MOVE, + /** High precision toggle (P) */ + LX200_CMD_PRECISION, + /** Stop movement commands (Q) */ + LX200_CMD_STOP, + /** Slew rate commands (R) */ + LX200_CMD_SLEW_RATE, + /** Set telescope parameters (S) */ + LX200_CMD_SET, + /** Tracking commands (T) */ + LX200_CMD_TRACKING, + /** Precision toggle (U) */ + LX200_CMD_PRECISION_TOGGLE, + /** Unknown command family */ + LX200_CMD_UNKNOWN +} lx200_command_family_t; + +/** + * @brief LX200 coordinate formats + */ +typedef enum { + /** HH:MM.T, sDD*MM */ + LX200_COORD_LOW_PRECISION, + /** HH:MM:SS, sDD*MM:SS */ + LX200_COORD_HIGH_PRECISION +} lx200_precision_t; + +/** + * @brief LX200 alignment modes + */ +typedef enum { + /** Polar alignment mode */ + LX200_ALIGN_POLAR, + /** Alt-Az alignment mode */ + LX200_ALIGN_ALTAZ, + /** Land alignment mode */ + LX200_ALIGN_LAND +} lx200_alignment_mode_t; + +/** + * @brief LX200 tracking rates + */ +typedef enum { + /** Tracking disabled */ + LX200_TRACK_OFF, + /** Sidereal tracking rate */ + LX200_TRACK_SIDEREAL, + /** Solar tracking rate */ + LX200_TRACK_SOLAR, + /** Lunar tracking rate */ + LX200_TRACK_LUNAR +} lx200_tracking_rate_t; + +/** + * @brief LX200 slew rates + */ +typedef enum { + /** Guide rate (0.5x sidereal) */ + LX200_SLEW_GUIDE, + /** Centering rate (8x sidereal) */ + LX200_SLEW_CENTERING, + /** Find rate (16x sidereal) */ + LX200_SLEW_FIND, + /** Slew rate (512x sidereal) */ + LX200_SLEW_SLEW, + /** Custom rate 0 */ + LX200_SLEW_CUSTOM_0, + /** Custom rate 1 */ + LX200_SLEW_CUSTOM_1, + /** Custom rate 2 */ + LX200_SLEW_CUSTOM_2, + /** Custom rate 3 */ + LX200_SLEW_CUSTOM_3, + /** Custom rate 4 */ + LX200_SLEW_CUSTOM_4, + /** Custom rate 5 */ + LX200_SLEW_CUSTOM_5, + /** Custom rate 6 */ + LX200_SLEW_CUSTOM_6, + /** Custom rate 7 */ + LX200_SLEW_CUSTOM_7, + /** Custom rate 8 */ + LX200_SLEW_CUSTOM_8, + /** Custom rate 9 */ + LX200_SLEW_CUSTOM_9 +} lx200_slew_rate_t; + +/* ============================================================================ + * STRUCTURE DEFINITIONS + * ============================================================================ */ + +/** + * @brief LX200 coordinate structure + */ +typedef struct { + /** Degrees component */ + int16_t degrees; + /** Minutes component */ + uint8_t minutes; + /** Seconds component */ + uint8_t seconds; + /** Tenths of minutes (low precision mode) */ + uint8_t tenths; + /** True if coordinate is negative */ + bool is_negative; + /** Coordinate precision */ + lx200_precision_t precision; +} lx200_coordinate_t; + +/** + * @brief LX200 time structure + */ +typedef struct { + /** Hours (0-23) */ + uint8_t hours; + /** Minutes (0-59) */ + uint8_t minutes; + /** Seconds (0-59) */ + uint8_t seconds; + /** True for 24h format, false for 12h */ + bool is_24h_format; +} lx200_time_t; + +/** + * @brief LX200 date structure + */ +typedef struct { + /** Month (1-12) */ + uint8_t month; + /** Day (1-31) */ + uint8_t day; + /** Year (0-99, represents 2000-2099) */ + uint8_t year; +} lx200_date_t; + +/** + * @brief LX200 parsed command structure + */ +typedef struct { + /** Command family */ + lx200_command_family_t family; + /** Command string (up to 3 chars + null) */ + char command[4]; + /** Command parameter */ + char parameter[LX200_MAX_COMMAND_LENGTH]; + /** Length of parameter */ + size_t parameter_length; + /** True if command has parameter */ + bool has_parameter; +} lx200_command_t; + +/** + * @brief LX200 parser state structure + */ +typedef struct { + /** Input buffer */ + char buffer[LX200_MAX_COMMAND_LENGTH]; + /** Current buffer length */ + size_t buffer_length; + /** True when command is complete */ + bool command_complete; + /** Current precision mode */ + lx200_precision_t precision_mode; +} lx200_parser_state_t; + +/* ============================================================================ + * FUNCTION DECLARATIONS + * ============================================================================ */ + +/** + * @brief Initialize LX200 parser state + * @param state Pointer to parser state structure + */ +void lx200_parser_init(lx200_parser_state_t *state); + +/** + * @brief Reset LX200 parser state + * @param state Pointer to parser state structure + */ +void lx200_parser_reset(lx200_parser_state_t *state); + +/** + * @brief Add data to LX200 parser buffer + * @param state Pointer to parser state structure + * @param data Pointer to input data + * @param length Length of input data + * @return Parse result code + */ +lx200_parse_result_t lx200_parser_add_data(lx200_parser_state_t *state, const char *data, + size_t length); + +/** + * @brief Parse complete LX200 command + * @param state Pointer to parser state structure + * @param command Pointer to output command structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_command(const lx200_parser_state_t *state, + lx200_command_t *command); + +/** + * @brief Parse LX200 command from string + * @param cmd_string Command string to parse + * @param command Pointer to output command structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_command_string(const char *cmd_string, lx200_command_t *command); + +/* ============================================================================ + * COORDINATE PARSING FUNCTIONS + * ============================================================================ */ + +/** + * @brief Parse right ascension coordinate + * @param str Input string (HH:MM:SS or HH:MM.T format) + * @param coord Pointer to output coordinate structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_ra_coordinate(const char *str, lx200_coordinate_t *coord); + +/** + * @brief Parse declination coordinate + * @param str Input string (sDD*MM:SS or sDD*MM format) + * @param coord Pointer to output coordinate structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_dec_coordinate(const char *str, lx200_coordinate_t *coord); + +/** + * @brief Parse altitude coordinate + * @param str Input string (sDD*MM:SS or sDD*MM format) + * @param coord Pointer to output coordinate structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_alt_coordinate(const char *str, lx200_coordinate_t *coord); + +/** + * @brief Parse azimuth coordinate + * @param str Input string (DDD*MM:SS or DDD*MM format) + * @param coord Pointer to output coordinate structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_az_coordinate(const char *str, lx200_coordinate_t *coord); + +/** + * @brief Parse longitude coordinate + * @param str Input string (sDDD*MM format) + * @param coord Pointer to output coordinate structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_longitude(const char *str, lx200_coordinate_t *coord); + +/** + * @brief Parse latitude coordinate + * @param str Input string (sDD*MM format) + * @param coord Pointer to output coordinate structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_latitude(const char *str, lx200_coordinate_t *coord); + +/* ============================================================================ + * TIME AND DATE PARSING FUNCTIONS + * ============================================================================ */ + +/** + * @brief Parse time value + * @param str Input string (HH:MM:SS format) + * @param time Pointer to output time structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_time(const char *str, lx200_time_t *time); + +/** + * @brief Parse date value + * @param str Input string (MM/DD/YY format) + * @param date Pointer to output date structure + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_date(const char *str, lx200_date_t *date); + +/** + * @brief Parse UTC offset + * @param str Input string (sHH or sHH.H format) + * @param offset Pointer to output offset in hours + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_utc_offset(const char *str, float *offset); + +/* ============================================================================ + * PARAMETER PARSING FUNCTIONS + * ============================================================================ */ + +/** + * @brief Parse tracking rate parameter + * @param str Input string (TT.T format) + * @param rate Pointer to output tracking rate + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_tracking_rate(const char *str, float *rate); + +/** + * @brief Parse slew rate parameter + * @param str Input string (single digit 0-9) + * @param rate Pointer to output slew rate + * @return Parse result code + */ +lx200_parse_result_t lx200_parse_slew_rate(const char *str, lx200_slew_rate_t *rate); + +/* ============================================================================ + * FORMATTING FUNCTIONS + * ============================================================================ */ + +/** + * @brief Format right ascension coordinate to string + * @param coord Pointer to coordinate structure + * @param str Output string buffer + * @param str_size Size of output buffer + * @return Number of characters written, or negative on error + */ +int lx200_format_ra_coordinate(const lx200_coordinate_t *coord, char *str, size_t str_size); + +/** + * @brief Format declination coordinate to string + * @param coord Pointer to coordinate structure + * @param str Output string buffer + * @param str_size Size of output buffer + * @return Number of characters written, or negative on error + */ +int lx200_format_dec_coordinate(const lx200_coordinate_t *coord, char *str, size_t str_size); + +/** + * @brief Format time to string + * @param time Pointer to time structure + * @param str Output string buffer + * @param str_size Size of output buffer + * @return Number of characters written, or negative on error + */ +int lx200_format_time(const lx200_time_t *time, char *str, size_t str_size); + +/** + * @brief Format date to string + * @param date Pointer to date structure + * @param str Output string buffer + * @param str_size Size of output buffer + * @return Number of characters written, or negative on error + */ +int lx200_format_date(const lx200_date_t *date, char *str, size_t str_size); + +/* ============================================================================ + * VALIDATION FUNCTIONS + * ============================================================================ */ + +/** + * @brief Validate coordinate values + * @param coord Pointer to coordinate structure + * @param coord_type Type of coordinate (RA, Dec, Alt, Az, etc.) + * @return true if coordinate is valid, false otherwise + */ +bool lx200_validate_coordinate(const lx200_coordinate_t *coord, + lx200_command_family_t coord_type); + +/** + * @brief Validate time values + * @param time Pointer to time structure + * @return true if time is valid, false otherwise + */ +bool lx200_validate_time(const lx200_time_t *time); + +/** + * @brief Validate date values + * @param date Pointer to date structure + * @return true if date is valid, false otherwise + */ +bool lx200_validate_date(const lx200_date_t *date); + +/* ============================================================================ + * UTILITY FUNCTIONS + * ============================================================================ */ + +/** + * @brief Get command family from command string + * @param command Command string + * @return Command family + */ +lx200_command_family_t lx200_get_command_family(const char *command); + +/** + * @brief Check if command expects a parameter + * @param command Command string + * @return true if command expects parameter, false otherwise + */ +bool lx200_command_has_parameter(const char *command); + +/** + * @brief Get expected parameter format for command + * @param command Command string + * @return Pointer to format description string + */ +const char *lx200_get_parameter_format(const char *command); + +/** + * @brief Convert parse result to string + * @param result Parse result code + * @return Pointer to result description string + */ +const char *lx200_parse_result_to_string(lx200_parse_result_t result); + +/** + * @brief Set precision mode + * @param state Pointer to parser state structure + * @param precision Precision mode to set + */ +void lx200_set_precision_mode(lx200_parser_state_t *state, lx200_precision_t precision); + +/** + * @brief Get current precision mode + * @param state Pointer to parser state structure + * @return Current precision mode + */ +lx200_precision_t lx200_get_precision_mode(const lx200_parser_state_t *state); + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 78dfa65..a9f3669 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,3 +1,4 @@ # SPDX-License-Identifier: Apache-2.0 add_subdirectory_ifdef(CONFIG_CUSTOM custom) +add_subdirectory_ifdef(CONFIG_LX200 lx200) diff --git a/lib/lx200/CMakeLists.txt b/lib/lx200/CMakeLists.txt index 3224a92..f36a2b6 100644 --- a/lib/lx200/CMakeLists.txt +++ b/lib/lx200/CMakeLists.txt @@ -2,4 +2,5 @@ # SPDX-License-Identifier: Apache-2.0 zephyr_library() -zephyr_ \ No newline at end of file +zephyr_library_sources_ifdef(CONFIG_LX200 lx200.c) +zephyr_library_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) \ No newline at end of file diff --git a/lib/lx200/Kconfig b/lib/lx200/Kconfig index a2cf2da..0ba79c2 100644 --- a/lib/lx200/Kconfig +++ b/lib/lx200/Kconfig @@ -5,3 +5,11 @@ config LX200 bool "Support for lx200 protocol" help This option enables support for the lx200 protocol. + +if LX200 + +module = LX200 +module-str = lx200 +source "subsys/logging/Kconfig.template.log_config" + +endif # LX200 diff --git a/lib/lx200/lx200.c b/lib/lx200/lx200.c new file mode 100644 index 0000000..32c655e --- /dev/null +++ b/lib/lx200/lx200.c @@ -0,0 +1,561 @@ +/** + * @file lx200.c + * @brief LX200 Protocol Parser Implementation + * + * Copyright (c) 2025, OpenAstroTech + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +LOG_MODULE_REGISTER(lx200, CONFIG_LX200_LOG_LEVEL); + +/** + * @brief Initialize LX200 parser state + */ +void lx200_parser_init(lx200_parser_state_t *state) +{ + if (state == NULL) { + LOG_ERR("lx200_parser_init: NULL state pointer"); + return; + } + + memset(state->buffer, 0, sizeof(state->buffer)); + state->buffer_length = 0; + state->command_complete = false; + state->precision_mode = LX200_COORD_HIGH_PRECISION; + + LOG_DBG("LX200 parser initialized with high precision mode"); +} + +/** + * @brief Reset LX200 parser state + */ +void lx200_parser_reset(lx200_parser_state_t *state) +{ + if (state == NULL) { + LOG_ERR("lx200_parser_reset: NULL state pointer"); + return; + } + + LOG_DBG("Resetting LX200 parser state (buffer_length=%zu)", state->buffer_length); + + memset(state->buffer, 0, sizeof(state->buffer)); + state->buffer_length = 0; + state->command_complete = false; +} + +/** + * @brief Convert parse result to string + */ +const char *lx200_parse_result_to_string(lx200_parse_result_t result) +{ + switch (result) { + case LX200_PARSE_OK: + return "OK"; + case LX200_PARSE_INCOMPLETE: + return "Incomplete"; + case LX200_PARSE_INVALID_PREFIX: + return "Invalid prefix"; + case LX200_PARSE_INVALID_TERMINATOR: + return "Invalid terminator"; + case LX200_PARSE_INVALID_COMMAND: + return "Invalid command"; + case LX200_PARSE_INVALID_PARAMETER: + return "Invalid parameter"; + case LX200_PARSE_BUFFER_OVERFLOW: + return "Buffer overflow"; + case LX200_PARSE_ERROR: + default: + return "Parse error"; + } +} + +/** + * @brief Set precision mode + */ +void lx200_set_precision_mode(lx200_parser_state_t *state, lx200_precision_t precision) +{ + if (state == NULL) { + LOG_ERR("lx200_set_precision_mode: NULL state pointer"); + return; + } + + LOG_INF("Changing precision mode from %d to %d", state->precision_mode, precision); + state->precision_mode = precision; +} + +/** + * @brief Get current precision mode + */ +lx200_precision_t lx200_get_precision_mode(const lx200_parser_state_t *state) +{ + if (state == NULL) { + return LX200_COORD_HIGH_PRECISION; + } + + return state->precision_mode; +} + +/** + * @brief Get command family from command string + */ +lx200_command_family_t lx200_get_command_family(const char *command) +{ + if (command == NULL || command[0] == '\0') { + LOG_ERR("lx200_get_command_family: Invalid command string"); + return LX200_CMD_UNKNOWN; + } + + lx200_command_family_t family; + + switch (command[0]) { + case 'A': + family = LX200_CMD_ALIGNMENT; + break; + case 'B': + family = LX200_CMD_RETICLE; + break; + case 'C': + family = LX200_CMD_SYNC; + break; + case 'D': + family = LX200_CMD_DISTANCE; + break; + case 'F': + family = LX200_CMD_FOCUSER; + break; + case 'G': + family = LX200_CMD_GET; + break; + case 'g': + family = LX200_CMD_GPS; + break; + case 'H': + family = LX200_CMD_TIME_FORMAT; + break; + case 'I': + family = LX200_CMD_INITIALIZE; + break; + case 'L': + family = LX200_CMD_LIBRARY; + break; + case 'M': + family = LX200_CMD_MOVE; + break; + case 'P': + family = LX200_CMD_PRECISION; + break; + case 'Q': + family = LX200_CMD_STOP; + break; + case 'R': + family = LX200_CMD_SLEW_RATE; + break; + case 'S': + family = LX200_CMD_SET; + break; + case 'T': + family = LX200_CMD_TRACKING; + break; + case 'U': + family = LX200_CMD_PRECISION_TOGGLE; + break; + default: + family = LX200_CMD_UNKNOWN; + LOG_WRN("Unknown command family for command '%s' (first char: '%c')", command, + command[0]); + break; + } + + if (family != LX200_CMD_UNKNOWN) { + LOG_DBG("Command '%s' mapped to family %d", command, family); + } + + return family; +} + +/** + * @brief Check if command expects a parameter + */ +bool lx200_command_has_parameter(const char *command) +{ + if (command == NULL || command[0] == '\0') { + return false; + } + + // Commands that typically have parameters start with 'S' (Set commands) + // This is a simplified implementation - in practice, you'd need a lookup table + return (command[0] == 'S'); +} + +/** + * @brief Get expected parameter format for command + */ +const char *lx200_get_parameter_format(const char *command) +{ + if (command == NULL || command[0] == '\0') { + return "None"; + } + + // This is a simplified implementation + // In practice, you'd need a comprehensive lookup table + switch (command[0]) { + case 'S': + if (command[1] == 'r') { + return "HH:MM:SS"; + } + if (command[1] == 'd') { + return "sDD*MM:SS"; + } + if (command[1] == 'L') { + return "HH:MM:SS"; + } + if (command[1] == 'C') { + return "MM/DD/YY"; + } + return "Various"; + default: + return "None"; + } +} + +/** + * @brief Add data to LX200 parser buffer + */ +lx200_parse_result_t lx200_parser_add_data(lx200_parser_state_t *state, const char *data, + size_t length) +{ + if (state == NULL || data == NULL || length == 0) { + LOG_ERR("lx200_parser_add_data: Invalid parameters (state=%p, data=%p, length=%zu)", + state, data, length); + return LX200_PARSE_ERROR; + } + + LOG_DBG("Adding %zu bytes to parser buffer (current length: %zu)", length, + state->buffer_length); + + // Check for buffer overflow + if (state->buffer_length + length >= LX200_MAX_COMMAND_LENGTH) { + LOG_ERR("Buffer overflow: current=%zu, adding=%zu, max=%d", state->buffer_length, + length, LX200_MAX_COMMAND_LENGTH); + return LX200_PARSE_BUFFER_OVERFLOW; + } + + // Copy data to buffer + memcpy(&state->buffer[state->buffer_length], data, length); + state->buffer_length += length; + state->buffer[state->buffer_length] = '\0'; + + LOG_HEXDUMP_DBG(data, length, "Received data:"); + + // Check if command is complete (ends with #) + if (state->buffer_length > 0 && + state->buffer[state->buffer_length - 1] == LX200_COMMAND_TERMINATOR) { + state->command_complete = true; + LOG_DBG("Command complete: '%s'", state->buffer); + return LX200_PARSE_OK; + } + + LOG_DBG("Command incomplete, buffer: '%s'", state->buffer); + return LX200_PARSE_INCOMPLETE; +} + +/** + * @brief Parse complete LX200 command + */ +lx200_parse_result_t lx200_parse_command(const lx200_parser_state_t *state, + lx200_command_t *command) +{ + if (state == NULL || command == NULL) { + LOG_ERR("lx200_parse_command: Invalid parameters (state=%p, command=%p)", state, + command); + return LX200_PARSE_ERROR; + } + + if (!state->command_complete) { + LOG_DBG("Command not complete yet, current buffer: '%s'", state->buffer); + return LX200_PARSE_INCOMPLETE; + } + + LOG_DBG("Parsing complete command: '%s'", state->buffer); + return lx200_parse_command_string(state->buffer, command); +} + +/** + * @brief Parse LX200 command from string + */ +lx200_parse_result_t lx200_parse_command_string(const char *cmd_string, lx200_command_t *command) +{ + if (cmd_string == NULL || command == NULL) { + LOG_ERR("lx200_parse_command_string: Invalid parameters (cmd_string=%p, " + "command=%p)", + cmd_string, command); + return LX200_PARSE_ERROR; + } + + size_t len = strlen(cmd_string); + LOG_DBG("Parsing command string: '%s' (length: %zu)", cmd_string, len); + + if (len < 2) { + LOG_ERR("Command too short: %zu bytes", len); + return LX200_PARSE_INVALID_COMMAND; + } + + // Check for valid prefix + if (cmd_string[0] != LX200_COMMAND_PREFIX) { + LOG_ERR("Invalid command prefix: expected '%c', got '%c'", LX200_COMMAND_PREFIX, + cmd_string[0]); + return LX200_PARSE_INVALID_PREFIX; + } + + // Check for valid terminator + if (cmd_string[len - 1] != LX200_COMMAND_TERMINATOR) { + LOG_ERR("Invalid command terminator: expected '%c', got '%c'", + LX200_COMMAND_TERMINATOR, cmd_string[len - 1]); + return LX200_PARSE_INVALID_TERMINATOR; + } + + // Extract command (skip prefix, stop at terminator or parameter start) + size_t cmd_len = 0; + for (size_t i = 1; i < len - 1 && cmd_len < 3; i++) { + char c = cmd_string[i]; + // Include alphabetic characters + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + command->command[cmd_len++] = c; + } else if (cmd_len > 0) { + // Check if this might be a symbol that's part of the command + // Only for specific command families that use symbol suffixes + char first_char = command->command[0]; + if ((first_char == 'B' || first_char == 'F' || first_char == 'T') && + (c == '+' || c == '-')) { + // These commands use +/- as part of the command + command->command[cmd_len++] = c; + } else if (first_char == 'R' && (c >= '0' && c <= '9')) { + // Rate commands can have numbers + command->command[cmd_len++] = c; + } else { + // Stop here - this starts the parameter + break; + } + } else { + // Stop at first character that's not part of a command + break; + } + } + command->command[cmd_len] = '\0'; + + LOG_DBG("Extracted command: '%s'", command->command); + + // Get command family + command->family = lx200_get_command_family(command->command); + LOG_DBG("Command family: %d", command->family); + + // Extract parameter if present + size_t param_start = 1 + cmd_len; + if (param_start < len - 1) { + command->has_parameter = true; + command->parameter_length = len - 1 - param_start; + if (command->parameter_length < LX200_MAX_COMMAND_LENGTH) { + memcpy(command->parameter, &cmd_string[param_start], + command->parameter_length); + command->parameter[command->parameter_length] = '\0'; + LOG_DBG("Extracted parameter: '%s' (length: %zu)", command->parameter, + command->parameter_length); + } else { + LOG_ERR("Parameter too long: %zu bytes", command->parameter_length); + return LX200_PARSE_BUFFER_OVERFLOW; + } + } else { + command->has_parameter = false; + command->parameter_length = 0; + command->parameter[0] = '\0'; + LOG_DBG("No parameter present"); + } + + LOG_INF("Successfully parsed LX200 command: '%s'%s%s", command->command, + command->has_parameter ? " with parameter: '" : "", + command->has_parameter ? command->parameter : ""); + + return LX200_PARSE_OK; +} + +// Placeholder implementations for the remaining functions +// These would need to be fully implemented based on the LX200 protocol specification + +lx200_parse_result_t lx200_parse_ra_coordinate(const char *str, lx200_coordinate_t *coord) +{ + LOG_WRN("lx200_parse_ra_coordinate: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement RA coordinate parsing + ARG_UNUSED(str); + ARG_UNUSED(coord); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_dec_coordinate(const char *str, lx200_coordinate_t *coord) +{ + LOG_WRN("lx200_parse_dec_coordinate: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement Dec coordinate parsing + ARG_UNUSED(str); + ARG_UNUSED(coord); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_alt_coordinate(const char *str, lx200_coordinate_t *coord) +{ + LOG_WRN("lx200_parse_alt_coordinate: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement Alt coordinate parsing + ARG_UNUSED(str); + ARG_UNUSED(coord); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_az_coordinate(const char *str, lx200_coordinate_t *coord) +{ + LOG_WRN("lx200_parse_az_coordinate: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement Az coordinate parsing + ARG_UNUSED(str); + ARG_UNUSED(coord); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_longitude(const char *str, lx200_coordinate_t *coord) +{ + LOG_WRN("lx200_parse_longitude: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement longitude parsing + ARG_UNUSED(str); + ARG_UNUSED(coord); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_latitude(const char *str, lx200_coordinate_t *coord) +{ + LOG_WRN("lx200_parse_latitude: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement latitude parsing + ARG_UNUSED(str); + ARG_UNUSED(coord); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_time(const char *str, lx200_time_t *time) +{ + LOG_WRN("lx200_parse_time: Function not implemented yet (str='%s')", str ? str : "NULL"); + // TODO: Implement time parsing + ARG_UNUSED(str); + ARG_UNUSED(time); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_date(const char *str, lx200_date_t *date) +{ + LOG_WRN("lx200_parse_date: Function not implemented yet (str='%s')", str ? str : "NULL"); + // TODO: Implement date parsing + ARG_UNUSED(str); + ARG_UNUSED(date); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_utc_offset(const char *str, float *offset) +{ + LOG_WRN("lx200_parse_utc_offset: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement UTC offset parsing + ARG_UNUSED(str); + ARG_UNUSED(offset); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_tracking_rate(const char *str, float *rate) +{ + LOG_WRN("lx200_parse_tracking_rate: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement tracking rate parsing + ARG_UNUSED(str); + ARG_UNUSED(rate); + return LX200_PARSE_ERROR; +} + +lx200_parse_result_t lx200_parse_slew_rate(const char *str, lx200_slew_rate_t *rate) +{ + LOG_WRN("lx200_parse_slew_rate: Function not implemented yet (str='%s')", + str ? str : "NULL"); + // TODO: Implement slew rate parsing + ARG_UNUSED(str); + ARG_UNUSED(rate); + return LX200_PARSE_ERROR; +} + +int lx200_format_ra_coordinate(const lx200_coordinate_t *coord, char *str, size_t str_size) +{ + LOG_WRN("lx200_format_ra_coordinate: Function not implemented yet"); + // TODO: Implement RA coordinate formatting + ARG_UNUSED(coord); + ARG_UNUSED(str); + ARG_UNUSED(str_size); + return -1; +} + +int lx200_format_dec_coordinate(const lx200_coordinate_t *coord, char *str, size_t str_size) +{ + LOG_WRN("lx200_format_dec_coordinate: Function not implemented yet"); + // TODO: Implement Dec coordinate formatting + ARG_UNUSED(coord); + ARG_UNUSED(str); + ARG_UNUSED(str_size); + return -1; +} + +int lx200_format_time(const lx200_time_t *time, char *str, size_t str_size) +{ + LOG_WRN("lx200_format_time: Function not implemented yet"); + // TODO: Implement time formatting + ARG_UNUSED(time); + ARG_UNUSED(str); + ARG_UNUSED(str_size); + return -1; +} + +int lx200_format_date(const lx200_date_t *date, char *str, size_t str_size) +{ + LOG_WRN("lx200_format_date: Function not implemented yet"); + // TODO: Implement date formatting + ARG_UNUSED(date); + ARG_UNUSED(str); + ARG_UNUSED(str_size); + return -1; +} + +bool lx200_validate_coordinate(const lx200_coordinate_t *coord, lx200_command_family_t coord_type) +{ + LOG_WRN("lx200_validate_coordinate: Function not implemented yet (coord_type=%d)", + coord_type); + // TODO: Implement coordinate validation + ARG_UNUSED(coord); + ARG_UNUSED(coord_type); + return false; +} + +bool lx200_validate_time(const lx200_time_t *time) +{ + LOG_WRN("lx200_validate_time: Function not implemented yet"); + // TODO: Implement time validation + ARG_UNUSED(time); + return false; +} + +bool lx200_validate_date(const lx200_date_t *date) +{ + LOG_WRN("lx200_validate_date: Function not implemented yet"); + // TODO: Implement date validation + ARG_UNUSED(date); + return false; +} diff --git a/tests/lib/lx200/CMakeLists.txt b/tests/lib/lx200/CMakeLists.txt index 3942740..07f3139 100644 --- a/tests/lib/lx200/CMakeLists.txt +++ b/tests/lib/lx200/CMakeLists.txt @@ -1,8 +1,12 @@ -# Copyright (c) 2021, Legrand North America, LLC. +# Copyright (c) 2025, OpenAstroTech # SPDX-License-Identifier: Apache-2.0 cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) -project(app_lib_custom_test) +project(lx200_lib_test) -target_sources(app PRIVATE src/main.c) +# Include the LX200 library test sources +target_sources(app PRIVATE + src/main.c + src/test_coordinates.c +) diff --git a/tests/lib/lx200/README.md b/tests/lib/lx200/README.md new file mode 100644 index 0000000..75acaa2 --- /dev/null +++ b/tests/lib/lx200/README.md @@ -0,0 +1,166 @@ +# LX200 Library Test Suite + +This directory contains comprehensive tests for the LX200 telescope protocol library. + +## Test Structure + +### `src/main.c` +Contains the main test suite covering: + +- **Parser Initialization and State Tests**: Testing parser initialization, reset, and precision mode management +- **Command Parsing Tests**: Testing various LX200 command formats and parameter extraction +- **Command Family and Utility Tests**: Testing command classification and utility functions +- **Complex Command Parsing Tests**: Testing edge cases and complex command scenarios +- **Error Handling Tests**: Testing malformed commands and error conditions + +### `src/test_coordinates.c` +Contains test specifications for coordinate parsing functions that are currently unimplemented: + +- **Coordinate Parsing Tests**: RA, Dec, Alt/Az, longitude/latitude parsing +- **Time and Date Parsing Tests**: Time and date format parsing +- **Rate Parsing Tests**: Tracking and slew rate parsing +- **Formatting Tests**: Coordinate and time formatting functions +- **Validation Tests**: Input validation functions + +## Test Categories + +### Implemented Functions (Passing Tests) +These tests verify the currently working functionality: + +- ✅ Parser state management +- ✅ Command string parsing +- ✅ Command family identification +- ✅ Parameter extraction +- ✅ Error handling for malformed commands +- ✅ Buffer management and overflow protection + +### Unimplemented Functions (Specification Tests) +These tests currently fail as expected but serve as specifications for future implementation: + +- ⏳ Coordinate parsing (RA, Dec, Alt/Az) +- ⏳ Geographic coordinate parsing (longitude, latitude) +- ⏳ Time and date parsing +- ⏳ Rate parsing (tracking, slew) +- ⏳ Coordinate and time formatting +- ⏳ Input validation functions + +## Running the Tests + +### Prerequisites +- Zephyr RTOS development environment +- LX200 library properly configured in the build system +- Zephyr environment sourced (`source ~/zephyrproject/zephyr/zephyr-env.sh`) + +### Build and Run with Twister + +The recommended way to run these tests is using Zephyr's Twister test framework: + +```bash +# From the test directory +cd tests/lib/lx200 +west twister -T . -p native_sim + +# Or use the provided script +./run_tests.sh + +# Run with verbose output +west twister -T . -p native_sim --verbose + +# Run with coverage analysis +west twister -T . -p native_sim --coverage + +# Run on different platforms +west twister -T . -p qemu_x86 +west twister -T . -p qemu_cortex_m3 + +# Run only LX200 tagged tests (useful in larger projects) +west twister -T . --tag lx200 +``` + +### Alternative: Manual Build and Run +```bash +# From the OpenAstroFirmware root directory +west build -p auto -b native_sim tests/lib/lx200 +west build -t run +``` + +### Expected Output +The test suite will show: +- All parser and utility tests passing (green) +- All coordinate/formatting tests failing as expected (showing "Function not implemented yet") + +## Test Coverage + +### Currently Tested Functions +- `lx200_parser_init()` +- `lx200_parser_reset()` +- `lx200_parser_add_data()` +- `lx200_parse_command()` +- `lx200_parse_command_string()` +- `lx200_get_command_family()` +- `lx200_command_has_parameter()` +- `lx200_get_parameter_format()` +- `lx200_parse_result_to_string()` +- `lx200_set_precision_mode()` +- `lx200_get_precision_mode()` + +### Functions Needing Implementation +- All coordinate parsing functions (`lx200_parse_*_coordinate()`) +- All formatting functions (`lx200_format_*()`) +- All validation functions (`lx200_validate_*()`) +- Time/date/rate parsing functions + +## Test Scenarios Covered + +1. **Valid Commands**: Standard LX200 commands with and without parameters +2. **Invalid Commands**: Malformed commands, wrong prefixes/terminators +3. **Boundary Conditions**: Maximum length commands, empty inputs +4. **Error Handling**: NULL pointers, buffer overflows +5. **State Management**: Parser state transitions +6. **Parameter Extraction**: Various parameter formats and special characters + +## Adding New Tests + +When implementing new functions: + +1. Move the corresponding test from `test_coordinates.c` to `main.c` +2. Update the test to verify actual functionality instead of "not implemented" +3. Add additional test cases for edge cases and error conditions +4. Update this README to reflect the new test coverage + +## LX200 Protocol Coverage + +The tests cover the major LX200 command families: + +- **A**: Alignment commands +- **B**: Reticle/accessory control +- **C**: Sync control +- **D**: Distance bars +- **F**: Focuser control +- **G**: Get telescope information +- **g**: GPS/magnetometer commands +- **H**: Time format command +- **I**: Initialize telescope +- **L**: Object library commands +- **M**: Movement commands +- **P**: High precision toggle +- **Q**: Movement stop commands +- **R**: Slew rate commands +- **S**: Set telescope parameters +- **T**: Tracking commands +- **U**: Precision toggle + +## Known Limitations + +1. Some complex parameter formats are simplified in the current implementation +2. Coordinate validation rules need to be implemented +3. Time zone handling is not yet implemented +4. Some LX200GPS extensions are not covered + +## Future Enhancements + +1. Add performance benchmarking tests +2. Add stress testing with rapid command sequences +3. Add tests for concurrent parser usage +4. Add integration tests with mock telescope hardware +5. Add fuzzing tests for robustness diff --git a/tests/lib/lx200/prj.conf b/tests/lib/lx200/prj.conf index b262fa2..9a4b9c3 100644 --- a/tests/lib/lx200/prj.conf +++ b/tests/lib/lx200/prj.conf @@ -1,2 +1,13 @@ CONFIG_ZTEST=y CONFIG_LX200=y + +# Enable logging for test debugging +CONFIG_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LX200_LOG_LEVEL_DBG=y + +# Enable assertions +CONFIG_ASSERT=y + +# Increase main stack size for complex tests +CONFIG_MAIN_STACK_SIZE=2048 diff --git a/tests/lib/lx200/src/main.c b/tests/lib/lx200/src/main.c index a521bcf..307b961 100644 --- a/tests/lib/lx200/src/main.c +++ b/tests/lib/lx200/src/main.c @@ -1,25 +1,570 @@ -/* - * Copyright (c) 2021 Legrand North America, LLC. +/** + * @file main.c + * @brief LX200 Library Test Suite * + * Copyright (c) 2025, OpenAstroTech * SPDX-License-Identifier: Apache-2.0 */ -/* - * @file test custom_lib library - * - * This suite verifies that the methods provided with the custom_lib - * library works correctly. +#include +#include +#include + +/* Test helper macros */ +#define ASSERT_PARSE_OK(result) zassert_equal(result, LX200_PARSE_OK, "Parse should succeed") +#define ASSERT_PARSE_ERROR(result, expected) zassert_equal(result, expected, "Expected specific parse error") + +/* Test fixtures */ +static lx200_parser_state_t parser_state; +static lx200_command_t command; + +/** + * @brief Setup function called before each test */ +static void lx200_test_setup(void *fixture) +{ + ARG_UNUSED(fixture); + lx200_parser_init(&parser_state); + memset(&command, 0, sizeof(command)); +} -#include +/** + * @brief Teardown function called after each test + */ +static void lx200_test_teardown(void *fixture) +{ + ARG_UNUSED(fixture); + lx200_parser_reset(&parser_state); +} -#include +/* ============================================================================ + * PARSER INITIALIZATION AND STATE TESTS + * ============================================================================ */ -#include +ZTEST(lx200_parser, test_parser_init_valid) +{ + lx200_parser_state_t state; + + lx200_parser_init(&state); + + zassert_equal(state.buffer_length, 0, "Buffer length should be 0"); + zassert_false(state.command_complete, "Command should not be complete"); + zassert_equal(state.precision_mode, LX200_COORD_HIGH_PRECISION, + "Should default to high precision"); + zassert_equal(state.buffer[0], '\0', "Buffer should be null-terminated"); +} + +ZTEST(lx200_parser, test_parser_init_null) +{ + /* Should not crash with NULL pointer */ + lx200_parser_init(NULL); + zassert_true(true, "Should handle NULL gracefully"); +} + +ZTEST(lx200_parser, test_parser_reset) +{ + /* Setup some state first */ + strcpy(parser_state.buffer, ":Gr"); + parser_state.buffer_length = 3; + parser_state.command_complete = true; + + lx200_parser_reset(&parser_state); + + zassert_equal(parser_state.buffer_length, 0, "Buffer length should be 0"); + zassert_false(parser_state.command_complete, "Command should not be complete"); + zassert_equal(parser_state.buffer[0], '\0', "Buffer should be null-terminated"); +} + +ZTEST(lx200_parser, test_precision_mode_functions) +{ + /* Test getting current precision */ + lx200_precision_t precision = lx200_get_precision_mode(&parser_state); + zassert_equal(precision, LX200_COORD_HIGH_PRECISION, "Should be high precision"); + + /* Test setting precision */ + lx200_set_precision_mode(&parser_state, LX200_COORD_LOW_PRECISION); + precision = lx200_get_precision_mode(&parser_state); + zassert_equal(precision, LX200_COORD_LOW_PRECISION, "Should be low precision"); + + /* Test NULL handling */ + precision = lx200_get_precision_mode(NULL); + zassert_equal(precision, LX200_COORD_HIGH_PRECISION, "Should default to high precision"); + + lx200_set_precision_mode(NULL, LX200_COORD_LOW_PRECISION); + /* Should not crash */ + zassert_true(true, "Should handle NULL gracefully"); +} + +/* ============================================================================ + * COMMAND PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_parser, test_add_data_valid_complete_command) +{ + const char *cmd = ":Gr#"; + lx200_parse_result_t result; + + result = lx200_parser_add_data(&parser_state, cmd, strlen(cmd)); + + ASSERT_PARSE_OK(result); + zassert_true(parser_state.command_complete, "Command should be complete"); + zassert_equal(parser_state.buffer_length, strlen(cmd), "Buffer length should match"); + zassert_mem_equal(parser_state.buffer, cmd, strlen(cmd), "Buffer should contain command"); +} + +ZTEST(lx200_parser, test_add_data_incomplete_command) +{ + const char *cmd = ":Gr"; + lx200_parse_result_t result; + + result = lx200_parser_add_data(&parser_state, cmd, strlen(cmd)); + + ASSERT_PARSE_ERROR(result, LX200_PARSE_INCOMPLETE); + zassert_false(parser_state.command_complete, "Command should not be complete"); + zassert_equal(parser_state.buffer_length, strlen(cmd), "Buffer length should match"); +} + +ZTEST(lx200_parser, test_add_data_incremental) +{ + lx200_parse_result_t result; + + /* Add data incrementally */ + result = lx200_parser_add_data(&parser_state, ":", 1); + ASSERT_PARSE_ERROR(result, LX200_PARSE_INCOMPLETE); + + result = lx200_parser_add_data(&parser_state, "G", 1); + ASSERT_PARSE_ERROR(result, LX200_PARSE_INCOMPLETE); + + result = lx200_parser_add_data(&parser_state, "r", 1); + ASSERT_PARSE_ERROR(result, LX200_PARSE_INCOMPLETE); + + result = lx200_parser_add_data(&parser_state, "#", 1); + ASSERT_PARSE_OK(result); + + zassert_true(parser_state.command_complete, "Command should be complete"); + zassert_str_equal(parser_state.buffer, ":Gr#", "Buffer should contain complete command"); +} + +ZTEST(lx200_parser, test_add_data_buffer_overflow) +{ + char long_data[LX200_MAX_COMMAND_LENGTH + 10]; + lx200_parse_result_t result; + + memset(long_data, 'X', sizeof(long_data) - 1); + long_data[sizeof(long_data) - 1] = '\0'; + + result = lx200_parser_add_data(&parser_state, long_data, sizeof(long_data) - 1); + + ASSERT_PARSE_ERROR(result, LX200_PARSE_BUFFER_OVERFLOW); +} + +ZTEST(lx200_parser, test_add_data_invalid_parameters) +{ + lx200_parse_result_t result; + + /* NULL state */ + result = lx200_parser_add_data(NULL, ":Gr#", 4); + ASSERT_PARSE_ERROR(result, LX200_PARSE_ERROR); + + /* NULL data */ + result = lx200_parser_add_data(&parser_state, NULL, 4); + ASSERT_PARSE_ERROR(result, LX200_PARSE_ERROR); + + /* Zero length */ + result = lx200_parser_add_data(&parser_state, ":Gr#", 0); + ASSERT_PARSE_ERROR(result, LX200_PARSE_ERROR); +} + +ZTEST(lx200_parser, test_parse_command_string_get_ra) +{ + const char *cmd = ":Gr#"; + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Gr", "Command should be 'Gr'"); + zassert_equal(command.family, LX200_CMD_GET, "Should be GET command family"); + zassert_false(command.has_parameter, "Should not have parameter"); + zassert_equal(command.parameter_length, 0, "Parameter length should be 0"); +} + +ZTEST(lx200_parser, test_parse_command_string_set_ra_with_parameter) +{ + const char *cmd = ":Sr14:30:45#"; + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Sr", "Command should be 'Sr'"); + zassert_equal(command.family, LX200_CMD_SET, "Should be SET command family"); + zassert_true(command.has_parameter, "Should have parameter"); + zassert_str_equal(command.parameter, "14:30:45", "Parameter should be time"); + zassert_equal(command.parameter_length, 8, "Parameter length should be 8"); +} + +ZTEST(lx200_parser, test_parse_command_string_various_commands) +{ + struct { + const char *cmd; + const char *expected_command; + lx200_command_family_t expected_family; + bool has_param; + } test_cases[] = { + {":AA#", "AA", LX200_CMD_ALIGNMENT, false}, + {":B+#", "B+", LX200_CMD_RETICLE, false}, + {":CM#", "CM", LX200_CMD_SYNC, false}, + {":D#", "D", LX200_CMD_DISTANCE, false}, + {":F+#", "F+", LX200_CMD_FOCUSER, false}, + {":GD#", "GD", LX200_CMD_GET, false}, + {":gT#", "gT", LX200_CMD_GPS, false}, + {":H#", "H", LX200_CMD_TIME_FORMAT, false}, + {":I#", "I", LX200_CMD_INITIALIZE, false}, + {":LM#", "LM", LX200_CMD_LIBRARY, false}, + {":Mn#", "Mn", LX200_CMD_MOVE, false}, + {":P#", "P", LX200_CMD_PRECISION, false}, + {":Q#", "Q", LX200_CMD_STOP, false}, + {":RG#", "RG", LX200_CMD_SLEW_RATE, false}, + {":Sd+45*30:15#", "Sd", LX200_CMD_SET, true}, + {":TL#", "TL", LX200_CMD_TRACKING, false}, + {":U#", "U", LX200_CMD_PRECISION_TOGGLE, false}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + memset(&command, 0, sizeof(command)); + lx200_parse_result_t result = lx200_parse_command_string(test_cases[i].cmd, &command); + + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, test_cases[i].expected_command, + "Command mismatch for %s", test_cases[i].cmd); + zassert_equal(command.family, test_cases[i].expected_family, + "Family mismatch for %s", test_cases[i].cmd); + zassert_equal(command.has_parameter, test_cases[i].has_param, + "Parameter flag mismatch for %s", test_cases[i].cmd); + } +} -ZTEST(lx200, test_dummy) +ZTEST(lx200_parser, test_parse_command_string_invalid_prefix) { - zassert_equal(0, 0, "Dummy test failed"); + const char *cmd = "Gr#"; /* Missing ':' prefix */ + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_ERROR(result, LX200_PARSE_INVALID_PREFIX); } -ZTEST_SUITE(lx200, NULL, NULL, NULL, NULL, NULL); +ZTEST(lx200_parser, test_parse_command_string_invalid_terminator) +{ + const char *cmd = ":Gr"; /* Missing '#' terminator */ + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_ERROR(result, LX200_PARSE_INVALID_TERMINATOR); +} + +ZTEST(lx200_parser, test_parse_command_string_too_short) +{ + const char *cmd = ":"; /* Too short */ + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_ERROR(result, LX200_PARSE_INVALID_COMMAND); +} + +ZTEST(lx200_parser, test_parse_command_string_null_parameters) +{ + lx200_parse_result_t result; + + /* NULL command string */ + result = lx200_parse_command_string(NULL, &command); + ASSERT_PARSE_ERROR(result, LX200_PARSE_ERROR); + + /* NULL command structure */ + result = lx200_parse_command_string(":Gr#", NULL); + ASSERT_PARSE_ERROR(result, LX200_PARSE_ERROR); +} + +ZTEST(lx200_parser, test_parse_command_from_state) +{ + lx200_parse_result_t result; + + /* Add complete command to parser */ + result = lx200_parser_add_data(&parser_state, ":Gr#", 4); + ASSERT_PARSE_OK(result); + + /* Parse command from state */ + result = lx200_parse_command(&parser_state, &command); + ASSERT_PARSE_OK(result); + + zassert_str_equal(command.command, "Gr", "Command should be 'Gr'"); + zassert_equal(command.family, LX200_CMD_GET, "Should be GET command family"); +} + +ZTEST(lx200_parser, test_parse_command_incomplete_state) +{ + lx200_parse_result_t result; + + /* Add incomplete command to parser */ + result = lx200_parser_add_data(&parser_state, ":Gr", 3); + ASSERT_PARSE_ERROR(result, LX200_PARSE_INCOMPLETE); + + /* Try to parse incomplete command */ + result = lx200_parse_command(&parser_state, &command); + ASSERT_PARSE_ERROR(result, LX200_PARSE_INCOMPLETE); +} + +/* ============================================================================ + * COMMAND FAMILY AND UTILITY TESTS + * ============================================================================ */ + +ZTEST(lx200_utils, test_get_command_family) +{ + struct { + const char *cmd; + lx200_command_family_t expected; + } test_cases[] = { + {"A", LX200_CMD_ALIGNMENT}, + {"AA", LX200_CMD_ALIGNMENT}, + {"B+", LX200_CMD_RETICLE}, + {"C", LX200_CMD_SYNC}, + {"D", LX200_CMD_DISTANCE}, + {"F", LX200_CMD_FOCUSER}, + {"G", LX200_CMD_GET}, + {"Gr", LX200_CMD_GET}, + {"g", LX200_CMD_GPS}, + {"gT", LX200_CMD_GPS}, + {"H", LX200_CMD_TIME_FORMAT}, + {"I", LX200_CMD_INITIALIZE}, + {"L", LX200_CMD_LIBRARY}, + {"M", LX200_CMD_MOVE}, + {"P", LX200_CMD_PRECISION}, + {"Q", LX200_CMD_STOP}, + {"R", LX200_CMD_SLEW_RATE}, + {"S", LX200_CMD_SET}, + {"T", LX200_CMD_TRACKING}, + {"U", LX200_CMD_PRECISION_TOGGLE}, + {"X", LX200_CMD_UNKNOWN}, + {"", LX200_CMD_UNKNOWN}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + lx200_command_family_t family = lx200_get_command_family(test_cases[i].cmd); + zassert_equal(family, test_cases[i].expected, + "Family mismatch for command '%s'", test_cases[i].cmd); + } + + /* Test NULL parameter */ + lx200_command_family_t family = lx200_get_command_family(NULL); + zassert_equal(family, LX200_CMD_UNKNOWN, "Should return UNKNOWN for NULL"); +} + +ZTEST(lx200_utils, test_command_has_parameter) +{ + struct { + const char *cmd; + bool expected; + } test_cases[] = { + {"Sr", true}, /* Set commands typically have parameters */ + {"Sd", true}, + {"SC", true}, + {"Gr", false}, /* Get commands typically don't */ + {"AA", false}, /* Alignment commands don't */ + {"Q", false}, /* Stop commands don't */ + {"", false}, /* Empty command */ + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + bool has_param = lx200_command_has_parameter(test_cases[i].cmd); + zassert_equal(has_param, test_cases[i].expected, + "Parameter flag mismatch for command '%s'", test_cases[i].cmd); + } + + /* Test NULL parameter */ + bool has_param = lx200_command_has_parameter(NULL); + zassert_false(has_param, "Should return false for NULL"); +} + +ZTEST(lx200_utils, test_get_parameter_format) +{ + struct { + const char *cmd; + const char *expected; + } test_cases[] = { + {"Sr", "HH:MM:SS"}, + {"Sd", "sDD*MM:SS"}, + {"SL", "HH:MM:SS"}, + {"SC", "MM/DD/YY"}, + {"S", "Various"}, + {"G", "None"}, + {"", "None"}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + const char *format = lx200_get_parameter_format(test_cases[i].cmd); + zassert_str_equal(format, test_cases[i].expected, + "Format mismatch for command '%s'", test_cases[i].cmd); + } + + /* Test NULL parameter */ + const char *format = lx200_get_parameter_format(NULL); + zassert_str_equal(format, "None", "Should return 'None' for NULL"); +} + +ZTEST(lx200_utils, test_parse_result_to_string) +{ + struct { + lx200_parse_result_t result; + const char *expected; + } test_cases[] = { + {LX200_PARSE_OK, "OK"}, + {LX200_PARSE_INCOMPLETE, "Incomplete"}, + {LX200_PARSE_INVALID_PREFIX, "Invalid prefix"}, + {LX200_PARSE_INVALID_TERMINATOR, "Invalid terminator"}, + {LX200_PARSE_INVALID_COMMAND, "Invalid command"}, + {LX200_PARSE_INVALID_PARAMETER, "Invalid parameter"}, + {LX200_PARSE_BUFFER_OVERFLOW, "Buffer overflow"}, + {LX200_PARSE_ERROR, "Parse error"}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + const char *str = lx200_parse_result_to_string(test_cases[i].result); + zassert_str_equal(str, test_cases[i].expected, + "String mismatch for result %d", test_cases[i].result); + } + + /* Test invalid result code */ + const char *str = lx200_parse_result_to_string((lx200_parse_result_t)999); + zassert_str_equal(str, "Parse error", "Should return 'Parse error' for invalid code"); +} + +/* ============================================================================ + * COMPLEX COMMAND PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_complex, test_long_parameter_command) +{ + const char *cmd = ":Sr12:34:56#"; + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Sr", "Command should be 'Sr'"); + zassert_true(command.has_parameter, "Should have parameter"); + zassert_str_equal(command.parameter, "12:34:56", "Parameter should be time"); + zassert_equal(command.parameter_length, 8, "Parameter length should be 8"); +} + +ZTEST(lx200_complex, test_multiple_commands_sequential) +{ + lx200_parse_result_t result; + + /* First command */ + result = lx200_parser_add_data(&parser_state, ":Gr#", 4); + ASSERT_PARSE_OK(result); + + result = lx200_parse_command(&parser_state, &command); + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Gr", "First command should be 'Gr'"); + + /* Reset and parse second command */ + lx200_parser_reset(&parser_state); + + result = lx200_parser_add_data(&parser_state, ":Gd#", 4); + ASSERT_PARSE_OK(result); + + result = lx200_parse_command(&parser_state, &command); + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Gd", "Second command should be 'Gd'"); +} + +ZTEST(lx200_complex, test_command_with_special_characters) +{ + const char *cmd = ":Sd+45*30:15#"; + lx200_parse_result_t result; + + result = lx200_parse_command_string(cmd, &command); + + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Sd", "Command should be 'Sd'"); + zassert_true(command.has_parameter, "Should have parameter"); + zassert_str_equal(command.parameter, "+45*30:15", "Parameter should contain special chars"); +} + +ZTEST(lx200_complex, test_boundary_conditions) +{ + lx200_parse_result_t result; + char buffer[LX200_MAX_COMMAND_LENGTH]; + + /* Test maximum length command */ + memset(buffer, 0, sizeof(buffer)); + buffer[0] = ':'; + buffer[1] = 'S'; + buffer[2] = 'r'; + /* Fill with parameter data up to max length - 2 (for ':' and '#') */ + for (int i = 3; i < LX200_MAX_COMMAND_LENGTH - 1; i++) { + buffer[i] = '0' + (i % 10); + } + buffer[LX200_MAX_COMMAND_LENGTH - 1] = '#'; + + result = lx200_parse_command_string(buffer, &command); + ASSERT_PARSE_OK(result); + zassert_str_equal(command.command, "Sr", "Command should be parsed correctly"); + zassert_true(command.has_parameter, "Should have parameter"); +} + +/* ============================================================================ + * ERROR HANDLING TESTS + * ============================================================================ */ + +ZTEST(lx200_errors, test_malformed_commands) +{ + struct { + const char *cmd; + lx200_parse_result_t expected; + } test_cases[] = { + {"", LX200_PARSE_INVALID_COMMAND}, + {"#", LX200_PARSE_INVALID_COMMAND}, + {":", LX200_PARSE_INVALID_COMMAND}, + {":G", LX200_PARSE_INVALID_TERMINATOR}, + {"G#", LX200_PARSE_INVALID_PREFIX}, + {":G$", LX200_PARSE_INVALID_TERMINATOR}, + {"$G#", LX200_PARSE_INVALID_PREFIX}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + lx200_parse_result_t result = lx200_parse_command_string(test_cases[i].cmd, &command); + zassert_equal(result, test_cases[i].expected, + "Error mismatch for command '%s'", test_cases[i].cmd); + } +} + +ZTEST(lx200_errors, test_parameter_too_long) +{ + char long_cmd[LX200_MAX_COMMAND_LENGTH * 2]; + lx200_parse_result_t result; + + /* Create command with parameter that's too long */ + strcpy(long_cmd, ":Sr"); + for (int i = 3; i < sizeof(long_cmd) - 2; i++) { + long_cmd[i] = '0'; + } + long_cmd[sizeof(long_cmd) - 2] = '#'; + long_cmd[sizeof(long_cmd) - 1] = '\0'; + + result = lx200_parse_command_string(long_cmd, &command); + ASSERT_PARSE_ERROR(result, LX200_PARSE_BUFFER_OVERFLOW); +} + +/* ============================================================================ + * TEST SUITE DEFINITIONS + * ============================================================================ */ + +ZTEST_SUITE(lx200_parser, NULL, NULL, lx200_test_setup, lx200_test_teardown, NULL); +ZTEST_SUITE(lx200_utils, NULL, NULL, NULL, NULL, NULL); +ZTEST_SUITE(lx200_complex, NULL, NULL, lx200_test_setup, lx200_test_teardown, NULL); +ZTEST_SUITE(lx200_errors, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/lib/lx200/src/test_coordinates.c b/tests/lib/lx200/src/test_coordinates.c new file mode 100644 index 0000000..957bcc2 --- /dev/null +++ b/tests/lib/lx200/src/test_coordinates.c @@ -0,0 +1,444 @@ +/** + * @file test_coordinates.c + * @brief LX200 Coordinate Parsing Test Suite + * + * These tests are for the coordinate parsing functions that are currently + * unimplemented. They serve as specifications for future implementation. + * + * Copyright (c) 2025, OpenAstroTech + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +/* Test fixtures */ +static lx200_coordinate_t coordinate; +static lx200_time_t time_val; +static lx200_date_t date_val; + +/** + * @brief Setup function called before each test + */ +static void coordinate_test_setup(void *fixture) +{ + ARG_UNUSED(fixture); + memset(&coordinate, 0, sizeof(coordinate)); + memset(&time_val, 0, sizeof(time_val)); + memset(&date_val, 0, sizeof(date_val)); +} + +/* ============================================================================ + * RIGHT ASCENSION COORDINATE PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_coordinates, test_parse_ra_high_precision) +{ + /* These tests will pass once the functions are implemented */ + + /* Test valid RA in high precision format: HH:MM:SS */ + const char *ra_str = "14:30:45"; + lx200_parse_result_t result = lx200_parse_ra_coordinate(ra_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); + + /* TODO: When implemented, test should be: + * ASSERT_PARSE_OK(result); + * zassert_equal(coordinate.degrees, 14, "Hours should be 14"); + * zassert_equal(coordinate.minutes, 30, "Minutes should be 30"); + * zassert_equal(coordinate.seconds, 45, "Seconds should be 45"); + * zassert_equal(coordinate.precision, LX200_COORD_HIGH_PRECISION, "Should be high precision"); + */ +} + +ZTEST(lx200_coordinates, test_parse_ra_low_precision) +{ + /* Test valid RA in low precision format: HH:MM.T */ + const char *ra_str = "14:30.5"; + lx200_parse_result_t result = lx200_parse_ra_coordinate(ra_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); + + /* TODO: When implemented, test should be: + * ASSERT_PARSE_OK(result); + * zassert_equal(coordinate.degrees, 14, "Hours should be 14"); + * zassert_equal(coordinate.minutes, 30, "Minutes should be 30"); + * zassert_equal(coordinate.tenths, 5, "Tenths should be 5"); + * zassert_equal(coordinate.precision, LX200_COORD_LOW_PRECISION, "Should be low precision"); + */ +} + +ZTEST(lx200_coordinates, test_parse_ra_boundary_values) +{ + struct { + const char *ra_str; + const char *description; + } test_cases[] = { + {"00:00:00", "Minimum RA value"}, + {"23:59:59", "Maximum RA value"}, + {"12:00:00", "Noon RA value"}, + {"06:30:15", "Mid-morning RA value"}, + {"18:45:30", "Evening RA value"}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + lx200_parse_result_t result = lx200_parse_ra_coordinate(test_cases[i].ra_str, &coordinate); + + /* Currently all return error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet: %s", + test_cases[i].description); + } +} + +/* ============================================================================ + * DECLINATION COORDINATE PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_coordinates, test_parse_dec_positive) +{ + /* Test positive declination: +DD*MM:SS */ + const char *dec_str = "+45*30:15"; + lx200_parse_result_t result = lx200_parse_dec_coordinate(dec_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); + + /* TODO: When implemented, test should be: + * ASSERT_PARSE_OK(result); + * zassert_equal(coordinate.degrees, 45, "Degrees should be 45"); + * zassert_equal(coordinate.minutes, 30, "Minutes should be 30"); + * zassert_equal(coordinate.seconds, 15, "Seconds should be 15"); + * zassert_false(coordinate.is_negative, "Should be positive"); + */ +} + +ZTEST(lx200_coordinates, test_parse_dec_negative) +{ + /* Test negative declination: -DD*MM:SS */ + const char *dec_str = "-30*15:45"; + lx200_parse_result_t result = lx200_parse_dec_coordinate(dec_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); + + /* TODO: When implemented, test should be: + * ASSERT_PARSE_OK(result); + * zassert_equal(coordinate.degrees, 30, "Degrees should be 30"); + * zassert_equal(coordinate.minutes, 15, "Minutes should be 15"); + * zassert_equal(coordinate.seconds, 45, "Seconds should be 45"); + * zassert_true(coordinate.is_negative, "Should be negative"); + */ +} + +ZTEST(lx200_coordinates, test_parse_dec_boundary_values) +{ + struct { + const char *dec_str; + const char *description; + } test_cases[] = { + {"+90*00:00", "Maximum positive declination"}, + {"-90*00:00", "Maximum negative declination"}, + {"+00*00:00", "Zero declination"}, + {"+45*00:00", "Mid-range positive"}, + {"-45*00:00", "Mid-range negative"}, + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_cases); i++) { + lx200_parse_result_t result = lx200_parse_dec_coordinate(test_cases[i].dec_str, &coordinate); + + /* Currently all return error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet: %s", + test_cases[i].description); + } +} + +/* ============================================================================ + * ALTITUDE/AZIMUTH COORDINATE PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_coordinates, test_parse_altitude) +{ + /* Test altitude parsing: sDD*MM:SS */ + const char *alt_str = "+30*15:30"; + lx200_parse_result_t result = lx200_parse_alt_coordinate(alt_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +ZTEST(lx200_coordinates, test_parse_azimuth) +{ + /* Test azimuth parsing: DDD*MM:SS */ + const char *az_str = "180*30:15"; + lx200_parse_result_t result = lx200_parse_az_coordinate(az_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +/* ============================================================================ + * GEOGRAPHIC COORDINATE PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_coordinates, test_parse_longitude) +{ + /* Test longitude parsing: sDDD*MM */ + const char *lon_str = "-122*30"; + lx200_parse_result_t result = lx200_parse_longitude(lon_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +ZTEST(lx200_coordinates, test_parse_latitude) +{ + /* Test latitude parsing: sDD*MM */ + const char *lat_str = "+37*45"; + lx200_parse_result_t result = lx200_parse_latitude(lat_str, &coordinate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +/* ============================================================================ + * TIME AND DATE PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_time_date, test_parse_time) +{ + /* Test time parsing: HH:MM:SS */ + const char *time_str = "14:30:45"; + lx200_parse_result_t result = lx200_parse_time(time_str, &time_val); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); + + /* TODO: When implemented, test should be: + * ASSERT_PARSE_OK(result); + * zassert_equal(time_val.hours, 14, "Hours should be 14"); + * zassert_equal(time_val.minutes, 30, "Minutes should be 30"); + * zassert_equal(time_val.seconds, 45, "Seconds should be 45"); + */ +} + +ZTEST(lx200_time_date, test_parse_date) +{ + /* Test date parsing: MM/DD/YY */ + const char *date_str = "12/25/23"; + lx200_parse_result_t result = lx200_parse_date(date_str, &date_val); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); + + /* TODO: When implemented, test should be: + * ASSERT_PARSE_OK(result); + * zassert_equal(date_val.month, 12, "Month should be 12"); + * zassert_equal(date_val.day, 25, "Day should be 25"); + * zassert_equal(date_val.year, 23, "Year should be 23"); + */ +} + +ZTEST(lx200_time_date, test_parse_utc_offset) +{ + /* Test UTC offset parsing: sHH or sHH.H */ + const char *offset_str = "-08"; + float offset = 0.0f; + lx200_parse_result_t result = lx200_parse_utc_offset(offset_str, &offset); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +/* ============================================================================ + * TRACKING AND SLEW RATE PARSING TESTS + * ============================================================================ */ + +ZTEST(lx200_rates, test_parse_tracking_rate) +{ + /* Test tracking rate parsing: TT.T */ + const char *rate_str = "60.1"; + float rate = 0.0f; + lx200_parse_result_t result = lx200_parse_tracking_rate(rate_str, &rate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +ZTEST(lx200_rates, test_parse_slew_rate) +{ + /* Test slew rate parsing */ + const char *rate_str = "RC"; + lx200_slew_rate_t rate = LX200_SLEW_GUIDE; + lx200_parse_result_t result = lx200_parse_slew_rate(rate_str, &rate); + + /* Currently returns error since not implemented */ + zassert_equal(result, LX200_PARSE_ERROR, "Function not implemented yet"); +} + +/* ============================================================================ + * COORDINATE FORMATTING TESTS + * ============================================================================ */ + +ZTEST(lx200_formatting, test_format_ra_coordinate) +{ + /* Test RA coordinate formatting */ + lx200_coordinate_t coord = { + .degrees = 14, + .minutes = 30, + .seconds = 45, + .is_negative = false, + .precision = LX200_COORD_HIGH_PRECISION + }; + + char buffer[32]; + int result = lx200_format_ra_coordinate(&coord, buffer, sizeof(buffer)); + + /* Currently returns error since not implemented */ + zassert_equal(result, -1, "Function not implemented yet"); +} + +ZTEST(lx200_formatting, test_format_dec_coordinate) +{ + /* Test Dec coordinate formatting */ + lx200_coordinate_t coord = { + .degrees = 45, + .minutes = 30, + .seconds = 15, + .is_negative = false, + .precision = LX200_COORD_HIGH_PRECISION + }; + + char buffer[32]; + int result = lx200_format_dec_coordinate(&coord, buffer, sizeof(buffer)); + + /* Currently returns error since not implemented */ + zassert_equal(result, -1, "Function not implemented yet"); +} + +ZTEST(lx200_formatting, test_format_time) +{ + /* Test time formatting */ + lx200_time_t time = { + .hours = 14, + .minutes = 30, + .seconds = 45, + .is_24h_format = true + }; + + char buffer[32]; + int result = lx200_format_time(&time, buffer, sizeof(buffer)); + + /* Currently returns error since not implemented */ + zassert_equal(result, -1, "Function not implemented yet"); +} + +ZTEST(lx200_formatting, test_format_date) +{ + /* Test date formatting */ + lx200_date_t date = { + .month = 12, + .day = 25, + .year = 23 + }; + + char buffer[32]; + int result = lx200_format_date(&date, buffer, sizeof(buffer)); + + /* Currently returns error since not implemented */ + zassert_equal(result, -1, "Function not implemented yet"); +} + +/* ============================================================================ + * VALIDATION TESTS + * ============================================================================ */ + +ZTEST(lx200_validation, test_validate_coordinate) +{ + /* Test coordinate validation */ + lx200_coordinate_t coord = { + .degrees = 45, + .minutes = 30, + .seconds = 15, + .is_negative = false, + .precision = LX200_COORD_HIGH_PRECISION + }; + + bool result = lx200_validate_coordinate(&coord, LX200_CMD_SET); + + /* Currently returns false since not implemented */ + zassert_false(result, "Function not implemented yet"); +} + +ZTEST(lx200_validation, test_validate_time) +{ + /* Test time validation */ + lx200_time_t time = { + .hours = 14, + .minutes = 30, + .seconds = 45, + .is_24h_format = true + }; + + bool result = lx200_validate_time(&time); + + /* Currently returns false since not implemented */ + zassert_false(result, "Function not implemented yet"); +} + +ZTEST(lx200_validation, test_validate_date) +{ + /* Test date validation */ + lx200_date_t date = { + .month = 12, + .day = 25, + .year = 23 + }; + + bool result = lx200_validate_date(&date); + + /* Currently returns false since not implemented */ + zassert_false(result, "Function not implemented yet"); +} + +/* ============================================================================ + * ERROR CASES FOR UNIMPLEMENTED FUNCTIONS + * ============================================================================ */ + +ZTEST(lx200_coordinates_errors, test_null_parameter_handling) +{ + /* Test that unimplemented functions handle NULL parameters gracefully */ + + zassert_equal(lx200_parse_ra_coordinate(NULL, &coordinate), LX200_PARSE_ERROR, + "Should handle NULL string"); + zassert_equal(lx200_parse_ra_coordinate("14:30:45", NULL), LX200_PARSE_ERROR, + "Should handle NULL coordinate"); + + zassert_equal(lx200_parse_dec_coordinate(NULL, &coordinate), LX200_PARSE_ERROR, + "Should handle NULL string"); + zassert_equal(lx200_parse_dec_coordinate("+45*30:15", NULL), LX200_PARSE_ERROR, + "Should handle NULL coordinate"); + + zassert_equal(lx200_parse_time(NULL, &time_val), LX200_PARSE_ERROR, + "Should handle NULL string"); + zassert_equal(lx200_parse_time("14:30:45", NULL), LX200_PARSE_ERROR, + "Should handle NULL time"); + + zassert_equal(lx200_parse_date(NULL, &date_val), LX200_PARSE_ERROR, + "Should handle NULL string"); + zassert_equal(lx200_parse_date("12/25/23", NULL), LX200_PARSE_ERROR, + "Should handle NULL date"); +} + +/* ============================================================================ + * TEST SUITE DEFINITIONS + * ============================================================================ */ + +ZTEST_SUITE(lx200_coordinates, NULL, NULL, coordinate_test_setup, NULL, NULL); +ZTEST_SUITE(lx200_time_date, NULL, NULL, coordinate_test_setup, NULL, NULL); +ZTEST_SUITE(lx200_rates, NULL, NULL, NULL, NULL, NULL); +ZTEST_SUITE(lx200_formatting, NULL, NULL, NULL, NULL, NULL); +ZTEST_SUITE(lx200_validation, NULL, NULL, NULL, NULL, NULL); +ZTEST_SUITE(lx200_coordinates_errors, NULL, NULL, coordinate_test_setup, NULL, NULL); diff --git a/tests/lib/lx200/testcase.yaml b/tests/lib/lx200/testcase.yaml index 28ddf5a..ee28a91 100644 --- a/tests/lib/lx200/testcase.yaml +++ b/tests/lib/lx200/testcase.yaml @@ -1,7 +1,18 @@ common: - tags: extensibility + tags: + - lx200 + - telescope + - protocol + - parser + timeout: 60 integration_platforms: - robin_nano - native_sim + platform_allow: + - native_sim + - qemu_x86 + - qemu_cortex_m3 + - robin_nano + tests: lib.lx200: {}