Skip to content

Conversation

@nateinaction
Copy link
Member

@nateinaction nateinaction commented Apr 21, 2025

Summary

This PR began as a new "state of health" system, but evolved into a major refactor primarily centered on a new, more flexible beaconing system. During development, increased radio packet sizes required a refactor of the Packet Manager, which in turn led to a broader adoption of packetized messaging throughout the board codebases, requiring a Command Data Handler rewrite to maintain the ability to command the satellite. Ground station code has now been moved out into its own repository.

Breaking Changes:

  • Removes the Satellite and Functions classes. Downstream code using these will need to migrate to the new interfaces.
  • Moves ground station code to a new repository: CircuitPython_Ground_Station. See migration instructions there.
  • Packet Manager and Command Data Handler have been refactored; old interfaces may no longer work.

Notable Features

Beacon

  • Introduces a new Beacon service that accepts and outputs details for any number of supported sensors or hardware.
  • Uses protos as generic base classes and a variadic argument for extensibility—making satellite beacon data infinitely expandable and adaptable.

Packet Manager

  • Now supports sending messages of any size, both from satellite to ground and vice versa.
  • Refactored to handle all sending/receiving on the satellite.

Command Data Handler Refactor

  • Now supports packetized messages, reading the following structure:
    {
      "password": "abc123",
      "command": "mycmd",
      "args": ["arg1", "arg2"]
    }

Ground Station

  • Radio test/ground station code is now in CircuitPython_Ground_Station and no longer included in flight builds.
  • New install directives added to the Makefile for the v4 board (PR). If we like this change, it can be ported to v5.

Logging Changes

  • Many INFO messages are now DEBUG, allowing users to set quieter log output as needed.

Other Changes

  • Updates some dev dependencies to resolve conflicts.
  • Splits dev dependencies from runtime dependencies in pyproject.toml (clerical only, no impact on developer workflow).
  • Adds a JSON autoformatter to pre-commit to resolve spacing inconsistencies in config.json files across repos.
  • Added tests for SleepHelper because it was the only file left without tests

How was this tested?

  • Added new unit tests
  • Ran code on hardware (see screenshots below)
  • Other (N/A)

Receiving Beacon

Captura de pantalla 2025-06-19 a la(s) 18 13 00

Sending joke request

Captura de pantalla 2025-06-19 a la(s) 15 32 36

Sending reset request

Captura de pantalla 2025-06-19 a la(s) 18 16 32

Sending change modulation request (⚠️ read-only OS, doesn't work atm)

Captura de pantalla 2025-06-19 a la(s) 18 11 30

Known Issues / Future Work

  • Change modulation request is not functional due to read-only OS constraints (see screenshot above).
  • Add additional strategies for sending messages to ensure delivery such as
    • retransmission when packets are dropped.

How to Review

  • Focus on the Beacon, Packet Manager, and Command Data Handler refactors—these are the core of the PR.
  • Ground station-related changes have been moved to their own repo.

Thank you for reviewing! Please reach out with any questions or suggestions.

JamesDCowley and others added 22 commits April 4, 2025 18:08
… health pre-repo organization to work with new repo structure)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Member Author

@nateinaction nateinaction Apr 21, 2025

Choose a reason for hiding this comment

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

Initially I had developed a complex system of getting the name for a flag or counter from the associated registers. This worked well enough but then I asked myself... what if the builder of the proveskit wanted to add their own registers? Having them hardcoded here is not sufficiently flexible for the kit so we moved to having a get_name() method on flags and counters instead. Not perfect but OK for now.

Copy link
Member Author

@nateinaction nateinaction Apr 21, 2025

Choose a reason for hiding this comment

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

I'm sorry for lumping this in here but as I began removing unused and hardcoded flag and counters from satellite I noticed that Satellite wasn't doing very much anymore. I moved two methods over to the functions class and was able to delete everything else.

gc.collect()
# all should be off from cubesat powermode

self.cubesat.f_softboot.toggle(True)
Copy link
Member Author

Choose a reason for hiding this comment

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

f_softboot had no functionality, it was being set here and then reset to false in the Satellite constructor.

Copy link
Member

Choose a reason for hiding this comment

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

Just for new context on this, the f_softboot flag is there so the satellite can check on startup whether or not it is restarting gracefully or if it is coming back from a hard error (i.e. the watchdog forced a reset due to a software hang or other functional interrupt).

We right now are indeed not using it for anything, but in the future would probably want to bring it back somehow so the satellite can evaluate on reset whether it is fine to continue business as usual or if it suffered a critical fault and should enter a safe mode. There may be some interesting things to play around with here with the supervisor module as well.

@nateinaction nateinaction changed the title DRAFT Feedback on state of health Feedback on state of health Apr 21, 2025
@nateinaction nateinaction marked this pull request as ready for review April 21, 2025 06:17
@nateinaction nateinaction mentioned this pull request Apr 21, 2025
3 tasks
@nateinaction nateinaction requested a review from Copilot June 19, 2025 23:52

This comment was marked as outdated.

@sonarqubecloud
Copy link

