Repository: proveskit/pysquared

Issue:
[FEATURE] Config 2.0 - Improved type hinting
<!-- New feature template !-->
## Story
* **As a** `circuitpy_fligh_software` developer
* **I want** to be able to know what config is available and what type to expect directly from my IDE
* **So that** I can be more efficient and safer while accessing config

## Acceptance Criteria

- [ ] Access to specific config looks something like `config.radio.sender_id`
- [ ] My IDE knows that `config.radio.sender_id` is an int

## Technical Details
After working with the config implementation on Sunday I want us to take another look at this implementation to see if we can squeeze a bit more developer safety and efficiency out of it.

Right now we access config with something like `config.get_dict("radio_cfg")["sender_id"]`. I'd like to move to something closer to a golang style unmarshall into a struct. Unmarshalling to a struct has the benefit of having known data types that can be accessed with something closer to `config.radio.sender_id`. We might also be able to add validation to each config member. Is there some way to design something like this in python?

In a perfect world we could use `dataclasses` but they're not available in circuitpython. That implementation might look something like this:
```py
from dataclasses import dataclass                                                                                       
                                                                                                                         
@dataclass                                                                                                              
class RadioConfig:                                                                                                      
   sender_id: int
   ...

@dataclass                                                                                                              
class Config:                                                                                                           
   cubesat_name: str
   ...

   @classmethod                                                                                                        
   def from_json(cls, config_path: str) -> "Config":
       with open("config.json", "r") as f:                                                                                 
         json_data = json.load(f)   
       # Convert the nested radio_cfg dict to RadioConfig object                                                       
       radio_cfg = RadioConfig(**json_data["radio_cfg"])                                                               
       json_data["radio_cfg"] = radio_cfg                                                                              
       return cls(**json_data)
```

This approach has several benefits:                                                                                

1. Type hints and IDE autocompletion                                                                                     
2. Automatic validation on initialization                                                                                
3. Immutable configuration (with `@dataclass(frozen=True)`)!!                                                   
4. Easy to extend with new validation rules                                                                              
5. Type checking tools like mypy can verify correct usage 

Without `dataclasses` we will need to do something more verbose. Maybe something like this?
```py
class RadioConfig:                                                                                                      
     def __init__(self, config_dict: dict) -> None:                                                                      
         self.sender_id: int = config_dict["sender_id"]
         ...

class Config:                                                                                                           
     def __init__(self, config_path: str) -> None:                                                                                         
         with open(config_path, "r") as f:                                                                             
             json_data = json.loads(f.read())                                                                            
                                                                                                                         
         self.cubesat_name: str = json_data["cubesat_name"]
         ...
```

We lose automatic type validation on init, and my beloved immutability but we keep most of the features.

Hints from issue comments:

Solution patch:
diff --git a/config.example.json b/config.example.json
index 51294d4..1787326 100644
--- a/config.example.json
+++ b/config.example.json
@@ -14,30 +14,29 @@
   "heating": false,
   "orpheus": true,
   "is_licensed": false,
-  "NORMAL_TEMP": 20,
-  "NORMAL_BATT_TEMP": 1,
-  "NORMAL_MICRO_TEMP": 20,
-  "NORMAL_CHARGE_CURRENT": 0.5,
-  "NORMAL_BATTERY_VOLTAGE": 6.9,
-  "CRITICAL_BATTERY_VOLTAGE": 6.6,
-  "vlowbatt": 6.0,
+  "normal_temp": 20,
+  "normal_battery_temp": 1,
+  "normal_micro_temp": 20,
+  "normal_charge_current": 0.5,
+  "normal_battery_voltage": 6.9,
+  "critical_battery_voltage": 6.6,
   "battery_voltage": 3.3,
   "current_draw": 240.5,
-  "REBOOT_TIME": 3600,
+  "reboot_time": 3600,
   "turbo_clock": false,
   "radio_cfg": {
     "sender_id": 251,
     "receiver_id": 250,
     "transmit_frequency": 437.4,
-    "LoRa_spreading_factor": 8,
+    "lora_spreading_factor": 8,
     "transmit_bandwidth": 125,
-    "LoRa_coding_rate": 8,
+    "lora_coding_rate": 8,
     "transmit_power": 23,
     "start_time": 80000
   },
   "super_secret_code": "ABCD",
   "repeat_code": "RP",
-  "jokereply": [
+  "joke_reply": [
     "Your Mom"
   ]
 }
diff --git a/config.json b/config.json
index 744e76e..5d013b9 100644
--- a/config.json
+++ b/config.json
@@ -48,30 +48,29 @@
 "heating": false,
 "orpheus":true,
 "is_licensed":false,
-"NORMAL_TEMP": 20,
-"NORMAL_BATT_TEMP": 1,
-"NORMAL_MICRO_TEMP": 20,
-"NORMAL_CHARGE_CURRENT": 0.5,
-"NORMAL_BATTERY_VOLTAGE": 6.9,
-"CRITICAL_BATTERY_VOLTAGE": 6.6,
-"vlowbatt": 6.0,
+"normal_temp": 20,
+"normal_battery_temp": 1,
+"normal_micro_temp": 20,
+"normal_charge_current": 0.5,
+"normal_battery_voltage": 6.9,
+"critical_battery_voltage": 6.6,
 "battery_voltage": 5.2,
 "current_draw": 240.5,
-"REBOOT_TIME": 3600,
+"reboot_time": 3600,
 "turbo_clock": false,
 "radio_cfg": {
         "sender_id": 251,
         "receiver_id": 250,
         "transmit_frequency": 437.4,
-        "LoRa_spreading_factor": 8,
+        "lora_spreading_factor": 8,
         "transmit_bandwidth": 125,
-        "LoRa_coding_rate": 8,
+        "lora_coding_rate": 8,
         "transmit_power": 23,
         "start_time": 80000
     },
 "super_secret_code": "ABCD",
 "repeat_code": "RP",
