Skip to content

feat: RoboticArm CSV import/export #256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

rahul31124
Copy link
Contributor

@rahul31124 rahul31124 commented Jul 16, 2025

Fixes #254

Changes

  • Added import_timeline_from_csv() method to the RoboticArm class
    Allows importing a timeline (CSV format) generated by the PSLab Mobile app and using it directly in Python.

  • Added export_timeline_to_csv() method to the RoboticArm class
    Enables exporting a Python-generated timeline to a timestamped CSV file, which can then be used in the PSLab Mobile app.

Summary by Sourcery

Add CSV import and export capabilities to the RoboticArm and improve timeline execution to handle missing values

New Features:

  • Add import_timeline_from_csv method to load servo timelines from CSV files
  • Add export_timeline_to_csv method to save Python-generated timelines as timestamped CSV files

Enhancements:

  • Update run_schedule to skip None entries when applying servo angles

Copy link

sourcery-ai bot commented Jul 16, 2025

Reviewer's Guide

This PR augments the RoboticArm class with CSV-based timeline import/export capabilities, refines run_schedule to skip null entries, and adds necessary module imports for CSV handling and timestamp generation.

Class diagram for RoboticArm CSV import/export methods

classDiagram
    class RoboticArm {
        ...
        run_schedule(timeline: List[List[int]], time_step: float)
        import_timeline_from_csv(filepath: str) List[List[int]]
        export_timeline_to_csv(timeline: List[List[Union[int, None]]], folderpath: str) None
    }
Loading

File-Level Changes

Change Details Files
Support optional None values in run_schedule
  • Wrap servo angle assignment in a None check to leave angles unchanged when timeline entry is null
pslab/external/motor.py
Add import_timeline_from_csv method
  • Define method signature and docstring for importing timelines
  • Open CSV with DictReader and iterate rows
  • Parse Servo1–Servo4 fields, mapping empty/null strings to None or converting to int
  • Assemble and return a List[List[int]] timeline
pslab/external/motor.py
Add export_timeline_to_csv method
  • Define method signature and docstring for exporting timelines
  • Generate timestamp-based filename and join with target folder
  • Write CSV header and rows, converting None values to “null” and prefixing rows with an index
  • Use csv.writer to output the file
pslab/external/motor.py
Introduce CSV and timestamp-related imports
  • Import csv and os modules
  • Import datetime from the datetime module
pslab/external/motor.py

Assessment against linked issues

Issue Objective Addressed Explanation
#254 Implement the ability to import a RoboticArm position schedule from a CSV file.
#254 Implement the ability to export a RoboticArm position schedule to a CSV file.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rahul31124 - I've reviewed your changes - here's some feedback:

  • Consider deriving the servo column names dynamically from self.servos instead of hard-coding "Servo1"–"Servo4", so the methods will work with any number of servos.
  • In export_timeline_to_csv, validate or create the output folder if it doesn’t exist (and surface an error if it can’t be written), and skip the timestamp column on import rather than assuming fixed position.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider deriving the servo column names dynamically from self.servos instead of hard-coding "Servo1"–"Servo4", so the methods will work with any number of servos.
- In export_timeline_to_csv, validate or create the output folder if it doesn’t exist (and surface an error if it can’t be written), and skip the timestamp column on import rather than assuming fixed position.

## Individual Comments

### Comment 1
<location> `pslab/external/motor.py:136` </location>
<code_context>
+            reader = csv.DictReader(csvfile)
+            for row in reader:
+                angles = []
+                for key in ["Servo1", "Servo2", "Servo3", "Servo4"]:
+                    value = row.get(key, "").strip().lower()
+                    if value in ("", "null", "none"):
+                        angles.append(None)
+                    else:
+                        angles.append(int(value))
+                timeline.append(angles)
+
</code_context>

<issue_to_address>
Assumes all servo columns are present and in order.

If columns are missing or out of order, parsing may fail or produce incorrect results. Please validate the header or handle columns more flexibly.
</issue_to_address>

### Comment 2
<location> `pslab/external/motor.py:166` </location>
<code_context>
+
+        with open(filepath, mode="w", newline="") as csvfile:
+            writer = csv.writer(csvfile)
+            writer.writerow(["Timestamp", "Servo1", "Servo2", "Servo3", "Servo4"])
+            for i, row in enumerate(timeline):
+                pos = ["null" if val is None else val for val in row]
</code_context>

<issue_to_address>
Header uses 'Timestamp' for what is actually a timestep index.

Consider renaming the column to 'Timestep' or 'Step' to better reflect its contents.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
            writer.writerow(["Timestamp", "Servo1", "Servo2", "Servo3", "Servo4"])
