diff --git a/main.py b/main.py
index d95f3c8e4522dcf70db9c5a8a1f55f3c65fc1c20..818c2ff432058872e21d3fd781107b137f2f0379 100644
--- a/main.py
+++ b/main.py
@@ -6,7 +6,7 @@ import queue
 import base64
 
 import pygame
-from flask import request, Flask, Response, Request
+from flask import request, Flask
 
 import ledpatterns
 from absled import ILedString
@@ -14,6 +14,8 @@ import screenwidgets
 from config import PROP_AVAILABLE_WIDGETS, PROP_AVAILABLE_PATTERNS, PROP_WINDOW_SCALE, PROP_WINDOW_FULLSCREEN, \
     PROP_SCREEN_WIDTH, PROP_SCREEN_HEIGHT, PROP_HIDE_MOUSE, PROP_LEDS_DRIVER, PROP_LEDS_LENGTH, PROP_LEDS_LOCATION, \
     PROP_FLASK_DEBUG, PROP_FLASK_HOST, PROP_FLASK_PORT, hw_config
+from utils.jsonencoding import get_cfg_from_request, decode_dict
+from utils.net import json_response, verify_json_request
 
 app = Flask(__name__)
 
@@ -64,30 +66,17 @@ def load_sequences():
             widget_sequence = seq.get("widgets", widget_sequence)
             pattern_sequence = seq.get("patterns", pattern_sequence)
 
+def save_sequences():
+    with open("sequence_config.json", "w") as f:
+        json.dump({
+            "widgets": widget_sequence,
+            "patterns": pattern_sequence
+        }, f, indent=4)
+
 mainthread_queue = queue.Queue()
 display_queue = queue.Queue()
 led_queue = queue.Queue()
 
-def json_response(data, status=200):
-    return Response(json.dumps(data), status=status, mimetype="application/json")
-
-def verify_json_request(req: Request, required_fields=None):
-    if required_fields is None:
-        required_fields = []
-
-    if not req.is_json:
-        return False, json_response({
-            "error": "Request is not JSON. Please make sure mimetype is set to application/json."
-        }, 400)
-
-    for field in required_fields:
-        if field not in req.json:
-            return False, json_response({
-                "error": f"Missing field {field}"
-            }, 400)
-
-    return True, None
-
 
 ##############
 # Web Routes #
@@ -133,13 +122,10 @@ def wr_set_widget():
             "error": f"Widget {widget_name} is not available"
         }, 400)
 
-    config_override = request.json.get("config", {})
-    if type(config_override) is not dict:
-        return json_response({
-            "error": "Config must be an Object"
-        }, 400)
-
-    config_b64 = base64.b64encode(json.dumps(config_override).encode("utf-8")).decode("utf-8")
+    # Get the config (or return error)
+    config_b64, config_error = get_cfg_from_request(request)
+    if config_error is not None:
+        return config_error
 
     # Send the pattern to the LED thread
     display_queue.put("SET_WIDGET:" + widget_name + ":" + config_b64)
@@ -169,13 +155,10 @@ def wr_set_pattern():
             "error": f"Pattern {pattern_name} is not available"
         }, 400)
 
-    config_override = request.json.get("config", {})
-    if type(config_override) is not dict:
-        return json_response({
-            "error": "Config must be an Object"
-        }, 400)
-
-    config_b64 = base64.b64encode(json.dumps(config_override).encode("utf-8")).decode("utf-8")
+    # Get the config (or return error)
+    config_b64, config_response = get_cfg_from_request(request)
+    if config_response is not None:
+        return config_response
 
     # Send the pattern to the LED thread
     led_queue.put("SET_PATTERN:" + pattern_name + ":" + config_b64)
@@ -377,10 +360,28 @@ if __name__ == "__main__":
         while True:
             try:
                 message = mainthread_queue.get_nowait()
+                parts = message.split(":")
                 if message == "EXIT":
                     display_queue.put("EXIT")
                     led_queue.put("EXIT")
                     break
+                elif parts[0] == "UPDATE_ALL_MODULE_CFG" and len(parts) == 2:
+                    module_config = decode_dict(parts[1])
+                    save_module_config()
+                elif parts[0] == "SAVE_WIDGET_SEQUENCE_CFG" and len(parts) == 2:
+                    widget_sequence = decode_dict(parts[1])
+                    with open("sequence_config.json", "w") as f:
+                        json.dump({
+                            "widgets": widget_sequence,
+                            "patterns": pattern_sequence
+                        }, f, indent=4)
+                elif parts[0] == "SAVE_PATTERN_SEQUENCE_CFG" and len(parts) == 2:
+                    pattern_sequence = decode_dict(parts[1])
+                    with open("sequence_config.json", "w") as f:
+                        json.dump({
+                            "widgets": widget_sequence,
+                            "patterns": pattern_sequence
+                        }, f, indent=4)
             except queue.Empty:
                 pass
             # Keep main thread alive
diff --git a/utils/jsonencoding.py b/utils/jsonencoding.py
new file mode 100644
index 0000000000000000000000000000000000000000..b9baf2a244e930e6e3edada5ff30e1a3abe1faa7
--- /dev/null
+++ b/utils/jsonencoding.py
@@ -0,0 +1,21 @@
+import base64
+import json
+
+from main import json_response
+
+
+def encode_dict(d: dict|list) -> str:
+    return base64.b64encode(json.dumps(d).encode("utf-8")).decode("utf-8")
+
+def decode_dict(s: str) -> dict|list:
+    return json.loads(base64.b64decode(s.encode("utf-8")).decode("utf-8"))
+
+def get_cfg_from_request(request):
+    config_override = request.json.get("config", {})
+    if type(config_override) is not dict:
+        return None, json_response({
+            "error": "Config must be an Object"
+        }, 400)
+
+    config_b64 = encode_dict(config_override)
+    return config_b64, None
\ No newline at end of file
diff --git a/utils/net.py b/utils/net.py
index ffc3f0e986255800d9c029771bf8cda9708c9676..8bdebb5560d39c656ba617fb5e25e29b0de622b3 100644
--- a/utils/net.py
+++ b/utils/net.py
@@ -1,7 +1,15 @@
+import json
 import socket
+from typing import Any
 
+from flask import Response, Request
 
-def get_ip():
+
+def get_ip() -> str:
+    """
+    Gets the IP address of the current machine
+    :return: the IP address as a string
+    """
     s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
     s.settimeout(0)
     try:
@@ -12,4 +20,38 @@ def get_ip():
         ip_addr = '127.0.0.1'
     finally:
         s.close()
-    return ip_addr
\ No newline at end of file
+    return ip_addr
+
+
+def json_response(data: Any, status=200) -> Response:
+    """
+    Generate a JSON response from any json-serializable data
+    :param data: The data to be sent
+    :param status: The status code of the response
+    :return: A JSON response
+    """
+    return Response(json.dumps(data), status=status, mimetype="application/json")
+
+
+def verify_json_request(req: Request, required_fields: list|None = None) -> tuple[bool, Response|None]:
+    """
+    Verify that a request is JSON and contains all required fields
+    :param req: The request to verify
+    :param required_fields: A list of required fields, or None
+    :return: A tuple containing a boolean indicating if the request is valid and a response if it is not
+    """
+    if required_fields is None:
+        required_fields = []
+
+    if not req.is_json:
+        return False, json_response({
+            "error": "Request is not JSON. Please make sure mimetype is set to application/json."
+        }, 400)
+
+    for field in required_fields:
+        if field not in req.json:
+            return False, json_response({
+                "error": f"Missing field {field}"
+            }, 400)
+
+    return True, None
\ No newline at end of file