-"jokereply": [
+"joke_reply": [
         "Your Mom",
         "Your Mum",
         "Your Face",
diff --git a/lib/pysquared/cdh.py b/lib/pysquared/cdh.py
index c600628..4c42752 100644
--- a/lib/pysquared/cdh.py
+++ b/lib/pysquared/cdh.py
@@ -29,11 +29,9 @@ class CommandDataHandler:
             b"\xa5\xb4": "joke_reply",
             b"\x56\xc4": "FSK",
         }
-        self._jokereply: list[str] = config.get_list("jokereply")
-        self._super_secret_code: bytes = config.get_str("super_secret_code").encode(
-            "utf-8"
-        )
-        self._repeat_code: bytes = config.get_str("repeat_code").encode("utf-8")
+        self._joke_reply: list[str] = config.joke_reply
+        self._super_secret_code: bytes = config.super_secret_code.encode("utf-8")
+        self._repeat_code: bytes = config.repeat_code.encode("utf-8")
         self.logger.info(
             "The satellite has a super secret code!",
             super_secret_code=self._super_secret_code,
@@ -141,7 +139,7 @@ class CommandDataHandler:
         cubesat.f_fsk.toggle(True)
 
     def joke_reply(self, cubesat: Satellite) -> None:
-        joke: str = random.choice(self._jokereply)
+        joke: str = random.choice(self._joke_reply)
         self.logger.info("Sending joke reply", joke=joke)
         cubesat.radio1.send(joke)
 
diff --git a/lib/pysquared/config.py b/lib/pysquared/config.py
index eda49b7..cb720dc 100644
--- a/lib/pysquared/config.py
+++ b/lib/pysquared/config.py
@@ -3,135 +3,55 @@ Class for encapsulating config.json. The goal is to
 distribute these values across the files & variables
 that use them. Instantiation happens in main.
 
-Also it allow values to be set temporarily using setter
-function, and values can be set permanently using the
-saver functions. Following the FPrime model.
+Also it allow values to be set temporarily or permanently using the
+Attempting to follow the FPrime model.
 """
 
 import json
 
 
-class Config:
-    """
-    Constructor
-    """
-
-    def __init__(self) -> None:
-        # parses json & assigns data to variables
-        with open("config.json", "r") as f:
-            json_data = f.read()
-        self._config: dict = json.loads(json_data)
-
-    """
-    Categorized getter functions
-    """
-
-    def get_str(self, key: str) -> str:
-        """Gets a string value from the config dictionary"""
-        return self._config[key]
-
-    def get_int(self, key: str) -> int:
-        """Gets an int value from the config dictionary"""
-        return self._config[key]
-
-    def get_float(self, key: str) -> float:
-        """Gets a float value from the config dictionary"""
-        return self._config[key]
-
-    def get_bool(self, key: str) -> bool:
-        """Gets a bool value from the config dictionary"""
-        return self._config[key]
-
-    def get_list(self, key: str) -> list[str]:
-        """Gets a list value from the config dictionary"""
-        return self._config[key]
-
-    def get_dict(self, key: str) -> dict[str, float]:
-        """Gets a dictionary value from the config dictionary"""
-        return self._config[key]
-
-    """
-    Categorized setter functions
-    """
-
-    def set_str(self, key: str, value: str) -> None:
-        """Sets the string value in the config dictionary
-        Does not save value to disk, will not persist through reboots
-        """
-        self._config[key] = value
+class RadioConfig:
+    def __init__(self, radio_dict: dict) -> None:
+        self.sender_id: int = radio_dict["sender_id"]
+        self.receiver_id: int = radio_dict["receiver_id"]
+        self.transmit_frequency: float = radio_dict["transmit_frequency"]
+        self.lora_spreading_factor: int = radio_dict["lora_spreading_factor"]
+        self.transmit_bandwidth: int = radio_dict["transmit_bandwidth"]
+        self.lora_coding_rate: int = radio_dict["lora_coding_rate"]
+        self.transmit_power: int = radio_dict["transmit_power"]
+        self.start_time: int = radio_dict["start_time"]
 
-    def set_int(self, key: str, value: int) -> None:
-        """Sets the string value in the config dictionary
-        Does not save value to disk, will not persist through reboots
-        """
-        self._config[key] = value
 
-    def set_float(self, key: str, value: float) -> None:
-        """Sets the string value in the config dictionary
-        Does not save value to disk, will not persist through reboots
-        """
-        self._config[key] = value
-
-    def set_bool(self, key: str, value: bool) -> None:
-        """Sets the string value in the config dictionary
-        Does not save value to disk, will not persist through reboots
-        """
-        self._config[key] = value
-
-    def set_list(self, key: str, value: str) -> None:
-        """Adds the string value to the list specified by the key inside of the config dictionary
-        Does not save value to disk, will not persist through reboots
-        """
-        self._config[key].append(value)
-
-    def set_dict(self, config_key: str, dict_key: str, dict_value: float) -> None:
-        """Sets the float value to the dictionary inside of the config dictionary
-        Does not save value to disk, will not persist through reboots
-        """
-        self._config[config_key][dict_key] = dict_value
-
-    """
-    Categorized saver functions
-    """
-
-    def save_str(self, key: str, str_value: str) -> None:
-        """Saves the string value to config.json
-        Saves value to disk, will persist through reboots
-        """
-        # add logic to write back to config
-        pass
-
-    def save_int(self, key: str, int_value: int) -> None:
-        """Saves the int value to config.json
-        Saves value to disk, will persist through reboots
-        """
-        # add logic to write back to config
-        pass
-
-    def save_float(self, key: str, float_value: float) -> None:
-        """Saves the float value to config.json
-        Saves value to disk, will persist through reboots
-        """
-        # add logic to write back to config
-        pass
-
-    def save_bool(self, key: str, bool_value: bool) -> None:
-        """Saves the bool value to config.json
-        Saves value to disk, will persist through reboots
-        """
-        # add logic to write back to config
-        pass
-
-    def save_list(self, key: str, list_value: str) -> None:
-        """Saves the list value to the list in config.json
-        Saves value to disk, will persist through reboots
-        """
-        # add logic to write back to config
-        pass
-
-    def save_dict(self, config_key: str, dict_key: str, dict_value: float) -> None:
-        """Saves the float value to the dict in config.json
-        Saves value to disk, will persist through reboots
-        """
-        # add logic to write back to config
-        pass
+class Config:
+    def __init__(self, config_path: str) -> None:
+        # parses json & assigns data to variables
+        with open(config_path, "r") as f:
+            json_data = json.loads(f.read())
+
+        self.radio_cfg: RadioConfig = RadioConfig(json_data["radio_cfg"])
+        self.cubesat_name: str = json_data["cubesat_name"]
+        self.callsign: str = json_data["callsign"]
+        self.last_battery_temp: float = json_data["last_battery_temp"]
+        self.sleep_duration: int = json_data["sleep_duration"]
+        self.detumble_enable_z: bool = json_data["detumble_enable_z"]
+        self.detumble_enable_x: bool = json_data["detumble_enable_x"]
+        self.detumble_enable_y: bool = json_data["detumble_enable_y"]
+        self.jokes: list[str] = json_data["jokes"]
+        self.debug: bool = json_data["debug"]
+        self.legacy: bool = json_data["legacy"]
+        self.heating: bool = json_data["heating"]
+        self.orpheus: bool = json_data["orpheus"]
+        self.is_licensed: bool = json_data["is_licensed"]
+        self.normal_temp: int = json_data["normal_temp"]
+        self.normal_battery_temp: int = json_data["normal_battery_temp"]
+        self.normal_micro_temp: int = json_data["normal_micro_temp"]
+        self.normal_charge_current: float = json_data["normal_charge_current"]
+        self.normal_battery_voltage: float = json_data["normal_battery_voltage"]
+        self.critical_battery_voltage: float = json_data["critical_battery_voltage"]
+        self.battery_voltage: float = json_data["battery_voltage"]
+        self.current_draw: float = json_data["current_draw"]
+        self.reboot_time: int = json_data["reboot_time"]
+        self.turbo_clock: bool = json_data["turbo_clock"]
+        self.super_secret_code: str = json_data["super_secret_code"]
+        self.repeat_code: str = json_data["repeat_code"]
+        self.joke_reply: list[str] = json_data["joke_reply"]
diff --git a/lib/pysquared/functions.py b/lib/pysquared/functions.py
index 32bd265..7a1140e 100755
--- a/lib/pysquared/functions.py
+++ b/lib/pysquared/functions.py
@@ -9,30 +9,34 @@ import gc
 import random
 import time
 
-import alarm
-
 from lib.pysquared.battery_helper import BatteryHelper
 from lib.pysquared.config import Config
 from lib.pysquared.logger import Logger
 from lib.pysquared.packet_manager import PacketManager
 from lib.pysquared.packet_sender import PacketSender
 from lib.pysquared.pysquared import Satellite
+from lib.pysquared.sleep_helper import SleepHelper
 
 try:
-    from typing import List, Literal, OrderedDict, Union
+    from typing import List, OrderedDict, Union
 
-    import circuitpython_typing
 except Exception:
     pass
 
 
 class functions:
-    def __init__(self, cubesat: Satellite, logger: Logger, config: Config) -> None:
+    def __init__(
+        self,
+        cubesat: Satellite,
+        logger: Logger,
+        config: Config,
+        sleep_helper: SleepHelper,
+    ) -> None:
         self.logger: Logger = logger
         self.cubesat: Satellite = cubesat
         self.battery: BatteryHelper = BatteryHelper(cubesat, logger)
         self.logger.info("Initializing Functionalities")
-
+        self.sleep_helper = sleep_helper
         self.packet_manager: PacketManager = PacketManager(
             logger=self.logger, max_packet_size=128
         )
@@ -41,18 +45,18 @@ class functions:
         )
 
         self.config: Config = config
-        self.cubesat_name: str = config.get_str("cubesat_name")
+        self.cubesat_name: str = config.cubesat_name
         self.error_count: int = 0
         self.facestring: list = [None, None, None, None, None]
-        self.jokes: list[str] = config.get_list("jokes")
-        self.last_battery_temp: float = config.get_float("last_battery_temp")
-        self.sleep_duration: int = config.get_int("sleep_duration")
-        self.callsign: str = config.get_str("callsign")
+        self.jokes: list[str] = config.jokes
+        self.last_battery_temp: float = config.last_battery_temp
+        self.sleep_duration: int = config.sleep_duration
+        self.callsign: str = config.callsign
         self.state_of_health_part1: bool = False
 
-        self.detumble_enable_z: bool = config.get_bool("detumble_enable_z")
-        self.detumble_enable_x: bool = config.get_bool("detumble_enable_x")
-        self.detumble_enable_y: bool = config.get_bool("detumble_enable_y")
+        self.detumble_enable_z: bool = config.detumble_enable_z
+        self.detumble_enable_x: bool = config.detumble_enable_x
+        self.detumble_enable_y: bool = config.detumble_enable_y
 
     """
     Satellite Management Functions
@@ -61,22 +65,6 @@ class functions:
     def current_check(self) -> float:
         return self.cubesat.current_draw
 
-    def safe_sleep(self, duration: int = 15) -> None:
-        self.logger.info("Setting Safe Sleep Mode")
-
-        iterations: int = 0
-
-        while duration > 15 and iterations < 12:
-            time_alarm: circuitpython_typing.Alarm = alarm.time.TimeAlarm(
-                monotonic_time=time.monotonic() + 15
-            )
-
-            alarm.light_sleep_until_alarms(time_alarm)
-            duration -= 15
-            iterations += 1
-
-            self.cubesat.watchdog_pet()
-
     def listen_loiter(self) -> None:
         self.logger.debug("Listening for 10 seconds")
         self.cubesat.watchdog_pet()
@@ -86,7 +74,7 @@ class functions:
 
         self.logger.debug("Sleeping for 20 seconds")
         self.cubesat.watchdog_pet()
-        self.safe_sleep(self.sleep_duration)
+        self.sleep_helper.safe_sleep(self.sleep_duration)
         self.cubesat.watchdog_pet()
 
     """
@@ -407,27 +395,3 @@ class functions:
         except Exception as e:
             self.logger.error("Detumble error", err=e)
         self.cubesat.RGB = (100, 100, 50)
-
-    def Short_Hybernate(self) -> Literal[True]:
-        self.logger.debug("Short Hybernation Coming UP")
-        gc.collect()
-        # all should be off from cubesat powermode
-
-        self.cubesat.enable_rf.value = False
-        self.cubesat.f_softboot.toggle(True)
-        self.safe_sleep(120)
-
-        self.cubesat.enable_rf.value = True
-        return True
-
-    def Long_Hybernate(self) -> Literal[True]:
-        self.logger.debug("LONG Hybernation Coming UP")
-        gc.collect()
-        # all should be off from cubesat powermode
-
-        self.cubesat.enable_rf.value = False
-        self.cubesat.f_softboot.toggle(True)
-        self.safe_sleep(600)
-
-        self.cubesat.enable_rf.value = True
-        return True
diff --git a/lib/pysquared/pysquared.py b/lib/pysquared/pysquared.py
index 4aa51d6..047a397 100644
--- a/lib/pysquared/pysquared.py
+++ b/lib/pysquared/pysquared.py
@@ -142,7 +142,7 @@ class Satellite:
                 self.spi0,
                 _rf_cs1,
                 _rf_rst1,
-                self.radio_cfg["transmit_frequency"],
+                self.config.radio_cfg.transmit_frequency,
                 # code_rate=8, code rate does not exist for RFM9xFSK
             )
             self.radio1.fsk_node_address = 1
@@ -155,19 +155,19 @@ class Satellite:
                 self.spi0,
                 _rf_cs1,
                 _rf_rst1,
-                self.radio_cfg["transmit_frequency"],
+                self.config.radio_cfg.transmit_frequency,
                 # code_rate=8, code rate does not exist for RFM9xFSK
             )
             self.radio1.max_output = True
-            self.radio1.tx_power = self.radio_cfg["transmit_power"]
-            self.radio1.spreading_factor = self.radio_cfg["LoRa_spreading_factor"]
+            self.radio1.tx_power = self.config.radio_cfg.transmit_power
+            self.radio1.spreading_factor = self.config.radio_cfg.lora_spreading_factor
 
             self.radio1.enable_crc = True
             self.radio1.ack_delay = 0.2
             if self.radio1.spreading_factor > 9:
                 self.radio1.preamble_length = self.radio1.spreading_factor
-        self.radio1.node = self.radio_cfg["sender_id"]
-        self.radio1.destination = self.radio_cfg["receiver_id"]
+        self.radio1.node = self.config.radio_cfg.sender_id
+        self.radio1.destination = self.config.radio_cfg.receiver_id
         self.hardware[hardware_key] = True
 
         # if self.legacy:
@@ -217,32 +217,32 @@ class Satellite:
             return
 
     def __init__(self, config: Config, logger: Logger) -> None:
-        self.cubesat_name: str = config.get_str("cubesat_name")
+        # here assiging config to a var so 'init_radio' function can
+        # access 'radio_cfg' inside config
+        self.config: Config = config
+        self.cubesat_name: str = config.cubesat_name
         """
         Big init routine as the whole board is brought up. Starting with config variables.
         """
-        self.legacy: bool = config.get_bool("legacy")
-        self.heating: bool = config.get_bool("heating")
-        self.orpheus: bool = config.get_bool("orpheus")  # maybe change var name
-        self.is_licensed: bool = config.get_bool("is_licensed")
+        self.legacy: bool = config.legacy
+        self.heating: bool = config.heating
+        self.orpheus: bool = config.orpheus  # maybe change var name
+        self.is_licensed: bool = config.is_licensed
         self.logger = logger
 
         """
         Define the normal power modes
         """
-        self.NORMAL_TEMP: int = config.get_int("NORMAL_TEMP")
-        self.NORMAL_BATT_TEMP: int = config.get_int("NORMAL_BATT_TEMP")
-        self.NORMAL_MICRO_TEMP: int = config.get_int("NORMAL_MICRO_TEMP")
-        self.NORMAL_CHARGE_CURRENT: float = config.get_float("NORMAL_CHARGE_CURRENT")
-        self.NORMAL_BATTERY_VOLTAGE: float = config.get_float("NORMAL_BATTERY_VOLTAGE")
-        self.CRITICAL_BATTERY_VOLTAGE: float = config.get_float(
-            "CRITICAL_BATTERY_VOLTAGE"
-        )
-        self.vlowbatt: float = config.get_float("vlowbatt")
-        self.battery_voltage: float = config.get_float("battery_voltage")
-        self.current_draw: float = config.get_float("current_draw")
-        self.REBOOT_TIME: int = config.get_int("REBOOT_TIME")
-        self.turbo_clock: bool = config.get_bool("turbo_clock")
+        self.normal_temp: int = config.normal_temp
+        self.normal_battery_temp: int = config.normal_battery_temp
+        self.normal_micro_temp: int = config.normal_micro_temp
+        self.normal_charge_current: float = config.normal_charge_current
+        self.normal_battery_voltage: float = config.normal_battery_voltage
+        self.critical_battery_voltage: float = config.critical_battery_voltage
+        self.battery_voltage: float = config.battery_voltage
+        self.current_draw: float = config.current_draw
+        self.reboot_time: int = config.reboot_time
+        self.turbo_clock: bool = config.turbo_clock
 
         """
         Setting up data buffers
@@ -278,8 +278,6 @@ class Satellite:
         self.CURRENTTIME: int = self.BOOTTIME
         self.UPTIME: int = 0
 
-        self.radio_cfg: dict[str, float] = config.get_dict("radio_cfg")
-
         self.hardware: OrderedDict[str, bool] = OrderedDict(
             [
                 ("I2C0", False),
@@ -657,7 +655,7 @@ class Satellite:
     def check_reboot(self) -> None:
         self.UPTIME: int = self.uptime
         self.logger.debug("Current up time stat:", uptime=self.UPTIME)
-        if self.UPTIME > self.REBOOT_TIME:
+        if self.UPTIME > self.reboot_time:
             self.micro.reset()
 
     def powermode(self, mode: str) -> None:
diff --git a/lib/pysquared/sleep_helper.py b/lib/pysquared/sleep_helper.py
new file mode 100644
index 0000000..070c637
--- /dev/null
+++ b/lib/pysquared/sleep_helper.py
@@ -0,0 +1,107 @@
+import gc
+import time
+
+import alarm
+import digitalio
+
+from lib.pysquared.logger import Logger
+from lib.pysquared.pysquared import Satellite
+
+try:
+    from typing import Literal
+
+    import circuitpython_typing
+except Exception:
+    pass
+
+
+class SleepHelper:
+    """
+    Class responsible for sleeping the Satellite to conserve power
+    """
+
+    def __init__(self, cubesat: Satellite, logger: Logger):
+        """
+        Creates a SleepHelper object.
+
+        :param cubesat: The Satellite object
+        :param logger: The Logger object allowing for log output
+
+        """
+        self.cubesat: Satellite = cubesat
+        self.logger: Logger = logger
+
+    def safe_sleep(self, duration: int = 15) -> None:
+        """
+        Puts the Satellite to sleep for specified duration, in seconds.
+
+        Current implementation results in an actual sleep duration that is a multiple of 15.
+        Current implementation only allows for a maximum sleep duration of 180 seconds.
+
+        :param duration: Specified time, in seconds, to sleep the Satellite for
+        """
+
+        self.logger.info("Setting Safe Sleep Mode")
+
+        iterations: int = 0
+
+        while duration >= 15 and iterations < 12:
+            time_alarm: circuitpython_typing.Alarm = alarm.time.TimeAlarm(
+                monotonic_time=time.monotonic() + 15
+            )
+
+            alarm.light_sleep_until_alarms(time_alarm)
+            duration -= 15
+            iterations += 1
+
+            self.cubesat.watchdog_pet()
+
+    def short_hibernate(self) -> Literal[True]:
+        """Puts the Satellite to sleep for 120 seconds"""
+
+        self.logger.debug("Short Hibernation Coming UP")
+        gc.collect()
+        # all should be off from cubesat powermode
+
+        # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool.
+        if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut):
+            self.cubesat.enable_rf.value = False
+        else:
+            self.cubesat.enable_rf = False
+
+        self.cubesat.f_softboot.toggle(True)
+        self.cubesat.watchdog_pet()
+        self.safe_sleep(120)
+
+        # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool.
+        if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut):
+            self.cubesat.enable_rf.value = True
+        else:
+            self.cubesat.enable_rf = True
+
+        return True
+
+    def long_hibernate(self) -> Literal[True]:
+        """Puts the Satellite to sleep for 180 seconds"""
+
+        self.logger.debug("LONG Hibernation Coming UP")
+        gc.collect()
+        # all should be off from cubesat powermode
+
+        # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool.
+        if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut):
+            self.cubesat.enable_rf.value = False
+        else:
+            self.cubesat.enable_rf = False
+
+        self.cubesat.f_softboot.toggle(True)
+        self.cubesat.watchdog_pet()
+        self.safe_sleep(600)
+
+        # checking the type of self.cubesat.enable_rf, as it can be a DigitalInOut object or a bool.
+        if isinstance(self.cubesat.enable_rf, digitalio.DigitalInOut):
+            self.cubesat.enable_rf.value = True
+        else:
+            self.cubesat.enable_rf = True
+
+        return True
diff --git a/main.py b/main.py
index a8ccee6..33426d5 100644
--- a/main.py
+++ b/main.py
@@ -17,6 +17,7 @@ import lib.pysquared.pysquared as pysquared
 from lib.pysquared.config import Config
 from lib.pysquared.logger import Logger
 from lib.pysquared.nvm.counter import Counter
+from lib.pysquared.sleep_helper import SleepHelper
 
 logger: Logger = Logger(
     error_counter=Counter(index=register.ERRORCNT, datastore=microcontroller.nvm)
@@ -32,16 +33,17 @@ try:
         time.sleep(1)
 
     logger.debug("Initializing Config")
-    config: Config = Config()
+    config: Config = Config("config.json")
 
     c = pysquared.Satellite(config, logger)
     c.watchdog_pet()
+    sleep_helper = SleepHelper(c, logger)
 
     import gc  # Garbage collection
 
     import lib.pysquared.functions as functions
 
-    f = functions.functions(c, logger, config)
+    f = functions.functions(c, logger, config, sleep_helper)
 
     def initial_boot():
         c.watchdog_pet()
@@ -103,13 +105,13 @@ try:
         initial_boot()
         c.watchdog_pet()
 
-        f.Long_Hybernate()
+        sleep_helper.long_hibernate()
 
     def minimum_power_operations():
         initial_boot()
         c.watchdog_pet()
 
-        f.Short_Hybernate()
+        sleep_helper.short_hibernate()
 
     ######################### MAIN LOOP ##############################
     try:
diff --git a/repl.py b/repl.py
index 74bfd56..6ca9623 100644
--- a/repl.py
+++ b/repl.py
@@ -9,6 +9,6 @@ from lib.pysquared.pysquared import Satellite
 logger: Logger = Logger(
     error_counter=Counter(index=register.ERRORCNT, datastore=microcontroller.nvm)
 )
-config: Config = Config()
+config: Config = Config("config.json")
 logger.info("Initializing a cubesat object as `c` in the REPL...")
 c: Satellite = Satellite(config, logger)


Test patch:
diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py
index 5efdac5..cecadab 100644
--- a/tests/unit/test_config.py
+++ b/tests/unit/test_config.py
@@ -19,21 +19,20 @@ CONFIG_SCHEMA = {
     "heating": bool,
     "orpheus": bool,
     "is_licensed": bool,
-    "NORMAL_TEMP": int,
-    "NORMAL_BATT_TEMP": int,
-    "NORMAL_MICRO_TEMP": int,
-    "NORMAL_CHARGE_CURRENT": float,
-    "NORMAL_BATTERY_VOLTAGE": float,
-    "CRITICAL_BATTERY_VOLTAGE": float,
-    "vlowbatt": float,
+    "normal_temp": int,
+    "normal_battery_temp": int,
+    "normal_micro_temp": int,
+    "normal_charge_current": float,
+    "normal_battery_voltage": float,
+    "critical_battery_voltage": float,
     "battery_voltage": float,
     "current_draw": float,
-    "REBOOT_TIME": int,
+    "reboot_time": int,
     "turbo_clock": bool,
     "radio_cfg": dict,
     "super_secret_code": str,
     "repeat_code": str,
-    "jokereply": list,
+    "joke_reply": list,
 }
 
 
@@ -62,9 +61,8 @@ def validate_config(config: Dict[str, Any]) -> None:
     # Validate voltage ranges
     voltage_fields = [
         "battery_voltage",
-        "NORMAL_BATTERY_VOLTAGE",
-        "CRITICAL_BATTERY_VOLTAGE",
-        "vlowbatt",
+        "normal_battery_voltage",
+        "critical_battery_voltage",
     ]
     for field in voltage_fields:
         value = config[field]
@@ -76,7 +74,7 @@ def validate_config(config: Dict[str, Any]) -> None:
         raise ValueError("Current draw cannot be negative")
 
     # Validate time values
-    time_fields = ["sleep_duration", "REBOOT_TIME"]
+    time_fields = ["sleep_duration", "reboot_time"]
     for field in time_fields:
         if config[field] <= 0:
             raise ValueError(f"{field} must be positive")
@@ -87,9 +85,9 @@ def validate_config(config: Dict[str, Any]) -> None:
         "sender_id": int,
         "receiver_id": int,
         "transmit_frequency": float,
-        "LoRa_spreading_factor": int,
+        "lora_spreading_factor": int,
         "transmit_bandwidth": int,
-        "LoRa_coding_rate": int,
+        "lora_coding_rate": int,
         "transmit_power": int,
         "start_time": int,
     }
@@ -161,10 +159,9 @@ def test_field_types(config_data):
     # Test numeric fields
     float_fields = [
         "last_battery_temp",
-        "NORMAL_CHARGE_CURRENT",
-        "NORMAL_BATTERY_VOLTAGE",
-        "CRITICAL_BATTERY_VOLTAGE",
-        "vlowbatt",
+        "normal_charge_current",
+        "normal_battery_voltage",
+        "critical_battery_voltage",
         "current_draw",
         "battery_voltage",
     ]
@@ -173,10 +170,10 @@ def test_field_types(config_data):
 
     int_fields = [
         "sleep_duration",
-        "NORMAL_TEMP",
-        "NORMAL_BATT_TEMP",
-        "NORMAL_MICRO_TEMP",
-        "REBOOT_TIME",
+        "normal_temp",
+        "normal_battery_temp",
+        "normal_micro_temp",
+        "reboot_time",
     ]
     for field in int_fields:
         assert isinstance(config_data[field], int), f"{field} must be an integer"
@@ -197,7 +194,7 @@ def test_field_types(config_data):
         assert isinstance(config_data[field], bool), f"{field} must be a boolean"
 
     # Test list fields
-    list_fields = ["jokes", "jokereply"]
+    list_fields = ["jokes", "joke_reply"]
     for field in list_fields:
         assert isinstance(config_data[field], list), f"{field} must be a list"
         assert all(
@@ -210,9 +207,9 @@ def test_field_types(config_data):
         "sender_id": int,
         "receiver_id": int,
         "transmit_frequency": float,
-        "LoRa_spreading_factor": int,
+        "lora_spreading_factor": int,
         "transmit_bandwidth": int,
-        "LoRa_coding_rate": int,
+        "lora_coding_rate": int,
         "transmit_power": int,
         "start_time": int,
     }
@@ -226,9 +223,8 @@ def test_voltage_ranges(config_data):
     """Test that voltage values are within expected ranges."""
     voltage_fields = [
         "battery_voltage",
-        "NORMAL_BATTERY_VOLTAGE",
-        "CRITICAL_BATTERY_VOLTAGE",
-        "vlowbatt",
+        "normal_battery_voltage",
+        "critical_battery_voltage",
     ]
     for field in voltage_fields:
         value = config_data[field]
@@ -238,7 +234,7 @@ def test_voltage_ranges(config_data):
 def test_time_values(config_data):
     """Test that time values are positive."""
     assert config_data["sleep_duration"] > 0, "sleep_duration must be positive"
-    assert config_data["REBOOT_TIME"] > 0, "REBOOT_TIME must be positive"
+    assert config_data["reboot_time"] > 0, "reboot_time must be positive"
 
 
 def test_current_draw_positive(config_data):
@@ -249,10 +245,10 @@ def test_current_draw_positive(config_data):
 def test_lists_not_empty(config_data):
     """Test that list fields are not empty."""
     assert len(config_data["jokes"]) > 0, "jokes list cannot be empty"
-    assert len(config_data["jokereply"]) > 0, "jokereply list cannot be empty"
+    assert len(config_data["joke_reply"]) > 0, "joke_reply list cannot be empty"
     assert all(
         isinstance(joke, str) for joke in config_data["jokes"]
     ), "All jokes must be strings"
     assert all(
-        isinstance(reply, str) for reply in config_data["jokereply"]
+        isinstance(reply, str) for reply in config_data["joke_reply"]
     ), "All joke replies must be strings"