=======
            writer.writerow(["Timestep", "Servo1", "Servo2", "Servo3", "Servo4"])
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `pslab/external/motor.py:168` </location>
<code_context>
+            writer = csv.writer(csvfile)
+            writer.writerow(["Timestamp", "Servo1", "Servo2", "Servo3", "Servo4"])
+            for i, row in enumerate(timeline):
+                pos = ["null" if val is None else val for val in row]
+                writer.writerow([i] + pos)
</code_context>

<issue_to_address>
Inconsistent handling of None values between import and export.

Consider using a single representation for None values (e.g., always 'null') in both import and export, and document this behavior for clarity.

Suggested implementation:

```python
        # Export timeline to CSV, using 'null' to represent None values.
        # When importing, 'null' will be interpreted as None.
        with open(filepath, mode="w", newline="") as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(["Timestamp", "Servo1", "Servo2", "Servo3", "Servo4"])
            for i, row in enumerate(timeline):
                pos = ["null" if val is None else val for val in row]
                writer.writerow([i] + pos)

```

```python
def import_timeline_from_csv(filepath):
    """
    Import a timeline from a CSV file, interpreting 'null' as None.

    Args:
        filepath (str): Path to the CSV file.

    Returns:
        list: Timeline data as a list of lists, with None for 'null' values.
    """
    timeline = []
    with open(filepath, mode="r", newline="") as csvfile:
        reader = csv.reader(csvfile)
        headers = next(reader)  # Skip header
        for row in reader:
            # row[0] is the timestamp/index, skip or store as needed
            positions = [None if val == "null" else float(val) for val in row[1:]]
            timeline.append(positions)
    return timeline

```

- If there is already an import function, update its logic to use `None if val == "null" else ...` for consistency.
- Ensure that all documentation and usage of import/export functions mention that 'null' is the standard representation for missing values.
- If the import function is used elsewhere, update those usages to use the new or updated function.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 136 to 141
for key in ["Servo1", "Servo2", "Servo3", "Servo4"]:
value = row.get(key, "").strip().lower()
if value in ("", "null", "none"):
angles.append(None)
else:
angles.append(int(value))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Assumes all servo columns are present and in order.

If columns are missing or out of order, parsing may fail or produce incorrect results. Please validate the header or handle columns more flexibly.

writer = csv.writer(csvfile)
writer.writerow(["Timestamp", "Servo1", "Servo2", "Servo3", "Servo4"])
for i, row in enumerate(timeline):
pos = ["null" if val is None else val for val in row]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Inconsistent handling of None values between import and export.

Consider using a single representation for None values (e.g., always 'null') in both import and export, and document this behavior for clarity.

Suggested implementation:

        # Export timeline to CSV, using 'null' to represent None values.
        # When importing, 'null' will be interpreted as None.
        with open(filepath, mode="w", newline="") as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(["Timestamp", "Servo1", "Servo2", "Servo3", "Servo4"])
            for i, row in enumerate(timeline):
                pos = ["null" if val is None else val for val in row]
                writer.writerow([i] + pos)
def import_timeline_from_csv(filepath):
    """
    Import a timeline from a CSV file, interpreting 'null' as None.

    Args:
        filepath (str): Path to the CSV file.

    Returns:
        list: Timeline data as a list of lists, with None for 'null' values.
    """
    timeline = []
    with open(filepath, mode="r", newline="") as csvfile:
        reader = csv.reader(csvfile)
        headers = next(reader)  # Skip header
        for row in reader:
            # row[0] is the timestamp/index, skip or store as needed
            positions = [None if val == "null" else float(val) for val in row[1:]]
            timeline.append(positions)
    return timeline
  • If there is already an import function, update its logic to use None if val == "null" else ... for consistency.
  • Ensure that all documentation and usage of import/export functions mention that 'null' is the standard representation for missing values.
  • If the import function is used elsewhere, update those usages to use the new or updated function.

@rahul31124 rahul31124 changed the title CSV import/export feat: RoboticArm CSV import/export Jul 16, 2025
@rahul31124 rahul31124 requested a review from bessman July 16, 2025 16:39
@rahul31124
Copy link
Contributor Author

Hello @bessman, could you please review this PR and suggest changes that might be needed?

angles = []
for key in ["Servo1", "Servo2", "Servo3", "Servo4"]:
value = row.get(key, "").strip().lower()
if value in ("", "null", "none"):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can None values actually be represented by all of these (empty string, null, none)? If so, why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in this case we treat None values as ("", "null", "none") because
The CSV exported by PSLab Mobile explicitly uses the string "null" for missing values
If users manually edit the CSV or add new entries, it's possible for values like an empty string "" or "none" to appear

Let me know your suggestion

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the export uses only "null", the import should only accept "null".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bessman I’ve now updated the import logic to only check for "null" as per your suggestion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

RoboticArm CSV import/export
2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy