Sfoglia il codice sorgente

Add connector images for console port types

Include generated drawings for console port types (shared by console ports and
console server ports), and display them in the console port forms, detail
panels, and cable termination panels.
Jeremy Stretch 8 ore fa
parent
commit
f17811f71c

+ 14 - 14
netbox/dcim/choices.py

@@ -327,22 +327,22 @@ class ConsolePortTypeChoices(ChoiceSet):
 
     CHOICES = (
         ('Serial', (
-            Choice(TYPE_DE9, 'DE-9'),
-            Choice(TYPE_DB25, 'DB-25'),
-            Choice(TYPE_RJ11, 'RJ-11'),
-            Choice(TYPE_RJ12, 'RJ-12'),
-            Choice(TYPE_RJ45, 'RJ-45'),
-            Choice(TYPE_MINI_DIN_8, 'Mini-DIN 8'),
+            Choice(TYPE_DE9, 'DE-9', image='connectors/console-port/de-9.svg'),
+            Choice(TYPE_DB25, 'DB-25', image='connectors/console-port/db-25.svg'),
+            Choice(TYPE_RJ11, 'RJ-11', image='connectors/console-port/rj-11.svg'),
+            Choice(TYPE_RJ12, 'RJ-12', image='connectors/console-port/rj-12.svg'),
+            Choice(TYPE_RJ45, 'RJ-45', image='connectors/console-port/rj-45.svg'),
+            Choice(TYPE_MINI_DIN_8, 'Mini-DIN 8', image='connectors/console-port/mini-din-8.svg'),
         )),
         ('USB', (
-            Choice(TYPE_USB_A, 'USB Type A'),
-            Choice(TYPE_USB_B, 'USB Type B'),
-            Choice(TYPE_USB_C, 'USB Type C'),
-            Choice(TYPE_USB_MINI_A, 'USB Mini A'),
-            Choice(TYPE_USB_MINI_B, 'USB Mini B'),
-            Choice(TYPE_USB_MICRO_A, 'USB Micro A'),
-            Choice(TYPE_USB_MICRO_B, 'USB Micro B'),
-            Choice(TYPE_USB_MICRO_AB, 'USB Micro AB'),
+            Choice(TYPE_USB_A, 'USB Type A', image='connectors/console-port/usb-a.svg'),
+            Choice(TYPE_USB_B, 'USB Type B', image='connectors/console-port/usb-b.svg'),
+            Choice(TYPE_USB_C, 'USB Type C', image='connectors/console-port/usb-c.svg'),
+            Choice(TYPE_USB_MINI_A, 'USB Mini A', image='connectors/console-port/usb-mini-a.svg'),
+            Choice(TYPE_USB_MINI_B, 'USB Mini B', image='connectors/console-port/usb-mini-b.svg'),
+            Choice(TYPE_USB_MICRO_A, 'USB Micro A', image='connectors/console-port/usb-micro-a.svg'),
+            Choice(TYPE_USB_MICRO_B, 'USB Micro B', image='connectors/console-port/usb-micro-b.svg'),
+            Choice(TYPE_USB_MICRO_AB, 'USB Micro AB', image='connectors/console-port/usb-micro-ab.svg'),
         )),
         ('Other', (
             Choice(TYPE_OTHER, 'Other'),

+ 11 - 1
netbox/dcim/forms/bulk_create.py

@@ -1,7 +1,7 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
-from dcim.choices import PowerOutletTypeChoices, PowerPortTypeChoices
+from dcim.choices import ConsolePortTypeChoices, PowerOutletTypeChoices, PowerPortTypeChoices
 from dcim.models import *
 from extras.models import Tag
 from netbox.forms.mixins import ChangelogMessageMixin, CustomFieldsMixin
@@ -54,6 +54,11 @@ class ConsolePortBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     model = ConsolePort
+    type = TypedChoiceField(
+        label=_('Type'),
+        choices=add_blank_choice(ConsolePortTypeChoices),
+        required=False,
+    )
     field_order = ('name', 'label', 'type', 'mark_connected', 'description', 'tags')
 
 
@@ -62,6 +67,11 @@ class ConsoleServerPortBulkCreateForm(
     DeviceBulkAddComponentForm
 ):
     model = ConsoleServerPort
+    type = TypedChoiceField(
+        label=_('Type'),
+        choices=add_blank_choice(ConsolePortTypeChoices),
+        required=False,
+    )
     field_order = ('name', 'label', 'type', 'speed', 'description', 'tags')
 
 

+ 4 - 4
netbox/dcim/forms/filtersets.py

@@ -1718,7 +1718,7 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
         FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
         FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
     )
-    type = forms.MultipleChoiceField(
+    type = MultipleChoiceField(
         label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False
@@ -1738,7 +1738,7 @@ class ConsolePortTemplateFilterForm(ModularDeviceComponentTemplateFilterForm):
         FieldSet('name', 'label', 'type', name=_('Attributes')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
-    type = forms.MultipleChoiceField(
+    type = MultipleChoiceField(
         label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False
@@ -1758,7 +1758,7 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
         FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
         FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
     )
-    type = forms.MultipleChoiceField(
+    type = MultipleChoiceField(
         label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False
@@ -1778,7 +1778,7 @@ class ConsoleServerPortTemplateFilterForm(ModularDeviceComponentTemplateFilterFo
         FieldSet('name', 'label', 'type', name=_('Attributes')),
         FieldSet('device_type_id', 'module_type_id', name=_('Device')),
     )
-    type = forms.MultipleChoiceField(
+    type = MultipleChoiceField(
         label=_('Type'),
         choices=ConsolePortTypeChoices,
         required=False

+ 6 - 0
netbox/dcim/models/device_components.py

@@ -469,6 +469,9 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, Tracki
         verbose_name = _('console port')
         verbose_name_plural = _('console ports')
 
+    def get_type_image(self):
+        return ConsolePortTypeChoices.images.get(self.type)
+
 
 class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
     """
@@ -496,6 +499,9 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint,
         verbose_name = _('console server port')
         verbose_name_plural = _('console server ports')
 
+    def get_type_image(self):
+        return ConsolePortTypeChoices.images.get(self.type)
+
 
 #
 # Power components

+ 3 - 1
netbox/dcim/tests/test_models.py

@@ -4132,10 +4132,12 @@ class SiteSignalTestCase(TestCase):
         site.save()
 
 
-class PowerTypeImageTestCase(TestCase):
+class ComponentTypeImageTestCase(TestCase):
 
     def test_get_type_image(self):
         for model, choices, value in (
+            (ConsolePort, ConsolePortTypeChoices, ConsolePortTypeChoices.TYPE_DE9),
+            (ConsoleServerPort, ConsolePortTypeChoices, ConsolePortTypeChoices.TYPE_RJ45),
             (PowerPort, PowerPortTypeChoices, PowerPortTypeChoices.TYPE_IEC_C14),
             (PowerOutlet, PowerOutletTypeChoices, PowerOutletTypeChoices.TYPE_IEC_C13),
         ):

+ 4 - 0
netbox/project-static/img/connectors/console-port/db-25.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M4.61 29.72A2.92 2.92 0 0 1 7.49 26.28L56.51 26.28A2.92 2.92 0 0 1 59.39 29.72L58.41 35.3A2.92 2.92 0 0 1 55.53 37.72L8.47 37.72A2.92 2.92 0 0 1 5.59 35.3Z"/></g>
+<g fill="#000"><circle cx="7.7" cy="29.92" r="1.17"/><circle cx="11.75" cy="29.92" r="1.17"/><circle cx="15.8" cy="29.92" r="1.17"/><circle cx="19.85" cy="29.92" r="1.17"/><circle cx="23.9" cy="29.92" r="1.17"/><circle cx="27.95" cy="29.92" r="1.17"/><circle cx="32" cy="29.92" r="1.17"/><circle cx="36.05" cy="29.92" r="1.17"/><circle cx="40.1" cy="29.92" r="1.17"/><circle cx="44.15" cy="29.92" r="1.17"/><circle cx="48.2" cy="29.92" r="1.17"/><circle cx="52.25" cy="29.92" r="1.17"/><circle cx="56.3" cy="29.92" r="1.17"/><circle cx="9.72" cy="34.08" r="1.17"/><circle cx="13.77" cy="34.08" r="1.17"/><circle cx="17.82" cy="34.08" r="1.17"/><circle cx="21.87" cy="34.08" r="1.17"/><circle cx="25.92" cy="34.08" r="1.17"/><circle cx="29.97" cy="34.08" r="1.17"/><circle cx="34.03" cy="34.08" r="1.17"/><circle cx="38.08" cy="34.08" r="1.17"/><circle cx="42.13" cy="34.08" r="1.17"/><circle cx="46.18" cy="34.08" r="1.17"/><circle cx="50.23" cy="34.08" r="1.17"/><circle cx="54.28" cy="34.08" r="1.17"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/de-9.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M5.43 26.62A6.89 6.89 0 0 1 12.21 18.53L51.79 18.53A6.89 6.89 0 0 1 58.57 26.62L56.25 39.77A6.89 6.89 0 0 1 49.47 45.47L14.53 45.47A6.89 6.89 0 0 1 7.75 39.77Z"/></g>
+<g fill="#000"><circle cx="13.13" cy="27.11" r="2.76"/><circle cx="22.56" cy="27.11" r="2.76"/><circle cx="32" cy="27.11" r="2.76"/><circle cx="41.44" cy="27.11" r="2.76"/><circle cx="50.87" cy="27.11" r="2.76"/><circle cx="17.85" cy="36.89" r="2.76"/><circle cx="27.28" cy="36.89" r="2.76"/><circle cx="36.72" cy="36.89" r="2.76"/><circle cx="46.15" cy="36.89" r="2.76"/></g>
+</svg>

+ 5 - 0
netbox/project-static/img/connectors/console-port/mini-din-8.svg

@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><circle cx="32" cy="32" r="28"/></g>
+<g fill="none" stroke="#000" stroke-width="2" stroke-linejoin="round"><path d="M36.94 10.88A21.69 21.69 0 0 1 51.01 42.43L45.73 42.43L45.73 48.79A21.69 21.69 0 0 1 18.27 48.79L18.27 42.43L12.99 42.43A21.69 21.69 0 0 1 27.06 10.88L27.06 15.53L36.94 15.53Z"/></g>
+<g fill="#000"><circle cx="21.02" cy="23.22" r="3.02"/><circle cx="32" cy="23.22" r="3.02"/><circle cx="42.98" cy="23.22" r="3.02"/><circle cx="18.27" cy="34.2" r="3.02"/><circle cx="34.2" cy="34.2" r="3.02"/><circle cx="45.73" cy="34.2" r="3.02"/><circle cx="26.51" cy="45.18" r="3.02"/><circle cx="37.49" cy="45.18" r="3.02"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/rj-11.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M4 5.45L60 5.45L60 44.21L50.7 44.21L50.7 53.74L43.51 53.74L43.51 58.55L20.49 58.55L20.49 53.74L13.3 53.74L13.3 44.21L4 44.21Z"/></g>
+<g fill="#000"><path d="M27.56 5.56L30.68 5.56L30.68 19.16L27.56 19.16Z"/><path d="M33.32 5.56L36.44 5.56L36.44 19.16L33.32 19.16Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/rj-12.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M4 5.45L60 5.45L60 44.21L50.7 44.21L50.7 53.74L43.51 53.74L43.51 58.55L20.49 58.55L20.49 53.74L13.3 53.74L13.3 44.21L4 44.21Z"/></g>
+<g fill="#000"><path d="M16.04 5.56L19.16 5.56L19.16 19.16L16.04 19.16Z"/><path d="M21.8 5.56L24.92 5.56L24.92 19.16L21.8 19.16Z"/><path d="M27.56 5.56L30.68 5.56L30.68 19.16L27.56 19.16Z"/><path d="M33.32 5.56L36.44 5.56L36.44 19.16L33.32 19.16Z"/><path d="M39.08 5.56L42.2 5.56L42.2 19.16L39.08 19.16Z"/><path d="M44.84 5.56L47.96 5.56L47.96 19.16L44.84 19.16Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/rj-45.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M4 9.97L60 9.97L60 42.13L46.81 42.13L46.81 50.03L41.54 50.03L41.54 54.03L22.46 54.03L22.46 50.03L17.19 50.03L17.19 42.13L4 42.13Z"/></g>
+<g fill="#000"><path d="M13.99 10.07L16.57 10.07L16.57 21.35L13.99 21.35Z"/><path d="M18.76 10.07L21.35 10.07L21.35 21.35L18.76 21.35Z"/><path d="M23.54 10.07L26.13 10.07L26.13 21.35L23.54 21.35Z"/><path d="M28.32 10.07L30.9 10.07L30.9 21.35L28.32 21.35Z"/><path d="M33.1 10.07L35.68 10.07L35.68 21.35L33.1 21.35Z"/><path d="M37.87 10.07L40.46 10.07L40.46 21.35L37.87 21.35Z"/><path d="M42.65 10.07L45.24 10.07L45.24 21.35L42.65 21.35Z"/><path d="M47.43 10.07L50.01 10.07L50.01 21.35L47.43 21.35Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-a.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M5.4 21.5L58.6 21.5A1.4 1.4 0 0 1 60 22.9L60 41.1A1.4 1.4 0 0 1 58.6 42.5L5.4 42.5A1.4 1.4 0 0 1 4 41.1L4 22.9A1.4 1.4 0 0 1 5.4 21.5Z"/></g>
+<g fill="#000"><path d="M8.67 23.6L55.33 23.6L55.33 32.47L8.67 32.47Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-b.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M13.22 6.31L50.78 6.31L60 15.53L60 57.69L4 57.69L4 15.53Z"/></g>
+<g fill="#000"><path d="M14.87 24.09L49.13 24.09L49.13 45.18L14.87 45.18Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-c.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M12.67 23.33L51.33 23.33A8.67 8.67 0 0 1 60 32L60 32A8.67 8.67 0 0 1 51.33 40.67L12.67 40.67A8.67 8.67 0 0 1 4 32L4 32A8.67 8.67 0 0 1 12.67 23.33Z"/></g>
+<g fill="#000"><path d="M11 29.67L53 29.67A1 1 0 0 1 54 30.67L54 33.33A1 1 0 0 1 53 34.33L11 34.33A1 1 0 0 1 10 33.33L10 30.67A1 1 0 0 1 11 29.67Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-micro-a.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M5.62 24.49L58.38 24.49A1.62 1.62 0 0 1 60 26.12L60 37.88A1.62 1.62 0 0 1 58.38 39.51L5.62 39.51A1.62 1.62 0 0 1 4 37.88L4 26.12A1.62 1.62 0 0 1 5.62 24.49Z"/></g>
+<g fill="#000"><path d="M11.71 26.52L52.29 26.52L52.29 30.17L11.71 30.17Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-micro-ab.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M5.62 24.49L58.38 24.49A1.62 1.62 0 0 1 60 26.12L60 37.88A1.62 1.62 0 0 1 58.38 39.51L5.62 39.51A1.62 1.62 0 0 1 4 37.88L4 26.12A1.62 1.62 0 0 1 5.62 24.49Z"/></g>
+<g fill="#000"><path d="M11.71 26.52L52.29 26.52L52.29 30.17L11.71 30.17Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-micro-b.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M60 24.49L60 33.83L54.32 39.51L9.68 39.51L4 33.83L4 24.49Z"/></g>
+<g fill="#000"><path d="M11.71 26.52L52.29 26.52L52.29 30.17L11.71 30.17Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-mini-a.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M4 20L60 20L60 34.4A9.6 9.6 0 0 1 50.4 44L13.6 44A9.6 9.6 0 0 1 4 34.4Z"/></g>
+<g fill="#000"><path d="M12 24.8L52 24.8L52 29.6L12 29.6Z"/></g>
+</svg>

+ 4 - 0
netbox/project-static/img/connectors/console-port/usb-mini-b.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
+<g fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"><path d="M4 20L60 20L60 33.6L55.2 33.6L50.4 44L13.6 44L8.8 33.6L4 33.6Z"/></g>
+<g fill="#000"><path d="M12 24.8L52 24.8L52 29.6L12 29.6Z"/></g>
+</svg>

+ 81 - 3
scripts/generate_connector_images.py

@@ -1,10 +1,10 @@
 #!/usr/bin/env python3
 """
-Generate the connector drawings displayed for power port and power outlet types.
+Generate the connector drawings displayed for power port, power outlet, and console port types.
 
 Each drawing depicts the mating face of a connector as a 64x64 SVG: the face is outlined and its contacts are
-filled solid black, on a transparent background (the UI inverts the drawings in dark mode). Outlets are drawn as
-seen from the front; ports (plugs and inlets) are drawn as the mirror image of the face they mate with. Geometry
+filled solid black, on a transparent background (the UI inverts the drawings in dark mode). Receptacles are drawn
+as seen from the front; plugs and inlets are drawn as the mirror image of the face they mate with. Geometry
 is defined in millimetres (x right, y down, origin at the face's centre) and each face is scaled to fill the frame,
 so relative physical size is not conveyed. Contacts too small to read at icon size may be exaggerated.
 
@@ -707,6 +707,63 @@ def ita_o(socket=True):
     )
 
 
+#
+# Console (D-subminiature, modular jacks, and Mini-DIN)
+#
+
+def d_sub(contacts, width, pitch):
+    """A D-subminiature connector with two staggered rows of contacts (the longer row along the wide side)."""
+    row, height = 2.84, 7.82
+    top = (contacts + 1) // 2
+    inset = height * math.tan(math.radians(10))
+    outline = rounded_polygon(
+        [(-width / 2, -height / 2), (width / 2, -height / 2), (width / 2 - inset, height / 2),
+         (-width / 2 + inset, height / 2)],
+        2,
+    )
+    drawing = Drawing(width, height).outline(outline)
+    for i in range(top):
+        drawing.fill(Circle((i - (top - 1) / 2) * pitch, -row / 2, 0.8))
+    for i in range(contacts - top):
+        drawing.fill(Circle((i - (contacts - top - 1) / 2) * pitch, row / 2, 0.8))
+    return drawing
+
+
+def modular_jack(width, keyway, contacts, positions):
+    """
+    A modular (RJ) jack, contacts along the top and the latch keyway at the bottom. contacts lists the populated
+    positions, numbered from 1 at the left.
+    """
+    # The opening spans y -3.42 to +5.95 (including the keyway); centre it vertically
+    dy = -1.265
+    x, kw = width / 2, keyway / 2
+    outline = polygon([(px, py + dy) for px, py in (
+        (-x, -3.42), (x, -3.42), (x, 3.42), (kw, 3.42), (kw, 5.1), (2.03, 5.1), (2.03, 5.95), (-2.03, 5.95),
+        (-2.03, 5.1), (-kw, 5.1), (-kw, 3.42), (-x, 3.42),
+    )])
+    drawing = Drawing(width, 9.37).outline(outline)
+    for n in contacts:
+        drawing.fill(rect((n - (positions + 1) / 2) * 1.016, -2.2 + dy, 0.55, 2.4))
+    return drawing
+
+
+def mini_din_8():
+    """A Mini-DIN 8 receptacle: eight contacts in an insert with a notch at the top and shoulders at the bottom."""
+    r = 3.95
+    notch_y = -math.sqrt(r ** 2 - 0.9 ** 2)
+    shoulder_x = math.sqrt(r ** 2 - 1.9 ** 2)
+    base_y = math.sqrt(r ** 2 - 2.5 ** 2)
+    insert = Path([
+        ('M', 0.9, notch_y), arc_to(0, 0, r, (0.9, notch_y), (shoulder_x, 1.9)), ('L', 2.5, 1.9), ('L', 2.5, base_y),
+        arc_to(0, 0, r, (2.5, base_y), (-2.5, base_y)), ('L', -2.5, 1.9), ('L', -shoulder_x, 1.9),
+        arc_to(0, 0, r, (-shoulder_x, 1.9), (-0.9, notch_y)), ('L', -0.9, -3), ('L', 0.9, -3), ('Z',),
+    ])
+    drawing = Drawing(10.2, 10.2).outline(Circle(0, 0, 5.1)).detail(insert)
+    for x, y in ((-2, -1.6), (0, -1.6), (2, -1.6), (-2.5, 0.4), (0.4, 0.4), (2.5, 0.4), (-1, 2.4), (1, 2.4)):
+        drawing.fill(Circle(x, y, 0.55))
+    return drawing
+
+
 #
 # USB (receptacle openings)
 #
@@ -1007,6 +1064,26 @@ POWER_PORTS = {
 }
 
 
+# Console port type: drawing function. USB console ports are receptacles.
+CONSOLE_PORTS = {
+    'de-9': lambda: d_sub(9, 16.26, 2.74),
+    'db-25': lambda: d_sub(25, 38.3, 2.77),
+    # RJ-11 is defined with only its two centre positions populated (47 CFR 68.502)
+    'rj-11': lambda: modular_jack(9.88, 6.6, (3, 4), 6),
+    'rj-12': lambda: modular_jack(9.88, 6.6, range(1, 7), 6),
+    'rj-45': lambda: modular_jack(11.91, 6.3, range(1, 9), 8),
+    'mini-din-8': mini_din_8,
+    'usb-a': usb_a,
+    'usb-b': usb_b,
+    'usb-c': usb_c,
+    'usb-mini-a': usb_mini_a,
+    'usb-mini-b': usb_mini_b,
+    'usb-micro-a': usb_micro_a,
+    'usb-micro-b': usb_micro_b,
+    'usb-micro-ab': usb_micro_a,
+}
+
+
 def write(directory, drawings):
     directory.mkdir(parents=True, exist_ok=True)
     expected = set()
@@ -1029,6 +1106,7 @@ def write(directory, drawings):
 def main():
     write(OUTPUT_DIR / 'power-outlet', POWER_OUTLETS)
     write(OUTPUT_DIR / 'power-port', POWER_PORTS)
+    write(OUTPUT_DIR / 'console-port', CONSOLE_PORTS)
 
 
 if __name__ == '__main__':