diff --git a/utils/tuning/libtuning/modules/agc/__init__.py b/utils/tuning/libtuning/modules/agc/__init__.py
new file mode 100644
index 000000000..4db9ca371
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+
+from libtuning.modules.agc.agc import AGC
+from libtuning.modules.agc.rkisp1 import AGCRkISP1
diff --git a/utils/tuning/libtuning/modules/agc/agc.py b/utils/tuning/libtuning/modules/agc/agc.py
new file mode 100644
index 000000000..9c8899bad
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/agc.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+
+from ..module import Module
+
+import libtuning as lt
+
+
+class AGC(Module):
+    type = 'agc'
+    hr_name = 'AGC (Base)'
+    out_name = 'GenericAGC'
+
+    # \todo Add sector shapes and stuff just like lsc
+    def __init__(self, *,
+                 debug: list):
+        super().__init__()
+
+        self.debug = debug
diff --git a/utils/tuning/libtuning/modules/agc/rkisp1.py b/utils/tuning/libtuning/modules/agc/rkisp1.py
new file mode 100644
index 000000000..4ac4d8ce7
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/rkisp1.py
@@ -0,0 +1,112 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+#
+# rkisp1.py - AGC module for tuning rkisp1
+
+from .agc import AGC
+
+import libtuning as lt
+
+
+class AGCRkISP1(AGC):
+    hr_name = 'AGC (RkISP1)'
+    out_name = 'Agc'
+    # \todo Not sure if this is useful. Probably will remove later.
+    compatible = ['rkisp1']
+
+    def __init__(self, *, **kwargs):
+        super().__init__(**kwargs)
+
+    # We don't actually need anything from the config file
+    def validate_config(self, config: dict) -> bool:
+        return True
+
+    def _generate_metering_modes(self) -> dict:
+        centre_weighted = {
+                'v10': [
+                    0, 0,  0, 0, 0,
+                    0, 6,  8, 6, 0,
+                    0, 8, 16, 8, 0,
+                    0, 6,  8, 6, 0,
+                    0, 0,  0, 0, 0
+                ],
+
+                'v12': [
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                    0, 0, 0, 3,  4, 3, 0, 0, 0,
+                    0, 0, 3, 6,  8, 6, 3, 0, 0,
+                    0, 0, 4, 8, 16, 8, 4, 0, 0,
+                    0, 0, 3, 6,  8, 6, 3, 0, 0,
+                    0, 0, 0, 3,  4, 3, 0, 0, 0,
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                ]
+        }
+
+        spot = {
+                'v10': [
+                    0, 0,  0, 0, 0,
+                    0, 2,  4, 2, 0,
+                    0, 4, 16, 4, 0,
+                    0, 2,  4, 2, 0,
+                    0, 0,  0, 0, 0
+                ],
+
+                'v12': [
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                    0, 0, 0, 1,  2, 1, 0, 0, 0,
+                    0, 0, 1, 2,  4, 2, 1, 0, 0,
+                    0, 0, 2, 4, 16, 4, 2, 0, 0,
+                    0, 0, 1, 2,  4, 2, 1, 0, 0,
+                    0, 0, 0, 1,  2, 1, 0, 0, 0,
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                    0, 0, 0, 0,  0, 0, 0, 0, 0,
+                ]
+        }
+
+        matrix = {
+                'v10': [1 for i in range(0, 25)],
+                'v12': [1 for i in range(0, 81)]
+        }
+
+        return {
+            'MeteringCentreWeighted': centre_weighted,
+            'MeteringSpot': spot,
+            'MeteringMatrix': matrix
+        }
+
+    def _generate_exposure_modes(self) -> dict:
+        normal = { 'shutter': [100, 10000, 30000, 60000, 120000],
+                   'gain': [1.0, 2.0, 4.0, 6.0, 6.0] }
+        short = { 'shutter': [100, 5000, 10000, 20000, 120000],
+                  'gain': [1.0, 2.0, 4.0, 6.0, 6.0]}
+
+        return { 'ExposureNormal': normal, 'ExposureShort': short }
+
+    def _generate_constraint_modes(self) -> dict:
+        normal = { 'lower': { 'qLo': 0.98, 'qHi': 1.0, 'yTarget': [ 0, 0.5, 1000, 0.5 ] } }
+        highlight = {
+            'lower': { 'qLo': 0.98, 'qHi': 1.0, 'yTarget': [ 0, 0.5, 1000, 0.5 ] },
+            'upper': { 'qLo': 0.98, 'qHi': 1.0, 'yTarget': [ 0, 0.8, 1000, 0.5 ] }
+        }
+
+        return { 'ConstraintNormal': normal, 'ConstraintHighlight': highlight }
+
+    def _generate_y_target(self) -> list:
+        return [0, 0.16, 1000, 0.165, 10000, 0.17]
+
+    def process(self, config: dict, images: list, outputs: dict) -> dict:
+        output = {}
+
+        output['AeMeteringMode'] = self._generate_metering_modes()
+        output['AeExposureMode'] = self._generate_exposure_modes()
+        output['AeConstraintMode'] = self._generate_constraint_modes()
+        output['relativeLuminanceTarget'] = self._generate_y_target()
+
+        # \todo Debug functionality
+
+        return output