@nateinaction nateinaction requested a review from Copilot June 25, 2025 01:42
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors core flight software components to introduce a flexible beacon service, overhaul packet handling, and rewrite command processing using packetized messaging.

  • Introduces a new Beacon class for extensible telemetry via variadic sensor inputs.
  • Moves and refactors packet logic into hardware/radio/packetizer/PacketManager to support arbitrary message sizes.
  • Rewrites CommandDataHandler to parse JSON commands over packets and handle them via a clean interface.

Reviewed Changes

Copilot reviewed 41 out of 43 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pysquared/beacon.py New Beacon service that aggregates and sends sensor data as JSON.
pysquared/hardware/radio/packetizer/packet_manager.py New packet manager for splitting, sending, and reassembling large payloads.
pysquared/cdh.py CommandDataHandler overhaul: listens for JSON, validates, dispatches commands.
pysquared/sleep_helper.py Refactored safe_sleep implementation with watchdog pets.
pysquared/hardware/radio/manager/**/* Updated radio manager classes to simplify send logic and standardize APIs.
Comments suppressed due to low confidence (4)

pysquared/hardware/radio/manager/base.py:54

  • The docstring for send still references license headers and payload assembly, but the implementation now just forwards raw bytes. Please update the docstring to accurately describe the current behavior.
    def send(self, data: bytes) -> bool:

pysquared/hardware/radio/manager/rfm9x.py:83

  • After validating the key, any parameter that isn't explicitly handled is silently ignored. Consider raising a KeyError or logging a warning for unsupported key values to avoid silent no-ops.
    def modify_config(self, key: str, value) -> None:

pysquared/beacon.py:114

  • [nitpick] The avg_readings method bails out on the first None return but we lack tests for scenarios where func returns values then None. Adding a test for mixed successful/None readings would improve coverage.
    def avg_readings(

pysquared/sleep_helper.py:37

  • [nitpick] The loop calls time.monotonic() multiple times per iteration, which can drift if the clock advances. Cache the current time once per loop to compute remaining sleep time more predictably.
        if duration > self.config.longest_allowable_sleep_time:

assert manager._radio == mock_lora_instance
# Check high SF optimization IS set
assert mock_lora_instance.preamble_length == 10
assert mock_lora_instance.low_datarate_optimize == 1
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason that we are not checking this assert statement anymore? We do actually want to ideally ensure that low_datarate_optimize is being successfully set for spreading factors of 10 or higher.

The spreading_factor of LoRa radios is one of the primary driving factors behind the radio's overall data rate. The attached image is an example of what it looks like for the LoRaWAN protocol. Lower data rates generally mean better signal reception. When the data rate starts getting really low flipping the register for low_datarate_optimize plays a major role in ensuring packets don't get dropped.
Screenshot 2025-06-26 at 12 32 48 PM

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I intentionally removed this! When I looked through the Adafruit RFM library I found that the spreading_factor setter was managing this for us. https://github.com/adafruit/Adafruit_CircuitPython_RFM/blob/e242320b57317beb11b28676f9440593fbbfbfe1/adafruit_rfm/rfm9x.py#L481-L485

Copy link
Member

@Mikefly123 Mikefly123 left a comment

Choose a reason for hiding this comment

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

Great work on this Nate! I think it overall looks great to me. I have just left a few comments on things to think about.

Nothing game breaking, so I think we'd be happy to go ahead and merge this stuff in and revisit a few of the questions in future pull requests!

self._logger.debug(
"Received all expected packets", received=total_packets
)
break
Copy link
Member

Choose a reason for hiding this comment

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

It might be a good idea for now to implement an else: statement here with a logger warning if not all packets were successfully received. I am presuming that in a future update we will add back in the retransmit request protocol for grabbing missed packets.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem with putting an else here is that we can't know that we won't receive more packets after this. Since to see this message you will already be in DEBUG and see the individual packets come in with the debug line on 121.

gc.collect()
# all should be off from cubesat powermode

self.cubesat.f_softboot.toggle(True)
Copy link
Member

Choose a reason for hiding this comment

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

Just for new context on this, the f_softboot flag is there so the satellite can check on startup whether or not it is restarting gracefully or if it is coming back from a hard error (i.e. the watchdog forced a reset due to a software hang or other functional interrupt).

We right now are indeed not using it for anything, but in the future would probably want to bring it back somehow so the satellite can evaluate on reset whether it is fine to continue business as usual or if it suffered a critical fault and should enter a safe mode. There may be some interesting things to play around with here with the supervisor module as well.


self._radio.send(data=str(eval(args)))

def exec_cmd(self, cubesat: Satellite, args: str) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Although I can see how it makes sense to remove query and exec for safety reasons, I think it would be a good idea to plan to return some kind of functionality along these lines in the near future as arbitrary code execution is a nice little "escape hatch" to have access to for emergency updates to the software over the air.

Maybe we can open a ticket to look back into this at some future time.

Copy link
Member Author

Choose a reason for hiding this comment

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

This made me think that individual satellite teams may want to be able to add their own commands. I think we need a new design for CDH that allows teams to expand what it's capable of doing on their satellite. Something like being able to provide a list of commands you want to have on your satellite and associating a function that will fire when those commands are received.

cdh(existing args, ("my_special_command", function), ...)

@nateinaction nateinaction merged commit 1e89ea6 into main Jun 26, 2025
5 checks passed
@nateinaction nateinaction deleted the ngay-new-state-of-health branch June 26, 2025 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants