diff --git a/.clang-format b/.clang-format index 86629fa4bae..bdd0d7d0674 100644 --- a/.clang-format +++ b/.clang-format @@ -1,21 +1,31 @@ -BasedOnStyle: None +BasedOnStyle: InheritParentConfig + +SortIncludes: CaseSensitive +IncludeBlocks: Regroup +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterFunction: true + BeforeElse: true + AfterControlStatement: MultiLine +TabWidth: 4 +IndentWidth: 4 +ColumnLimit: 120 -SortIncludes: true -IncludeBlocks: Regroup IncludeIsMainRegex: '([-_](test|unittest))?$' IncludeIsMainSourceRegex: '' IncludeCategories: - - Regex: '^' - Priority: 2 - SortPriority: 0 - - Regex: '^<.*\.h>' - Priority: 1 - SortPriority: 0 - - Regex: '^<.*' - Priority: 2 - SortPriority: 0 - - Regex: '.*' - Priority: 3 - SortPriority: 0 + - Regex: '^' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 diff --git a/.cproject b/.cproject index cf2c5ad4ffd..60361317451 100644 --- a/.cproject +++ b/.cproject @@ -55,24 +55,24 @@ - - - - - + + + + + - - + + - - - - + + + + - + @@ -128,24 +128,24 @@ - - - - - + + + + + - - + + - - - - + + + + - + diff --git a/.gitignore b/.gitignore index 8ea27953298..dbdefb9dc25 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,4 @@ xxx /tmp/ /.computed/ *.DS_Store +.idea/ \ No newline at end of file diff --git a/.oppbuildspec b/.oppbuildspec index bb9d2c5de3c..f00ffd86d02 100644 --- a/.oppbuildspec +++ b/.oppbuildspec @@ -1,5 +1,5 @@ - + diff --git a/.project b/.project index 2bcc7693144..a956de0517a 100644 --- a/.project +++ b/.project @@ -5,6 +5,11 @@ + + org.python.pydev.PyDevBuilder + + + org.omnetpp.cdt.MakefileBuilder @@ -93,5 +98,6 @@ org.eclipse.cdt.core.ccnature org.eclipse.cdt.managedbuilder.core.managedBuildNature org.omnetpp.main.omnetppnature + org.python.pydev.pythonNature diff --git a/doc/src/nedtags.xml b/doc/src/nedtags.xml index b64975ef2ab..493f5522d64 100644 --- a/doc/src/nedtags.xml +++ b/doc/src/nedtags.xml @@ -348,6 +348,18 @@ inet.linklayer.bmac.BMacInterface inet.linklayer.bmac.BMacInterface.html + + inet.showcases.tsn.timesynchronization.gptp_bmca.BmcaShowcaseDiamond + inet.showcases.tsn.timesynchronization.gptp_bmca.BmcaShowcaseDiamond.html + + + inet.showcases.tsn.timesynchronization.gptp_bmca.BmcaShowcaseDiamondAsymmetric + inet.showcases.tsn.timesynchronization.gptp_bmca.BmcaShowcaseDiamondAsymmetric.html + + + inet.showcases.tsn.timesynchronization.gptp_bmca.BmcaShowcaseSimple + inet.showcases.tsn.timesynchronization.gptp_bmca.BmcaShowcaseSimple.html + inet.mobility.single.BonnMotionMobility inet.mobility.single.BonnMotionMobility.html @@ -1868,6 +1880,10 @@ inet.linklayer.ieee8021as.GptpBridge inet.linklayer.ieee8021as.GptpBridge.html + + inet.linklayer.ieee8021as.GptpDomainClassifier + inet.linklayer.ieee8021as.GptpDomainClassifier.html + inet.linklayer.ieee8021as.GptpEndstation inet.linklayer.ieee8021as.GptpEndstation.html @@ -1956,6 +1972,10 @@ inet.tests.ethernet.HostsWithSwitch inet.tests.ethernet.HostsWithSwitch.html + + inet.linklayer.ieee8021as.HotStandby + inet.linklayer.ieee8021as.HotStandby.html + inet.examples.ethernet.lans.HubLAN inet.examples.ethernet.lans.HubLAN.html @@ -2932,6 +2952,10 @@ inet.queueing.server.InstantServer inet.queueing.server.InstantServer.html + + inet.clock.servo.InstantServoClock + inet.clock.servo.InstantServoClock.html + inet.showcases.visualizer.canvas.instrumentfigures.InstrumentShowcase inet.showcases.visualizer.canvas.instrumentfigures.InstrumentShowcase.html @@ -3508,6 +3532,10 @@ inet.showcases.measurement.jitter.JitterMeasurementShowcase inet.showcases.measurement.jitter.JitterMeasurementShowcase.html + + inet.showcases.tsn.timesynchronization.gptp.JumpingClockGptpShowcase + inet.showcases.tsn.timesynchronization.gptp.JumpingClockGptpShowcase.html + inet.examples.inet.kidsnw1.KIDSNw1 inet.examples.inet.kidsnw1.KIDSNw1.html @@ -3948,6 +3976,10 @@ inet.examples.sctp.cmttest.multihomed inet.examples.sctp.cmttest.multihomed.html + + inet.showcases.tsn.timesynchronization.gptp.MultiHopGptpShowcase + inet.showcases.tsn.timesynchronization.gptp.MultiHopGptpShowcase.html + inet.protocolelement.measurement.MultiMeasurementLayer inet.protocolelement.measurement.MultiMeasurementLayer.html @@ -4980,6 +5012,10 @@ inet.applications.pingapp.PingApp inet.applications.pingapp.PingApp.html + + inet.clock.servo.PiServoClock + inet.clock.servo.PiServoClock.html + inet.showcases.wireless.power.PowerConsumptionShowcase inet.showcases.wireless.power.PowerConsumptionShowcase.html @@ -5717,8 +5753,8 @@ inet.tutorials.queueing.ServerTutorialStep.html - inet.clock.model.SettableClock - inet.clock.model.SettableClock.html + inet.clock.servo.ServoClockBase + inet.clock.servo.ServoClockBase.html inet.linklayer.shortcut.ShortcutInterface @@ -6509,12 +6545,12 @@ inet.tests.ethernet.TwoHosts.html - inet.showcases.tsn.timesynchronization.gptp.TwoMasterClocksRingGptpShowcase - inet.showcases.tsn.timesynchronization.gptp.TwoMasterClocksRingGptpShowcase.html + inet.showcases.tsn.timesynchronization.gptp_hotstandby.TwoMasterClocksRingGptpShowcase + inet.showcases.tsn.timesynchronization.gptp_hotstandby.TwoMasterClocksRingGptpShowcase.html - inet.showcases.tsn.timesynchronization.gptp.TwoMasterClocksTreeGptpShowcase - inet.showcases.tsn.timesynchronization.gptp.TwoMasterClocksTreeGptpShowcase.html + inet.showcases.tsn.timesynchronization.gptp_hotstandby.TwoMasterClocksTreeGptpShowcase + inet.showcases.tsn.timesynchronization.gptp_hotstandby.TwoMasterClocksTreeGptpShowcase.html inet.examples.ospfv2.areas.TwoNetsArea diff --git a/doc/src/users-guide/ch-clock.rst b/doc/src/users-guide/ch-clock.rst index b1bc167322b..a008c0e5dd7 100644 --- a/doc/src/users-guide/ch-clock.rst +++ b/doc/src/users-guide/ch-clock.rst @@ -54,12 +54,25 @@ return values. The interface contains functions such as :fun:`getClockTime()`, :fun:`cancelClockEvent()`. INET contains optional clock modules (not used by default) at the network node -and the network interface levels. The following clock models are available: +and the network interface levels. The following simple clock models are available: - :ned:`IdealClock`: clock time is identical to the simulation time. - :ned:`OscillatorBasedClock`: clock time is the number of oscillator ticks multiplied by the nominal tick length. -- :ned:`SettableClock`: a clock that can be set to a different clock time. + +Above clock models are not adjustable during runtime. +To this end, INET provides several clocks with an adjustable time and drift rate. +These clocks are based on the :ned:`ServoClockBase` extending the :ned:`OscillatorBasedClock`: + +- :ned:`InstantServoClock`: A simple clock servo module supports jumping its clock to another time and optionally + adjusting the drift rate. This allows for a simple synchronization with another clock. +- :ned:`PiServoClock`: A PI-based servo clock using proportional (kp) and integral (ki) gains + to in sync with another time source source while smoothly adjusting the drift rate. + + +If multiple clocks are needed INET provides a :ned:`MultiClock`. +This clock manages holds multiple subclocks with a programmatically switchable active clock. +This is for example useful for multi-domain gPTP synchronization. Clock Time ---------- @@ -134,7 +147,7 @@ provides clocks and oscillators that implement the interface required by the oscillator states from the :ned:`ScenarioManager` XML script and also to mix these operations with many other supported operations. -For example, the :ned:`SettableClock` model supports setting the clock time and +For example, the :ned:`InstantServoClock` model supports setting the clock time and optionally resetting the oscillator at a specific moment of simulation time as follows: diff --git a/doc/src/users-guide/ch-tsn.rst b/doc/src/users-guide/ch-tsn.rst index eaed4b17292..533be8d3673 100644 --- a/doc/src/users-guide/ch-tsn.rst +++ b/doc/src/users-guide/ch-tsn.rst @@ -61,8 +61,12 @@ can still be used to keep track of time in the network nodes. - :ned:`OscillatorBasedClock` models a clock that has a potentially drifting oscillator -- :ned:`SettableClock` extends the previous model with the capability of setting - the clock time +- :ned:`ServoClockBase` extends the previous model with the approach for clock + adjustments +- :ned:`InstantServoClock` extends :ned:`ServoClockBase` with the capability of + adjusting clock by jumping to another time and adjusting the drift rate +- :ned:`PiServoClock` extends :ned:`ServoClockBase` for synchronizing the clock + with an external time source smoothly by implementing a PI controller Similarly to the above, the following gPTP time synchronization related protocol modules and network nodes can also be used to build time synchronization in a @@ -74,18 +78,6 @@ network: - :ned:`GptpMaster` models a gPTP time synchronization master network node - :ned:`GptpSlave` models a gPTP time synchronization slave network node -In order to implement node failure (e.g. master clock) and link failure (e.g. -between gPTP bridges) protection, multiple time synchronization domains are -required. These time domains operate independently of each other, and it's up to -the clock user modules of each network node to decide which clock they are using. -Typically, they use the active clock of the :ned:`MultiClock`, and there has to -be some means of changing the active clocks when failover happens. The following -modules can be used to implement multiple time domains: - -- :ned:`MultiClock` contains several subclocks for the different time domains -- :ned:`MultiDomainGptp` contains several gPTP submodules for the different - time domains - The following parameters can be used to enable the gPTP time synchronization in various predefined network nodes: @@ -94,6 +86,30 @@ in various predefined network nodes: - :par:`hasGptp` parameter enables the gPTP time synchronization protocol in gPTP specific network nodes +There are two possible ways to define the synchronization topology of a network. +The first one is by setting up a static the synchronization topology manually. +The second one is to use the Best Master Clock Algorithm (BMCA) to automatically +determine the synchronization topology. +Please refer to their respective showcases: +:doc:`/showcases/tsn/timesynchronization/gptp/doc/index` and :doc:`/showcases/tsn/timesynchronization/gptp_bmca/doc/index` + +The usage of BMCA is one way to protect the synchronization network +topology against node and link failures, it is not the only one. +Another option is the usage of multiple gPTP synchronization domains. +These time domains operate independently of each other, and it's up to +the clock user modules of each network node to decide which clock they are using. +Typically, they use the active clock of the :ned:`MultiClock`, and there has to +be some means of changing the active clocks when failover happens. +One such a failover mechanism is the :ned:`HotStandby` mechanism, as defined by the IEEE 802.1ASdm amendment. +For a detailed description please refer to the HotStandby showcase: +:doc:`/showcases/tsn/timesynchronization/gptp_hotstandby/doc/index`. + +The following modules can be used to implement multiple time domains: + +- :ned:`MultiClock` contains several subclocks for the different time domains +- :ned:`MultiDomainGptp` contains several gPTP submodules for the different + time domains + .. _ug:sec:tsn:streamfiltering: Per-stream Filtering and Policing @@ -412,4 +428,4 @@ All of these configurators automatically discover the network topology and then taking into account their own independent configuration they compute the necessary parameters for the individual underlying modules and configure them. However, anything they can do, can also be done from INI files manually, and the result -can also be seen at the configured module parameters in the runtime user interface. \ No newline at end of file +can also be seen at the configured module parameters in the runtime user interface. diff --git a/examples/inet/udpclientserver/omnetpp.ini b/examples/inet/udpclientserver/omnetpp.ini index 412b966909c..d9c4a1edcea 100644 --- a/examples/inet/udpclientserver/omnetpp.ini +++ b/examples/inet/udpclientserver/omnetpp.ini @@ -54,14 +54,14 @@ sim-time-limit = 10s extends = udp_OK_ipv4 description = "UDP OK with clock" **.client1.app[0].clockModule = "^.clock" -*.client1.clock.typename = "SettableClock" +*.client1.clock.typename = "InstantServoClock" [Config udp_OK_ipv4_clock_drift] sim-time-limit = 10s extends = udp_OK_ipv4_clock description = "UDP OK with drifting clock" **.client1.app[0].clockModule = "^.clock" -*.client1.clock.typename = "SettableClock" +*.client1.clock.typename = "InstantServoClock" *.client1.clock.initialClockTime = 2ms *.client1.clock.oscillator.driftRate = 1000ppm diff --git a/showcases/index.rst b/showcases/index.rst index 4eda3b32965..1efc9735236 100644 --- a/showcases/index.rst +++ b/showcases/index.rst @@ -18,6 +18,10 @@ the simulations (NED, ini, and other files) and the web pages are in the Latest changes: +- :doc:`tsn/timesynchronization/gptp/doc/index` updated for INET 4.x (2025-04-03) +- :doc:`tsn/timesynchronization/gptp_bmca/doc/index` updated for INET 4.x (2025-04-03) +- :doc:`tsn/timesynchronization/gptp_hotstandby/doc/index` updated for INET 4.x (2024-04-03) +- :doc:`tsn/timesynchronization/clockdrift/doc/index` updated for INET 4.x (2023-03-17) - :doc:`emulation/voip/doc/mininet` added (2024-02-26) - :doc:`tsn/trafficshaping/underthehood/doc/index` updated (2023-07-28) - :doc:`tsn/trafficshaping/cbsandtas/doc/index` released (2023-07-28) @@ -25,8 +29,6 @@ Latest changes: - :doc:`tsn/trafficshaping/asynchronousshaper/doc/index` updated (2023-07-28) - :doc:`tsn/trafficshaping/creditbasedshaper/doc/index` updated (2023-07-28) - :doc:`tsn/combiningfeatures/gptpandtas/doc/index` updated (2023-07-28) -- :doc:`tsn/timesynchronization/gptp/doc/index` updated for INET 4.5 (2023-07-28) -- :doc:`tsn/timesynchronization/clockdrift/doc/index` updated for INET 4.5 (2023-03-17) - :doc:`Several Time-Sensitive Networking showcases released ` (2022-06-13) All showcases: diff --git a/showcases/showcases b/showcases/showcases new file mode 120000 index 00000000000..0011860f0a6 --- /dev/null +++ b/showcases/showcases @@ -0,0 +1 @@ +../../showcases \ No newline at end of file diff --git a/showcases/tsn/combiningfeatures/gptpandtas/doc/index.rst b/showcases/tsn/combiningfeatures/gptpandtas/doc/index.rst index b878e290c3f..45c876269b1 100644 --- a/showcases/tsn/combiningfeatures/gptpandtas/doc/index.rst +++ b/showcases/tsn/combiningfeatures/gptpandtas/doc/index.rst @@ -68,7 +68,7 @@ application traffic, we configure ``tsnDevice1`` to send 10B UDP packets to Clocks and Clock Drift ++++++++++++++++++++++ -Clocks use :ned:`MultiClock` and :ned:`SettableClock` modules with constant +Clocks use :ned:`MultiClock` and :ned:`InstantServoClock` modules with constant drift oscillators. We set the initial drift rate and the drift rate change with a random uniform distribution, and limit the drift rate change: diff --git a/showcases/tsn/combiningfeatures/gptpandtas/omnetpp.ini b/showcases/tsn/combiningfeatures/gptpandtas/omnetpp.ini index f61ef223d6a..bebc61eb7aa 100644 --- a/showcases/tsn/combiningfeatures/gptpandtas/omnetpp.ini +++ b/showcases/tsn/combiningfeatures/gptpandtas/omnetpp.ini @@ -1,10 +1,13 @@ [General] network = GptpAndTasShowcase sim-time-limit = 4s -#abstract-config = true (requires omnet 7) +abstract = true **.displayInterfaceTables = true +**.hasHotStandby = false +**.clock[*].typename = "InstantServoClock" + # all Ethernet interfaces have 100 Mbps speed *.*.eth[*].bitrate = 100Mbps diff --git a/showcases/tsn/timesynchronization/clockdrift/doc/index.rst b/showcases/tsn/timesynchronization/clockdrift/doc/index.rst index 0781ba1218d..0466bec0a4f 100644 --- a/showcases/tsn/timesynchronization/clockdrift/doc/index.rst +++ b/showcases/tsn/timesynchronization/clockdrift/doc/index.rst @@ -37,11 +37,8 @@ clock and oscillator models, we can simulate `constant` and `random` clock drift rates. To model time synchronization, the time of clocks can be set by `synchronizer` modules. -Several clock models are available: - -- :ned:`OscillatorBasedClock`: Has an `oscillator` submodule; keeps time by counting the oscillator's ticks. Depending on the oscillator submodule, can model `constant` and `random clock drift rate` -- :ned:`SettableClock`: Same as the oscillator-based clock but the time can be set from C++ or a scenario manager script -- :ned:`IdealClock`: The clock's time is the same as the simulation time; for testing purposes +Several clock models are available. For a detailed description please take a look at this documentation: +:doc:`/users-guide/ch-clock` The following oscillator models are available: @@ -50,7 +47,8 @@ The following oscillator models are available: - :ned:`RandomDriftOscillator`: the oscillator changes drift rate over time; for modeling `random clock drift` Synchronizers are implemented as application-layer modules. For clock synchronization, the synchronizer -modules need to set the time of clocks, thus only the SettableClock supports synchronization. +modules need to set the time of clocks, thus only the clocks based on the +:ned:`ServoClockBase` module support synchronization. The following synchronizer modules are available: - :ned:`SimpleClockSynchronizer`: Uses an out-of-band mechanism to synchronize clocks, instead of a real clock synchronization protocol. Useful for simulations where the details of time synchronization are not important. @@ -173,7 +171,7 @@ The out-of-band synchronization settings are defined in a base configuration, `` :end-before: ConstantClockDriftOutOfBandSync Since we want to use clock synchronization, we need to be able to set the -clocks, so network nodes have :ned:`SettableClock` modules. The +clocks, so network nodes have :ned:`InstantServoClock` modules. The ``defaultOverdueClockEventHandlingMode = "execute"`` setting means that when setting the clock forward, events that become overdue are done immediately. We use the :ned:`SimpleClockSynchronizer` for out-of-band synchronization. diff --git a/showcases/tsn/timesynchronization/clockdrift/omnetpp.ini b/showcases/tsn/timesynchronization/clockdrift/omnetpp.ini index 645eea04f93..82373350cfa 100644 --- a/showcases/tsn/timesynchronization/clockdrift/omnetpp.ini +++ b/showcases/tsn/timesynchronization/clockdrift/omnetpp.ini @@ -1,7 +1,7 @@ [General] network = ClockDriftShowcase sim-time-limit = 0.1s -#abstract-config = true (requires omnet 7) +abstract = true # disable legacy Ethernet *.*.ethernet.typename = "EthernetLayer" @@ -81,9 +81,9 @@ description = "Clocks with random drift rate" [Config OutOfBandSyncBase] description = "Base config for out-of-band synchronization" -#abstract-config = true (requires omnet 7) +abstract = true -*.source*.clock.typename = "SettableClock" +*.source*.clock.typename = "InstantServoClock" *.source*.clock.defaultOverdueClockEventHandlingMode = "execute" *.source*.numApps = 2 @@ -106,12 +106,13 @@ description = "Clocks are periodically synchronized using gPTP" extends = RandomClockDrift *.switch*.hasGptp = true +**.eth[*].phyLayer.typename = "EthernetStreamingPhyLayer" *.switch*.gptp.syncInterval = 500us *.switch*.gptp.pdelayInterval = 1ms *.switch*.gptp.pdelayInitialOffset = 0ms -*.switch*.clock.typename = "SettableClock" +*.switch*.clock.typename = "InstantServoClock" *.switch1.gptp.gptpNodeType = "MASTER_NODE" *.switch1.gptp.masterPorts = ["eth0", "eth1", "eth2"] # eth* @@ -119,7 +120,7 @@ extends = RandomClockDrift *.switch2.gptp.gptpNodeType = "SLAVE_NODE" *.switch2.gptp.slavePort = "eth0" -*.source*.clock.typename = "SettableClock" +*.source*.clock.typename = "InstantServoClock" *.source*.numApps = 2 *.source*.app[1].typename = "Gptp" diff --git a/showcases/tsn/timesynchronization/gptp/GptpShowcase.anf b/showcases/tsn/timesynchronization/gptp/GptpShowcase.anf index 61c5a8f256d..81c47d3a93e 100644 --- a/showcases/tsn/timesynchronization/gptp/GptpShowcase.anf +++ b/showcases/tsn/timesynchronization/gptp/GptpShowcase.anf @@ -3,14 +3,18 @@ - - - - + + + + + + + + - - + + - + @@ -689,7 +691,7 @@ utils.export_data_if_needed(df, props) ]]> - + @@ -783,7 +785,7 @@ utils.export_data_if_needed(df, props) ]]> - + @@ -948,7 +950,7 @@ utils.export_data_if_needed(df, props) ]]> - + @@ -965,41 +967,17 @@ utils.export_data_if_needed(df, props) - - + + - - - + - - - - + + + + + + + + + + + + + - - @@ -1052,11 +1058,10 @@ utils.export_data_if_needed(df, props) - ]]> - + @@ -1067,16 +1072,16 @@ utils.export_data_if_needed(df, props) - ]]> - + @@ -1132,47 +1137,50 @@ utils.export_data_if_needed(df, props) ]]> - - - + - + - + + + + + + + - + + - - - + + + - - - + + - - + - + @@ -1265,7 +1273,7 @@ utils.export_data_if_needed(df, props) ]]> - + @@ -1359,7 +1367,7 @@ utils.export_data_if_needed(df, props) ]]> - + @@ -1524,7 +1532,7 @@ utils.export_data_if_needed(df, props) ]]> - + @@ -1541,41 +1549,17 @@ utils.export_data_if_needed(df, props) - - + + - - - + - - - - + + + + + + + + + + + + + - - @@ -1628,11 +1640,10 @@ utils.export_data_if_needed(df, props) - ]]> - + @@ -1643,16 +1654,16 @@ utils.export_data_if_needed(df, props) - ]]> - + @@ -1708,53 +1719,50 @@ utils.export_data_if_needed(df, props) ]]> - - - + - + - + + + + + + + - - - + + + - - + + + - - - + + - - + - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -]]> - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/showcases/tsn/timesynchronization/gptp_bmca/GptpShowcase.ned b/showcases/tsn/timesynchronization/gptp_bmca/GptpShowcase.ned new file mode 100644 index 00000000000..4f88f6fdc36 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/GptpShowcase.ned @@ -0,0 +1,90 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.showcases.tsn.timesynchronization.gptp_bmca; + +import inet.common.scenario.ScenarioManager; +import inet.networks.base.TsnNetworkBase; +import inet.node.ethernet.EthernetLink; +import inet.node.tsn.TsnClock; +import inet.node.tsn.TsnDevice; +import inet.node.tsn.TsnSwitch; + +network BmcaShowcaseSimple extends TsnNetworkBase +{ + submodules: + scenarioManager: ScenarioManager { + @display("p=100,800;is=s"); + } + tsnDevice1: TsnDevice { + @display("p=500,150"); + } + tsnSwitch: TsnSwitch { + @display("p=500,300"); + } + tsnDevice2: TsnDevice { + @display("p=400,450"); + } + tsnDevice3: TsnDevice { + @display("p=600,450"); + } + connections: + tsnDevice1.ethg++ <--> EthernetLink <--> tsnSwitch.ethg++; + tsnSwitch.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; + tsnSwitch.ethg++ <--> EthernetLink <--> tsnDevice3.ethg++; +} + +network BmcaShowcaseDiamond extends TsnNetworkBase +{ + submodules: + scenarioManager: ScenarioManager { + @display("p=100,800;is=s"); + } + tsnDevice1: TsnDevice { + @display("p=500,150"); + } + tsnSwitchA1: TsnSwitch { + @display("p=450,300"); + } + tsnSwitchB1: TsnSwitch { + @display("p=550,300"); + } + tsnDevice2: TsnDevice { + @display("p=500,450"); + } + connections: + tsnDevice1.ethg++ <--> EthernetLink <--> tsnSwitchA1.ethg++; + tsnDevice1.ethg++ <--> EthernetLink <--> tsnSwitchB1.ethg++; + tsnSwitchA1.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; + tsnSwitchB1.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; +} + +network BmcaShowcaseDiamondAsymmetric extends TsnNetworkBase +{ + submodules: + scenarioManager: ScenarioManager { + @display("p=100,800;is=s"); + } + tsnDevice1: TsnDevice { + @display("p=500,150"); + } + tsnSwitchA1: TsnSwitch { + @display("p=450,250"); + } + tsnSwitchA2: TsnSwitch { + @display("p=450,350"); + } + tsnSwitchB1: TsnSwitch { + @display("p=550,300"); + } + tsnDevice2: TsnDevice { + @display("p=500,450"); + } + connections: + tsnDevice1.ethg++ <--> EthernetLink <--> tsnSwitchA1.ethg++; + tsnDevice1.ethg++ <--> EthernetLink <--> tsnSwitchB1.ethg++; + tsnSwitchA1.ethg++ <--> EthernetLink <--> tsnSwitchA2.ethg++; + tsnSwitchA2.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; + tsnSwitchB1.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; +} diff --git a/showcases/tsn/timesynchronization/gptp_bmca/asym-diamond-link-failure.xml b/showcases/tsn/timesynchronization/gptp_bmca/asym-diamond-link-failure.xml new file mode 100644 index 00000000000..f412068df92 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/asym-diamond-link-failure.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/showcases/tsn/timesynchronization/gptp_bmca/diamond-link-failure.xml b/showcases/tsn/timesynchronization/gptp_bmca/diamond-link-failure.xml new file mode 100644 index 00000000000..c23d24ba8e3 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/diamond-link-failure.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/showcases/tsn/timesynchronization/gptp_bmca/doc/index.rst b/showcases/tsn/timesynchronization/gptp_bmca/doc/index.rst new file mode 100644 index 00000000000..cca28632c23 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/doc/index.rst @@ -0,0 +1,197 @@ +gPTP: Best Master Clock Algorithm +================================= + +Overview +~~~~~~~~ + +The Best Master Clock Algorithm (BMCA) is a fundamental component of gPTP protocol (IEEE 802.1AS). +Most of it's definition, however is defined in the Precision Time Protocol (PTP, see IEEE 1588). +It dynamically selects the most suitable master clock in a distributed network to ensure accurate synchronization +without manual intervention. + +gPTP nodes continously transmit Announce messages to their neighboring nodes, +transmitting information about it's current master clock (e.g. user-defined priorities, clock quality paramaters, ...). +BMCA continuously evaluates available clocks based on these Announce messages, ensuring that the highest-quality +clock is chosen as the Grandmaster Clock. If the current master clock becomes unavailable, BMCA re-evaluates +the network after a timeout and selects the next best candidate to maintain synchronization. +All these parmeters can be defined in the :ned:`Gptp` module. + +The Model +~~~~~~~~~ + +In this showcase, we provide three BMCA network scenarios in the showcase: BmcaShowcaseSimple, BmcaPrioChange, BmcaDiamond and BmcaDiamondAsymmetric. + +- **BMCA Simple Network**: A basic setup with a single switch connecting three devices. The best master clock is selected dynamically based on BMCA rules, + ensuring time synchronization across the network. +- **BMCA with Priority Change**: Similar to the simple setup but introduces dynamic priority changes. Devices with different priorities compete for the best master clock role, + showcasing BMCA’s adaptability. +- **BMCA Diamond Topology**: A redundant diamond-shaped network where devices are connected through two switches. The best master clock is determined dynamically, + ensuring synchronization even if a switch or link fails. +- **BMCA Asymmetric Diamond**: An asymmetric variation of the diamond topology, introducing different link configurations and priorities. + Demonstrates BMCA’s ability to handle uneven network structures while maintaining synchronization. + +.. TODO: .. TODO: Consider updating the notation for `gptp_hotstandby` if needed. + +In the ``General`` configuration, which has a similar setup to the :doc:`/showcases/tsn/timesynchronization/gptp/doc/index` showcase, +we enable :ned:`Gptp` modules in all network nodes and configure a random clock drift rate for all clocks. + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: enable time synchronization + :end-at: driftRate + +Each simulation is detailed in the following sections. + +BMCA Simple Network +~~~~~~~~~~~~~~~~~~~ + +In the BMCA Simple Network, we have a basic setup with a single switch connecting three devices. The best master clock is selected dynamically based on +BMCA rules, ensuring time synchronization across the network. + +.. figure:: media/BmcaShowcaseSimple.png + :align: center + +Our goal is to demonstrate BMCA's ability to select the best master clock in a simple network. +We configure the devices with different priorities. + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: *.tsnDevice1.gptp.grandmasterPriority1 = 2 + :end-at: .tsnDevice3.gptp.grandmasterPriority1 = 3 + +In this setup: + +- `TsnDevice1` has the highest priority with a value of 2 +- `TsnDevice3` follows with a priority of 3 +- `TsnDevice2` has the lowest priority, with the default value of 255 + +.. note:: Lower priority values indicate higher priority in BMCA. + +For our scenario, we setup a link failure between the intended GM (`tsnDevice1`) and the `tsnSwitch`: + +.. literalinclude:: ../simple-link-failure.xml + :language: xml + +The following configuration defines the grandmaster priority settings: + +The BMCA algorithm will evaluate these priorities and select the best master clock based on the defined criteria. +The below chart shows the clock synchronization behavior throughout the simulation: + +.. figure:: media/BMCA_zoomed.png + :align: center + +The chart shows the clock time differences between devices over the simulation period. +We can observe the following: + +- All devices maintain good synchronization with a gradual upward drift until 7.5s +- At 7.5s ``tsnDevice1`` is disconnected. +- The :par:`announceTimeout` (typically 3 times the announce interval and thus 3s) is emitted on the other clocks due + to missing Announce messages from the GM. +- At around 10s the new GM (``tsnDevice3``) is selected and after two Sync messages, the other devices start to follow + the new GM. +- At 14.5s the link between ``tsnDevice1`` and ``tsnSwitch`` is restored. +- A new Announce message is sent by ``tsnDevice1`` at approx. 15s and the other devices start to follow it again after + two Sync messages. + +The chart effectively demonstrates how the grandmaster clock role shifts between devices during link failures, +and how the synchronization is maintained among the devices in the operational part of the network. + +BMCA with Priority Change +------------------------- + +Using the same network, we now use a different scenario. +Instead of cutting the link to the GM, we will change the priority of ``tsnDevice3`` to become the intended GM at 10s: + +.. literalinclude:: ../simple-priority-change.xml + :language: xml + +After this priority change and the consecutive Announce message, the other devices will select +``tsnDevice3`` as the new GM: + +.. figure:: media/BMCA_PrioChange_zoomed.png + :align: center + + +BMCA Diamond Topology +~~~~~~~~~~~~~~~~~~~~~ + +In the BMCA Diamond Topology, we create a redundant network with multiple paths between two devices. This configuration demonstrates BMCA's ability to +handle network failures while maintaining synchronization. + +The network consists of two TSN devices (``tsnDevice1`` and ``tsnDevice2``) connected through two switches (``tsnSwitchA1`` and ``tsnSwitchB1``). Each device +has a gPTP module for clock synchronization. + +.. figure:: media/BmcaShowcaseDiamond.png + :align: center + +Both devices are connected to both switches, forming a diamond-shaped topology that provides path redundancy. In this setup, ``tsnDevice1`` is configured +as the grandmaster with priority 1. All other values use the default values. + +The link failure settings are configured in the diamond-link-failure.xml file: + +.. literalinclude:: ../diamond-link-failure.xml + :language: xml + +Between 1.5s and 6.5s, the link between ``tsnDevice1`` and ``tsnSwitchB1`` is cut. +Between 10.5s and 15.5s, the link between ``tsnDevice2`` and ``tsnSwitchB1`` is cut. + +.. figure:: media/BMCA_Diamond_zoomed.png + :align: center + +As the figure shows, ``tsnDevice1`` remains the GM throughout the simulation. + +Further, we can see, that in the case of the first link failure, ``tsnDevice2`` (orange) and ``tsnSwitchB1`` (red) +start to drift away for 3s (announceTimeout) before the BMCA algorithm starts to establish the new +synchronization tree via ``tsnSwitchA1``. + +In the second link failure case, we can see that only ``tsnSwitchB1`` (red) starts to drift away. +This indicates that in the fully operational network +the spanning tree is established via ``tsnSwitchB1`` to ``tsnDevice2``. + +Finally, the figure shows that upon re-establishing the link between ``tsnDevice1`` and ``tsnSwitchB1`` +in the first failure case, there is no out-of-sync time and the network transitions smoothly +to the new topology. + + +BMCA Asymmetric Diamond +~~~~~~~~~~~~~~~~~~~~~~~ + +The BMCA Asymmetric Diamond topology extends the diamond configuration by introducing an asymmetric structure +with different path lengths. +This setup is used to test if the BMCA implementation does select the shorter synchronization path in case of +multiple redundant paths. + +The network structure is provided in the following figure: + +.. figure:: media/BmcaShowcaseDiamondAsymmetric.png + :align: center + +In this asymmetric topology: + +- The left path through ``tsnSwitchA1`` and ``tsnSwitchA2`` is longer (two hops) +- The right path through ``tsnSwitchB1`` is shorter (one hop) + +We again set up link failures in this network: + +.. literalinclude:: ../asym-diamond-link-failure.xml + :language: xml + +The following failure case happen: + +1. From 1.5s to 5.5s the link between ``tsnDevice1`` and ``tsnSwitchA1`` is cut. +2. From 7.5s to 11.5s the link between ``tsnSwitch1`` and ``tsnSwitchB1`` is cut. +3. From 13.5s to 17.5s the link between ``tsnSwitchA1`` and ``tsnSwitchA2`` is cut. + +We assume that ``tsnDevice2`` is synchronized via ``tsnSwitchB1``, as it is the shorter path. +The following figure confirms this: + +.. figure:: media/BMCAAsymmetricDiamond.png + :align: center + +1. When the link between ``tsnDevice1`` and ``tsnSwitchA1`` is cut, only ``tsnSwitchA2`` and ``tsnSwitchA2`` drift away.Between +2. When the link between ``tsnSwitch1`` and ``tsnSwitchB1`` is cut, ``tsnDevice2`` and ``tsnSwitchB1`` drift away. +3. When the link between ``tsnSwitchA2`` and ``tsnSwitchA1`` is cut, only ``tsnSwitchA2`` drifts away. + +In all cases, the network is able to set up a new synchronization tree over the operational redundant path. + + diff --git a/showcases/tsn/timesynchronization/gptp_bmca/doc/media b/showcases/tsn/timesynchronization/gptp_bmca/doc/media new file mode 120000 index 00000000000..661d2180f40 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/doc/media @@ -0,0 +1 @@ +../../../../../media/showcases/tsn/timesynchronization/gptp_bmca/doc \ No newline at end of file diff --git a/showcases/tsn/timesynchronization/gptp_bmca/omnetpp.ini b/showcases/tsn/timesynchronization/gptp_bmca/omnetpp.ini new file mode 100644 index 00000000000..dba1d211d85 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/omnetpp.ini @@ -0,0 +1,121 @@ +[General] +seed-set = 0 +sim-time-limit = 20s +description = "abstract" +abstract=true + +# enable time synchronization in all network nodes +*.*.hasTimeSynchronization = true + +**.clock.oscillator.typename = "RandomDriftOscillator" +**.oscillator.changeInterval = 12.5ms +**.oscillator.driftRate = uniform(-100ppm, 100ppm) +**.oscillator.driftRateChange = uniform(-1ppm, 1ppm) +**.oscillator.driftRateChangeUpperLimit = 100ppm +**.oscillator.driftRateChangeLowerLimit = -100ppm + +**.sendAnnounceImmediately = true + +**.kp=8 +**.ki=7 +**.offsetThreshold = 1 us + +**.pdelayInitialOffset = 1ms + + +# all Ethernet interfaces have 100 Mbps speed +*.*.eth[*].bitrate = 100Mbps + +*.visualizer.typename = "IntegratedMultiCanvasVisualizer" +*.visualizer.infoVisualizer[*].displayInfos = true + +**.gptp.gptpNodeType = "BMCA_NODE" +**.gptp.slavePort = "" +**.gptp.masterPorts = [] + + +[Config BMCA] +network = BmcaShowcaseSimple +description = "Showcase BMCA" + +*.scenarioManager.script = xmldoc("simple-link-failure.xml") + + +*.tsnDevice*.gptp.bmcaPorts = ["eth0"] +*.tsnSwitch.gptp.bmcaPorts = ["eth0", "eth1", "eth2"] + + +*.tsnDevice1.gptp.grandmasterPriority1 = 2 +*.tsnDevice3.gptp.grandmasterPriority1 = 3 + + +# Set all reference clocks to master clock so the time difference can be visualized +**.referenceClock = "tsnDevice1.clock" + +# data link visualizer displays gPTP time synchronization packets +*.visualizer.dataLinkVisualizer[0].displayLinks = true +*.visualizer.dataLinkVisualizer[0].activityLevel = "protocol" +*.visualizer.dataLinkVisualizer[0].packetFilter = "GptpSync" +*.visualizer.dataLinkVisualizer[0].lineColor = "blue2" + +*.visualizer.numInfoVisualizers = 3 +*.visualizer.infoVisualizer[0].modules = "*.tsnClock.clock" +*.tsnDevice1.clock.displayStringTextFormat = "time: %T" +*.visualizer.infoVisualizer[1].modules = "*.tsnSwitch.clock" +*.visualizer.infoVisualizer[1].placementHint = "topLeft" +*.visualizer.infoVisualizer[2].modules = "*.tsnDevice*.clock" +*.visualizer.infoVisualizer[2].placementHint = "bottom" +*.tsnDevice*.clock.displayStringTextFormat = "diff: %d" +*.tsnSwitch.clock.displayStringTextFormat = "diff: %d" + +[Config BMCA_PrioChange] +extends = BMCA +description = "Showcase BMCA with priority change" + +*.scenarioManager.script = xmldoc("simple-priority-change.xml") + + + +[Config BmcaDiamondBase] +abstract=true + + +*.tsn*.gptp.bmcaPorts = ["eth0", "eth1"] + +*.tsnDevice1.gptp.grandmasterPriority1 = 1 + + +# Set all reference clocks to master clock so the time difference can be visualized +**.referenceClock = "tsnDevice1.clock" + +# data link visualizer displays gPTP time synchronization packets +*.visualizer.dataLinkVisualizer[0].displayLinks = true +*.visualizer.dataLinkVisualizer[0].activityLevel = "protocol" +*.visualizer.dataLinkVisualizer[0].packetFilter = "GptpSync" +*.visualizer.dataLinkVisualizer[0].lineColor = "blue2" + +*.visualizer.numInfoVisualizers = 3 +*.visualizer.infoVisualizer[0].modules = "*.tsnClock.clock" +*.tsnDevice1.clock.displayStringTextFormat = "time: %T" +*.visualizer.infoVisualizer[1].modules = "*.tsnSwitch*.clock" +*.visualizer.infoVisualizer[1].placementHint = "topLeft" +*.visualizer.infoVisualizer[2].modules = "*.tsnDevice*.clock" +*.visualizer.infoVisualizer[2].placementHint = "bottom" +*.tsnDevice*.clock.displayStringTextFormat = "diff: %d" +*.tsnSwitch*.clock.displayStringTextFormat = "diff: %d" + +[Config BmcaDiamond] +network = BmcaShowcaseDiamond +extends = BmcaDiamondBase +description = "Showcase BMCA with Diamond topology" + +*.scenarioManager.script = xmldoc("diamond-link-failure.xml") + + +[Config BmcaDiamondAsymmetric] +network = BmcaShowcaseDiamondAsymmetric +extends = BmcaDiamondBase +description = "Showcase BMCA with asymmetric diamond topology" +seed-set = 1 + +*.scenarioManager.script = xmldoc("asym-diamond-link-failure.xml") diff --git a/showcases/tsn/timesynchronization/gptp_bmca/simple-link-failure.xml b/showcases/tsn/timesynchronization/gptp_bmca/simple-link-failure.xml new file mode 100644 index 00000000000..c12a414612f --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/simple-link-failure.xml @@ -0,0 +1,4 @@ + + + + diff --git a/showcases/tsn/timesynchronization/gptp_bmca/simple-priority-change.xml b/showcases/tsn/timesynchronization/gptp_bmca/simple-priority-change.xml new file mode 100644 index 00000000000..1c1052a3e6e --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_bmca/simple-priority-change.xml @@ -0,0 +1,3 @@ + + + diff --git a/showcases/tsn/timesynchronization/gptp_hotstandby/GptpShowcase.anf b/showcases/tsn/timesynchronization/gptp_hotstandby/GptpShowcase.anf new file mode 100644 index 00000000000..4ed0fb10623 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_hotstandby/GptpShowcase.anf @@ -0,0 +1,10389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/showcases/tsn/timesynchronization/gptp_hotstandby/GptpShowcase.ned b/showcases/tsn/timesynchronization/gptp_hotstandby/GptpShowcase.ned new file mode 100644 index 00000000000..eb032210e15 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_hotstandby/GptpShowcase.ned @@ -0,0 +1,107 @@ +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +package inet.showcases.tsn.timesynchronization.gptp_hotstandby; + +import inet.common.scenario.ScenarioManager; +import inet.networks.base.TsnNetworkBase; +import inet.node.ethernet.EthernetLink; +import inet.node.tsn.TsnClock; +import inet.node.tsn.TsnDevice; +import inet.node.tsn.TsnSwitch; + +network TwoMasterClocksTreeGptpShowcase extends TsnNetworkBase +{ + submodules: + scenarioManager: ScenarioManager { + @display("p=100,500;is=s"); + } + tsnClock1: TsnClock { + @display("p=500,150"); + } + tsnClock2: TsnClock { + @display("p=700,150"); + } + tsnSwitch1: TsnSwitch { + @display("p=500,300"); + } + tsnSwitch2: TsnSwitch { + @display("p=700,300"); + } + tsnDevice1: TsnDevice { + @display("p=300,450"); + } + tsnDevice2: TsnDevice { + @display("p=500,450"); + } + tsnDevice3: TsnDevice { + @display("p=700,450"); + } + tsnDevice4: TsnDevice { + @display("p=900,450"); + } + connections: + tsnClock1.ethg++ <--> EthernetLink <--> tsnSwitch1.ethg++; + tsnClock2.ethg++ <--> EthernetLink <--> tsnSwitch2.ethg++; + tsnSwitch1.ethg++ <--> EthernetLink <--> tsnSwitch2.ethg++; + tsnSwitch1.ethg++ <--> EthernetLink <--> tsnDevice1.ethg++; + tsnSwitch1.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; + tsnSwitch2.ethg++ <--> EthernetLink <--> tsnDevice3.ethg++; + tsnSwitch2.ethg++ <--> EthernetLink <--> tsnDevice4.ethg++; +} + + +network TwoMasterClocksRingGptpShowcase extends TsnNetworkBase +{ + submodules: + tsnClock1: TsnClock { + @display("p=500,150"); + } + tsnClock2: TsnClock { + @display("p=900,600"); + } + tsnSwitch1: TsnSwitch { + @display("p=700,150"); + } + tsnSwitch2: TsnSwitch { + @display("p=500,300"); + } + tsnSwitch3: TsnSwitch { + @display("p=500,450"); + } + tsnSwitch4: TsnSwitch { + @display("p=700,600"); + } + tsnSwitch5: TsnSwitch { + @display("p=900,450"); + } + tsnSwitch6: TsnSwitch { + @display("p=900,300"); + } + tsnDevice1: TsnDevice { + @display("p=300,300"); + } + tsnDevice2: TsnDevice { + @display("p=300,450"); + } + tsnDevice3: TsnDevice { + @display("p=1100,450"); + } + tsnDevice4: TsnDevice { + @display("p=1100,300"); + } + connections: + tsnClock1.ethg++ <--> EthernetLink <--> tsnSwitch1.ethg++; + tsnClock2.ethg++ <--> EthernetLink <--> tsnSwitch4.ethg++; + tsnSwitch1.ethg++ <--> EthernetLink <--> tsnSwitch2.ethg++; + tsnSwitch2.ethg++ <--> EthernetLink <--> tsnSwitch3.ethg++; + tsnSwitch3.ethg++ <--> EthernetLink <--> tsnSwitch4.ethg++; + tsnSwitch4.ethg++ <--> EthernetLink <--> tsnSwitch5.ethg++; + tsnSwitch5.ethg++ <--> EthernetLink <--> tsnSwitch6.ethg++; + tsnSwitch6.ethg++ <--> EthernetLink <--> tsnSwitch1.ethg++; + tsnSwitch2.ethg++ <--> EthernetLink <--> tsnDevice1.ethg++; + tsnSwitch3.ethg++ <--> EthernetLink <--> tsnDevice2.ethg++; + tsnSwitch5.ethg++ <--> EthernetLink <--> tsnDevice3.ethg++; + tsnSwitch6.ethg++ <--> EthernetLink <--> tsnDevice4.ethg++; +} diff --git a/showcases/tsn/timesynchronization/gptp_hotstandby/doc/index.rst b/showcases/tsn/timesynchronization/gptp_hotstandby/doc/index.rst new file mode 100644 index 00000000000..8ff9ab140ff --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_hotstandby/doc/index.rst @@ -0,0 +1,303 @@ +MultiDomain gPTP and Hot-Standby +================================= + +Overview +-------- + +In this showcase we present the multi-domain gPTP feature of INET +together with a basic implementation of the IEEE 802.1ASdm :ned:`HotStandby` amendment. +The showcase consists of two networks scenarios: + +- **Primary and Hot-Standby Master Clocks**: More complex setup with two time domains for a primary and a hot-standby master clock. + It implements the :ned:`HotStandby` module, so if the primary master node goes offline, + the stand-by clock can take over and become the new Master Clock. +- **Two Master Clocks Exploiting Network Redundancy**: A larger network containing four domains and a primary and a hot-standby master node, + with two time domains each. Time synchronization is protected against the failure of a master node and any link in the network. + We do not use HotStandby in this scenario. + +Primary and Hot-Standby Master Clocks +------------------------------------- + +The network of this showcase +contains one primary master clock node and one hot-standby master clock node. +Both master clock nodes have their own time synchronization domain. The switch +and device nodes have two clocks, each synchronizing to one of the master clocks +separately. The only connection between the two time domains is in the +hot-standby master clock that is also synchronized to the primary master clock. +This connection effectively causes the two time domains to be totally +synchronized and allows seamless failover in the case of the master clock +failure. + +The network contains two clock nodes (:ned:`TsnClock`) and four TSN device nodes (:ned:`TsnDevice`), connected by two TSN switches (:ned:`TsnSwitch`): + +.. figure:: media/PrimaryAndHotStandbyNetwork.png + :align: center + +Our goal is to configure the two gPTP spanning trees for the two time domains. +In this setup, the clock nodes have one clock, and the others have two (one for +each domain). + +- ``tsnClock1`` (the primary master) has one clock and one gPTP domain, and disseminates timing information to `domain 0` of `all` other nodes. +- ``tsnClock2`` (the hot-standby master) has one clock and two gPTP domains, and disseminates timing information of its `domain 1` to `domain 1` of all other nodes except ``tsnClock1``. +- The clock in ``tsnClock2`` gets synchronized to the primary master node in `domain 0`. + +Let's see the configuration in omnetpp.ini, starting with settings for the clock nodes: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: *.tsnClock2.clock.typename = "PiServoClock" + :end-at: *.tsnClock2.gptp.domain[1].masterPorts = ["eth0"] + +We configure ``tsnClock2`` to have a :ned:`PiServoClock`, so we can set the +time. We configure ``tsnClock1`` to have a :ned:`Gptp` module, and set it as a +master node. Also, we specify that it should use its own clock, and set the only +interface, ``eth0`` to be a master port (the node will send gPTP sync messages +on that port). + +In ``tsnClock2``, we need two :ned:`Gptp` modules (one is a leaf, the other a +root in the tree), so we set the type of the ``gptp`` module to +:ned:`MultiDomainGptp` with two domains. Both domains use the only clock in the +node, but one of them acts as a gPTP master, the other one a gPTP slave (using +the same port, ``eth0``). + +Here is the configuration for the switches: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: *.tsnSwitch*.clock.typename = "MultiClock" + :end-at: *.tsnSwitch2.gptp.domain[1].masterPorts = ["eth1", "eth2", "eth3"] + +We configure the switches to have two clocks and two :ned:`Gptp` modules (one +for each domain). We then specify the spanning tree by setting the ports (the +:par:`gptpModuleType` is ``BRIDGE_NODE`` by default in :ned:`TsnSwitch`, so we +don't need to specify that). In both domains, the interface connecting to the +clock node is the slave port, and the others are master ports. The only +exception is that ``tsnSwitch1`` shouldn't send sync messages to ``tsnClock1`` +(as we don't want it getting synchronized to anything), so the ``eth1`` +interface isn't set as a master port. + +Finally, here is the configuration for the devices: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: *.tsnDevice*.clock.typename = "MultiClock" + :end-at: *.tsnDevice*.gptp.domain[*].slavePort = "eth0" + +Just as in the switches, we need two clocks and two :ned:`Gptp` modules in the +devices as well, so we use :ned:`MultiClock` and :ned:`MultiDomainGptp` with two +submodules. We configure each device's ``gptp`` module to use the +:ned:`MultiClock` module in the device; the appropriate sub-clock for the domain +is automatically selected. We set all ``gptp`` modules to use the only interface +as the slave port (the Gptp module type is ``SLAVE_NODE`` by default in +:ned:`TsnDevice`, so we don't need to configure that). + +We also configure some offsets for the pDelay measurement and gPTP sync messages in the different domains so they are not transmitted concurrently and suffer queueing delays. + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: pdelayInitialOffset + :end-at: domain[1].syncInitialOffset + +Lastly, we enable the :ned:`HotStandby` module on every device besides the +primary and the hot standby masterclock. +This is achieved by setting the :par:`hasHotStandby` parameter: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: *.tsnClock2.gptp.hasHotStandby = false + :end-at: **.hasHotStandby = true + +The :ned:`HotStandby` module is a basic implementation of the IEEE 802.1ASdm +amendment and keeps track of the synchronization state of contained gPTP modules. +In case one gPTP module loses synchronization, it will switch the active clock index +of the :ned:`MultiClock` to the standby clock in case it is still in sync. + +The following is a video of the time synchronization process at the beginning of +the simulation. The clock time in master nodes, and the time difference to this +clock time in the other nodes are displayed for each clock. Messages for gPTP +are visualized as arrows. The visualization is color-coded according to domain: + +.. video_noloop:: media/PrimaryAndHotStandbyMasterClocks.mp4 + :align: center + :width: 100% + +First, the bridge and slave nodes measure link delay by exchanging pDelay +messages. Then, the master clocks send gPTP sync messages. Note that there is a +jump in the time difference when the clocks are set to the new time after the +gPTP follow-up messages are received. + +The spanning tree is visualized as the datalink-layer gPTP message +transmissions. This outlines the flow of timing information in the network, +originating from the master clock: + +.. figure:: media/PrimaryAndHotStandbyMasterClocks_tree.png + :align: center + +To simulate the behavior of the :ned:`HotStandby` failover system, we add a +scenario script whit the following failure cases: + +- From 3.1s to 7.1s the primary master clock is disconnected from the network. +- From 10.1s to 14.1s the link between ``tsnSwitch1`` and ``tsnSwitch2`` is broken, + leading to the network splitting in half. + +.. literalinclude:: ../link_failure.xml + :language: xml + +Let's examine some clock drift charts. Instead of plotting clock drift for all +clocks in one chart, let's use four charts so they are less cluttered. Here is +the clock drift (clock time difference to simulation time) of the two `master +clocks`: + +.. figure:: media/PrimaryAndHotStandBy_masterclocks.png + :align: center + +Both master clocks have a random drift rate, but the hot-standby master clock's +time and clock drift rate are periodically synchronized to the primary. +However, in the failure cases, there is no synchronization between the primary +and the hot-standby master clock possible, thus they drift apart. + +Here is the clock drift of all clocks in `time domain 0` (primary master) during the first failure case: + +.. figure:: media/PrimaryAndHotStandBy_timedomain0_zoomed.png + :align: center + +Each clock has a distinct drift rate, while the master fluctuating randomly. +The slave clocks are periodically synchronized with the master clock. before and after +the first failure case. +During the first failure case, however, it becomes evident that no time synchronization is possible +in domain 1 during that time. + +The following graph shows the same timeframe but plots the +activeClock instead of domain 0: + +.. figure:: media/PrimaryAndHotStandBy_activeclocks_failure1.png + :align: center + +It is evident that shortly after the primary master is disconnected +a sync timeout occurs (typically ``3xsyncInterval=375ms``) and +the :ned:`HotStandby` module switches the :par:`activeClockIndex` to 1. + +In the second failure case a similar thing happens. +However here, there are two parts of the network diverging from each other. +The left parts still synchronizes to the primary master clock, +while the right part synchronizes to the hot-standby master clock: + +.. figure:: media/PrimaryAndHotStandBy_activeclocks_failure2.png + :align: center + +.. _sh:tsn:timesync:gptp:redundancy: + +Two Master Clocks Exploiting Network Redundancy +----------------------------------------------- + +In this configuration the network topology is a ring. The primary master clock +and the hot-standby master clock each have two separate time domains. One time +domain uses the clockwise and another one uses the counterclockwise direction in +the ring topology to disseminate the clock time in the network. This approach +provides protection against the failure of the primary master node `and` a +single link failure in the ring because all bridges can be reached in both +directions by one of the time synchronization domains of both master clocks. + +Here is the network (it uses the same node types as the previous ones, +:ned:`TsnClock`, :ned:`TsnSwitch` and :ned:`TsnDevice`): + +.. figure:: media/TwoMasterClocksNetwork.png + :align: center + +The time synchronization redundancy is achieved in the following way: + +- The primary master node has one clock and two master gPTP time domains. The domains send timing information in the ring in the clockwise and counterclockwise direction. +- The hot-standby master node has two slave and two master gPTP domains, and two sub-clocks. Domains 0 and 1 sync the two clocks to the primary master's two domains, and domains 2 and 3 send timing information of the two clocks in both directions in the ring. +- Switch and device nodes have four domains (and four sub-clocks), with domains 0 and 1 syncing to the primary master node, and domains 2 and 3 to the hot-standby master node. +- Consequently, gPTP modules in the switches are gPTP bridges, in the devices, gPTP slaves. + +In case of failure of the primary master node `and` a link in the ring, the +switches and devices would have at least one synchronized clock they could +switch over to. + +How do we configure this scheme? We add the needed gPTP domains and clocks, and +configure the spanning tree outlined above. Some important aspects when setting +the ports and clocks: + +- We don't want to forward any timing information to the primary master node, so we set the master ports in ``tsnSwitch1`` accordingly. +- We take care not to forward timing messages to a switch that originally sent it (so sync messages don't go in circles indefinitely). For example, tsnSwitch6 shouldn't send sync messages to ``tsnSwitch1`` in domain 0. +- The hot-standby master node has just two clocks, used by the four domains. The timing information from domains 0 and 1 is transferred to domains 2 and 3 here. So we set domains 0 and 2 to use ``clock[0]``, and domains 1 and 3 to use ``clock[1]``. + +Here is the configuration for the clock nodes: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: TwoMasterClocksExploitingNetworkRedundancy + :end-at: *.tsnClock2.gptp.domain[3].masterPorts = ["eth0"] + +We set ``tsnClock1`` to have two :ned:`Gptp` modules, each using the only clock +in the host. The type of the clock network nodes is :ned:`TsnClock`; in these, +the :ned:`Gptp` modules are set to master by default. We set the master ports in +both modules, so they disseminate timing information on their only Ethernet +interface. + +``tsnClock2`` is set to have four gPTP domains. Since ``tsnClock2`` has only two +sub-clocks, we need to specify that domains 2 and 3 use ``clock[0]`` and +``clock[1]`` in the :ned:`MultiClock` module (it is sufficient to set the +:par:`clockModule` parameter to the :ned:`MultiClock` module, as it assigns the +sub-clocks to the domains automatically). + +Thus, in ``tsnClock2``, domains 0 and 1 are gPTP slaves, syncing to the two +domains of the primary master. Domains 2 and 3 are gPTP masters, and disseminate +the time of the clocks set by the first two domains. + +The configuration for the switches is the following: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-after: *.tsnClock2.gptp.domain[3].masterPorts = ["eth0"] + :end-at: *.tsnSwitch6.gptp.domain[3].slavePort = "eth1" + +Here is the configuration for the devices: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: *.tsnDevice*.clock.typename = "MultiClock" + :end-at: *.tsnDevice*.gptp.domain[*].slavePort = "eth0" + +Finally, we configure offsets for the four domains, so that they don't send sync +messages at the same time: + +.. literalinclude:: ../omnetpp.ini + :language: ini + :start-at: **.pdelayInitialOffset = 0.1ms + :end-at: *.*.gptp.domain[3].syncInitialOffset = syncInterval * 4 / 4 + +Here is the spanning tree visualized by gPTP messages: + +.. figure:: media/ExploitingNetworkRedundancy_tree.png + :align: center + +Just as in the previous sections, let's examine the clock drift of the different +clocks in the network. Here is the clock drift of the master clocks: + +.. figure:: media/ExploitingNetworkRedundancy_masterclocks_zoomed.png + :align: center + +The clocks of the hot-standby master node are synced to the time of the primary +master periodically. Note that the sync times have the offset we configured. +Let's see the clock drifts in domain 0 (the primary master clock is plotted with +the thicker line): + +.. figure:: media/ExploitingNetworkRedundancy_domain0_zoomed.png + :align: center + +In Domain 0, all clocks sync to the primary master clock. They sync at the same +time, because the offsets are between domains. The clock drift in Domain 1 is +similar, so we don't include it here. Let's see Domain 2 (the primary master +clock is displayed with a dashed line for reference, as it's not part of this +domain; the hot-standby master clock in this domain is displayed with the +thicker line): + +.. figure:: media/ExploitingNetworkRedundancy_domain2_zoomed.png + :align: center + +All switches and devices sync to the hot-standby master clock (which itself is +synced to the primary master periodically). + +.. note:: The charts for all domains are available in the .anf file in the showcase's folder. diff --git a/showcases/tsn/timesynchronization/gptp_hotstandby/doc/media b/showcases/tsn/timesynchronization/gptp_hotstandby/doc/media new file mode 120000 index 00000000000..d32774ceae2 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_hotstandby/doc/media @@ -0,0 +1 @@ +../../../../../media/showcases/tsn/timesynchronization/gptp_hotstandby/doc \ No newline at end of file diff --git a/showcases/tsn/timesynchronization/gptp_hotstandby/link_failure.xml b/showcases/tsn/timesynchronization/gptp_hotstandby/link_failure.xml new file mode 100644 index 00000000000..45a8db6ce12 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_hotstandby/link_failure.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/showcases/tsn/timesynchronization/gptp_hotstandby/omnetpp.ini b/showcases/tsn/timesynchronization/gptp_hotstandby/omnetpp.ini new file mode 100644 index 00000000000..520bc153309 --- /dev/null +++ b/showcases/tsn/timesynchronization/gptp_hotstandby/omnetpp.ini @@ -0,0 +1,296 @@ +[General] +seed-set = 0 +sim-time-limit = 20s +description = "abstract" + +# enable time synchronization in all network nodes +*.*.hasTimeSynchronization = true + +# all oscillators have a constant drift rate (specified with a random distribution for each one) +# except for the master clocks, which have a random drift rate +#**.clock.oscillator.typename = "RandomDriftOscillator" +*.tsnClock*.clock.oscillator.typename = "RandomDriftOscillator" +**.oscillator.changeInterval = 12.5ms +**.oscillator.driftRate = uniform(-100ppm, 100ppm) +**.oscillator.driftRateChange = uniform(-1ppm, 1ppm) +**.oscillator.driftRateChangeUpperLimit = 100ppm +**.oscillator.driftRateChangeLowerLimit = -100ppm + +**.kp=8 +**.ki=7 +**.offsetThreshold = 3us + + +# all Ethernet interfaces have 100 Mbps speed +*.*.eth[*].bitrate = 100Mbps + +*.visualizer.typename = "IntegratedMultiCanvasVisualizer" +*.visualizer.infoVisualizer[*].displayInfos = true + +[Config PrimaryAndHotStandbyMasterClocks] +network = TwoMasterClocksTreeGptpShowcase +description = "Extended tree topology with two master clocks" + +*.scenarioManager.script = xmldoc("link_failure.xml") + +**.oscillator.typename = "RandomDriftOscillator" + +*.tsnClock2.gptp.hasHotStandby = false +**.hasHotStandby = true + +# TSN clock2 has a pi clock +*.tsnClock2.clock.typename = "PiServoClock" + +*.tsnClock1.gptp.typename = "Gptp" +*.tsnClock1.gptp.clockModule = "tsnClock1.clock" +*.tsnClock1.gptp.masterPorts = ["eth0"] + +*.tsnClock2.gptp.typename = "MultiDomainGptp" +*.tsnClock2.gptp.numDomains = 2 +*.tsnClock2.gptp.domain[*].clockModule = "tsnClock2.clock" +*.tsnClock2.gptp.domain[0].gptpNodeType = "SLAVE_NODE" +*.tsnClock2.gptp.domain[0].slavePort = "eth0" +*.tsnClock2.gptp.domain[1].gptpNodeType = "MASTER_NODE" +*.tsnClock2.gptp.domain[1].masterPorts = ["eth0"] + +# TSN switches have multiple clocks +*.tsnSwitch*.clock.typename = "MultiClock" +*.tsnSwitch*.clock.numClocks = 2 + +# TSN switches have multiple gPTP time synchronization domains +*.tsnSwitch*.gptp.typename = "MultiDomainGptp" +*.tsnSwitch*.gptp.numDomains = 2 +*.tsnSwitch1.gptp.domain[0].slavePort = "eth0" +*.tsnSwitch1.gptp.domain[0].masterPorts = ["eth1", "eth2", "eth3"] +*.tsnSwitch1.gptp.domain[1].slavePort = "eth1" +*.tsnSwitch1.gptp.domain[1].masterPorts = ["eth2", "eth3"] # eth1 is omitted because no sync needed towards primary master +*.tsnSwitch2.gptp.domain[0].slavePort = "eth1" +*.tsnSwitch2.gptp.domain[0].masterPorts = ["eth0", "eth2", "eth3"] +*.tsnSwitch2.gptp.domain[1].slavePort = "eth0" +*.tsnSwitch2.gptp.domain[1].masterPorts = ["eth1", "eth2", "eth3"] + +# TSN devices have multiple clocks +*.tsnDevice*.clock.typename = "MultiClock" +*.tsnDevice*.clock.numClocks = 2 + +# TSN devices have multiple gPTP time synchronization domains +*.tsnDevice*.gptp.typename = "MultiDomainGptp" +*.tsnDevice*.gptp.numDomains = 2 +*.tsnDevice1.gptp.clockModule = "tsnDevice1.clock" +*.tsnDevice2.gptp.clockModule = "tsnDevice2.clock" +*.tsnDevice3.gptp.clockModule = "tsnDevice3.clock" +*.tsnDevice4.gptp.clockModule = "tsnDevice4.clock" +*.tsnDevice*.gptp.domain[*].slavePort = "eth0" + +# different initial gPTP pdelay measurement and time synchronization offsets +**.pdelayInitialOffset = 100us +*.*.gptp.domain[0].syncInitialOffset = syncInterval * 1 / 2 +*.*.gptp.domain[1].syncInitialOffset = syncInterval * 2 / 2 + +# set reference clocks so the time difference can be visualized +**.clock[0].referenceClock = "tsnClock1.clock" +**.clock[1].referenceClock = "tsnClock2.clock" + +# multiple data link visualizers display different gPTP time synchronization domain packets +*.visualizer.numDataLinkVisualizers = 2 +*.visualizer.dataLinkVisualizer[*].displayLinks = true +*.visualizer.dataLinkVisualizer[*].activityLevel = "protocol" +*.visualizer.dataLinkVisualizer[0].packetFilter = expr((has(GptpSync) && GptpSync.domainNumber == 0) || (has(GptpFollowUp) && GptpFollowUp.domainNumber == 0)) +*.visualizer.dataLinkVisualizer[1].packetFilter = expr((has(GptpSync) && GptpSync.domainNumber == 1) || (has(GptpFollowUp) && GptpFollowUp.domainNumber == 1)) +*.visualizer.dataLinkVisualizer[0].tags = "primary GM" +*.visualizer.dataLinkVisualizer[1].tags = "hot-standby GM" +*.visualizer.dataLinkVisualizer[0].lineColor = "blue2" +*.visualizer.dataLinkVisualizer[1].lineColor = "red2" + +*.visualizer.numInfoVisualizers = 8 +*.visualizer.infoVisualizer[0].modules = "*.tsnDevice*.*.clock[0]" +*.visualizer.infoVisualizer[0].textColor = "blue" +*.visualizer.infoVisualizer[1].modules = "*.tsnDevice*.*.clock[1]" +*.visualizer.infoVisualizer[1].textColor = "red" +*.visualizer.infoVisualizer[0..1].placementHint = "bottom" +*.visualizer.infoVisualizer[2].modules = "*.tsnClock1.clock" +*.visualizer.infoVisualizer[3].modules = "*.tsnClock2.clock" +*.visualizer.infoVisualizer[2].textColor = "blue" +*.visualizer.infoVisualizer[3].textColor = "red" +*.tsnClock*.clock.displayStringTextFormat = "time: %T" +*.visualizer.infoVisualizer[4].modules = "*.tsnSwitch1.*.clock[0]" +*.visualizer.infoVisualizer[5].modules = "*.tsnSwitch1.*.clock[1]" +*.visualizer.infoVisualizer[6].modules = "*.tsnSwitch2.*.clock[0]" +*.visualizer.infoVisualizer[7].modules = "*.tsnSwitch2.*.clock[1]" +*.visualizer.infoVisualizer[{4,6}].textColor = "blue" +*.visualizer.infoVisualizer[{5,7}].textColor = "red" +*.tsnDevice*.clock.*.displayStringTextFormat = "diff: %d" +*.tsnSwitch*.clock.*.displayStringTextFormat = "diff: %d" +*.visualizer.infoVisualizer[4..5].placementHint = "topLeft" +*.visualizer.infoVisualizer[6..7].placementHint = "topRight" + +[Config TwoMasterClocksExploitingNetworkRedundancy] +network = TwoMasterClocksRingGptpShowcase +description = "Ring topology with redundant time synchronization domains" +# clock visualization note: bridge and slave nodes display difference from corresponding master clock + +**.hasHotStandby = false + +# TSN clock2 has multiple clocks +*.tsnClock2.clock.typename = "MultiClock" +*.tsnClock2.clock.numClocks = 2 + +# TSN clocks have multiple gPTP time synchronization domains +*.tsnClock*.gptp.typename = "MultiDomainGptp" +*.tsnClock1.gptp.numDomains = 2 +*.tsnClock1.gptp.domain[0..1].clockModule = "tsnClock1.clock" +*.tsnClock1.gptp.domain[0].masterPorts = ["eth0"] +*.tsnClock1.gptp.domain[1].masterPorts = ["eth0"] +*.tsnClock2.gptp.numDomains = 4 +*.tsnClock2.gptp.domain[2..3].clockModule = "tsnClock2.clock" +*.tsnClock2.gptp.domain[0].gptpNodeType = "SLAVE_NODE" +*.tsnClock2.gptp.domain[0].slavePort = "eth0" +*.tsnClock2.gptp.domain[1].gptpNodeType = "SLAVE_NODE" +*.tsnClock2.gptp.domain[1].slavePort = "eth0" +*.tsnClock2.gptp.domain[2].gptpNodeType = "MASTER_NODE" +*.tsnClock2.gptp.domain[2].masterPorts = ["eth0"] +*.tsnClock2.gptp.domain[3].gptpNodeType = "MASTER_NODE" +*.tsnClock2.gptp.domain[3].masterPorts = ["eth0"] + +# TSN switches have multiple clocks +*.tsnSwitch*.clock.typename = "MultiClock" +*.tsnSwitch*.clock.numClocks = 4 + +# TSN switches have multiple gPTP time synchronization domains +*.tsnSwitch*.gptp.typename = "MultiDomainGptp" +*.tsnSwitch*.gptp.numDomains = 4 + +# TSN switch 1 +*.tsnSwitch1.gptp.domain[0].masterPorts = ["eth1"] +*.tsnSwitch1.gptp.domain[0].slavePort = "eth0" +*.tsnSwitch1.gptp.domain[1].masterPorts = ["eth2"] +*.tsnSwitch1.gptp.domain[1].slavePort = "eth0" +*.tsnSwitch1.gptp.domain[2].masterPorts = ["eth1"] +*.tsnSwitch1.gptp.domain[2].slavePort = "eth2" +*.tsnSwitch1.gptp.domain[3].masterPorts = ["eth2"] +*.tsnSwitch1.gptp.domain[3].slavePort = "eth1" + +# TSN switch 2 +*.tsnSwitch2.gptp.domain[0].masterPorts = ["eth1", "eth2"] +*.tsnSwitch2.gptp.domain[0].slavePort = "eth0" +*.tsnSwitch2.gptp.domain[1].masterPorts = ["eth2"] +*.tsnSwitch2.gptp.domain[1].slavePort = "eth1" +*.tsnSwitch2.gptp.domain[2].masterPorts = ["eth1", "eth2"] +*.tsnSwitch2.gptp.domain[2].slavePort = "eth0" +*.tsnSwitch2.gptp.domain[3].masterPorts = ["eth0", "eth2"] +*.tsnSwitch2.gptp.domain[3].slavePort = "eth1" + +# TSN switch 3 +*.tsnSwitch3.gptp.domain[0].masterPorts = ["eth1", "eth2"] +*.tsnSwitch3.gptp.domain[0].slavePort = "eth0" +*.tsnSwitch3.gptp.domain[1].masterPorts = ["eth0", "eth2"] +*.tsnSwitch3.gptp.domain[1].slavePort = "eth1" +*.tsnSwitch3.gptp.domain[2].masterPorts = ["eth2"] +*.tsnSwitch3.gptp.domain[2].slavePort = "eth0" +*.tsnSwitch3.gptp.domain[3].masterPorts = ["eth0", "eth2"] +*.tsnSwitch3.gptp.domain[3].slavePort = "eth1" + +# TSN switch 4 +*.tsnSwitch4.gptp.domain[0].masterPorts = ["eth0", "eth2"] +*.tsnSwitch4.gptp.domain[0].slavePort = "eth1" +*.tsnSwitch4.gptp.domain[1].masterPorts = ["eth0", "eth1"] +*.tsnSwitch4.gptp.domain[1].slavePort = "eth2" +*.tsnSwitch4.gptp.domain[2].masterPorts = ["eth2"] +*.tsnSwitch4.gptp.domain[2].slavePort = "eth0" +*.tsnSwitch4.gptp.domain[3].masterPorts = ["eth1"] +*.tsnSwitch4.gptp.domain[3].slavePort = "eth0" + +# TSN switch 5 +*.tsnSwitch5.gptp.domain[0].masterPorts = ["eth1", "eth2"] +*.tsnSwitch5.gptp.domain[0].slavePort = "eth0" +*.tsnSwitch5.gptp.domain[1].masterPorts = ["eth0", "eth2"] +*.tsnSwitch5.gptp.domain[1].slavePort = "eth1" +*.tsnSwitch5.gptp.domain[2].masterPorts = ["eth1", "eth2"] +*.tsnSwitch5.gptp.domain[2].slavePort = "eth0" +*.tsnSwitch5.gptp.domain[3].masterPorts = ["eth2"] +*.tsnSwitch5.gptp.domain[3].slavePort = "eth1" + +# TSN switch 6 +*.tsnSwitch6.gptp.domain[0].masterPorts = ["eth2"] +*.tsnSwitch6.gptp.domain[0].slavePort = "eth0" +*.tsnSwitch6.gptp.domain[1].masterPorts = ["eth0", "eth2"] +*.tsnSwitch6.gptp.domain[1].slavePort = "eth1" +*.tsnSwitch6.gptp.domain[2].masterPorts = ["eth1", "eth2"] +*.tsnSwitch6.gptp.domain[2].slavePort = "eth0" +*.tsnSwitch6.gptp.domain[3].masterPorts = ["eth0", "eth2"] +*.tsnSwitch6.gptp.domain[3].slavePort = "eth1" + +# TSN devices have multiple clocks +*.tsnDevice*.clock.typename = "MultiClock" +*.tsnDevice*.clock.numClocks = 4 + +# TSN devices have multiple gPTP time synchronization domains +*.tsnDevice*.gptp.typename = "MultiDomainGptp" +*.tsnDevice*.gptp.numDomains = 4 +*.tsnDevice1.gptp.clockModule = "tsnDevice1.clock" +*.tsnDevice2.gptp.clockModule = "tsnDevice2.clock" +*.tsnDevice3.gptp.clockModule = "tsnDevice3.clock" +*.tsnDevice4.gptp.clockModule = "tsnDevice4.clock" +*.tsnDevice*.gptp.domain[*].slavePort = "eth0" + +# different initial gPTP pdelay measurement and time synchronization offsets +**.pdelayInitialOffset = 0.1ms +*.*.gptp.domain[0].syncInitialOffset = syncInterval * 1 / 4 +*.*.gptp.domain[1].syncInitialOffset = syncInterval * 2 / 4 +*.*.gptp.domain[2].syncInitialOffset = syncInterval * 3 / 4 +*.*.gptp.domain[3].syncInitialOffset = syncInterval * 4 / 4 + +# multiple data link visualizers display different gPTP time synchronization domain packets +*.visualizer.typename = "IntegratedMultiCanvasVisualizer" +*.visualizer.numDataLinkVisualizers = 4 +*.visualizer.dataLinkVisualizer[*].displayLinks = true +*.visualizer.dataLinkVisualizer[*].activityLevel = "protocol" +*.visualizer.dataLinkVisualizer[0].packetFilter = expr(has(GptpSync) && GptpSync.domainNumber == 0) +*.visualizer.dataLinkVisualizer[1].packetFilter = expr(has(GptpSync) && GptpSync.domainNumber == 1) +*.visualizer.dataLinkVisualizer[2].packetFilter = expr(has(GptpSync) && GptpSync.domainNumber == 2) +*.visualizer.dataLinkVisualizer[3].packetFilter = expr(has(GptpSync) && GptpSync.domainNumber == 3) +*.visualizer.dataLinkVisualizer[0].tags = "primary GM domain0" +*.visualizer.dataLinkVisualizer[1].tags = "primary GM domain1" +*.visualizer.dataLinkVisualizer[2].tags = "hot-standby GM domain0" +*.visualizer.dataLinkVisualizer[3].tags = "hot-standby GM domain1" +*.visualizer.dataLinkVisualizer[0].lineColor = "blue4" +*.visualizer.dataLinkVisualizer[1].lineColor = "blue1" +*.visualizer.dataLinkVisualizer[2].lineColor = "red4" +*.visualizer.dataLinkVisualizer[3].lineColor = "red1" + +*.tsnClock1.clock.referenceClock = "tsnClock1.clock" # so how does it sync to domain1? +*.tsnClock2.clock.clock[*].referenceClock = "tsnClock1.clock" +*.tsnSwitch*.clock.clock[0..1].referenceClock = "tsnClock1.clock" +*.tsnDevice*.clock.clock[0..1].referenceClock = "tsnClock1.clock" +*.tsnSwitch*.clock.clock[2..3].referenceClock = "tsnClock2.clock.clock[1]" +*.tsnDevice*.clock.clock[2..3].referenceClock = "tsnClock2.clock.clock[1]" +*.tsnSwitch*.clock.clock[*].displayStringTextFormat = "%d" +*.tsnDevice*.clock.clock[*].displayStringTextFormat = "%d" +*.tsnClock1.clock.displayStringTextFormat = "time: %T" +*.tsnClock2.clock.clock*.displayStringTextFormat = "time: %T\ndiff: %d" + +*.visualizer.numInfoVisualizers = 11 +*.visualizer.infoVisualizer[0].modules = "*.tsnClock1.clock" +*.visualizer.infoVisualizer[0].textColor = "blue4" +*.visualizer.infoVisualizer[1].modules = "*.tsnSwitch1.*.clock[0] or *.tsnSwitch2.*.clock[0] or *.tsnSwitch6.*.clock[0] or *.tsnDevice1.*.clock[0] or *.tsnDevice4.*.clock[0]" +*.visualizer.infoVisualizer[1].textColor = "blue4" +*.visualizer.infoVisualizer[2].modules = "*.tsnSwitch1.*.clock[1] or *.tsnSwitch2.*.clock[1] or *.tsnSwitch6.*.clock[1] or *.tsnDevice1.*.clock[1] or *.tsnDevice4.*.clock[1]" +*.visualizer.infoVisualizer[2].textColor = "blue1" +*.visualizer.infoVisualizer[3].modules = "*.tsnSwitch1.*.clock[2] or *.tsnSwitch2.*.clock[2] or *.tsnSwitch6.*.clock[2] or *.tsnDevice1.*.clock[2] or *.tsnDevice4.*.clock[2]" +*.visualizer.infoVisualizer[3].textColor = "red4" +*.visualizer.infoVisualizer[4].modules = "*.tsnSwitch1.*.clock[3] or *.tsnSwitch2.*.clock[3] or *.tsnSwitch6.*.clock[3] or *.tsnDevice1.*.clock[3] or *.tsnDevice4.*.clock[3]" +*.visualizer.infoVisualizer[4].textColor = "red1" +*.visualizer.infoVisualizer[5].modules = "*.tsnDevice2.*.clock[0] or *.tsnDevice3.*.clock[0] or *.tsnSwitch3.*.clock[0] or *.tsnSwitch4.*.clock[0] or *.tsnSwitch5.*.clock[0]" +*.visualizer.infoVisualizer[5].textColor = "blue4" +*.visualizer.infoVisualizer[6].modules = "*.tsnDevice2.*.clock[1] or *.tsnDevice3.*.clock[1] or *.tsnSwitch3.*.clock[1] or *.tsnSwitch4.*.clock[1] or *.tsnSwitch5.*.clock[1]" +*.visualizer.infoVisualizer[6].textColor = "blue1" +*.visualizer.infoVisualizer[7].modules = "*.tsnDevice2.*.clock[2] or *.tsnDevice3.*.clock[2] or *.tsnSwitch3.*.clock[2] or *.tsnSwitch4.*.clock[2] or *.tsnSwitch5.*.clock[2]" +*.visualizer.infoVisualizer[7].textColor = "red4" +*.visualizer.infoVisualizer[8].modules = "*.tsnDevice2.*.clock[3] or *.tsnDevice3.*.clock[3] or *.tsnSwitch3.*.clock[3] or *.tsnSwitch4.*.clock[3] or *.tsnSwitch5.*.clock[3]" +*.visualizer.infoVisualizer[8].textColor = "red1" +*.visualizer.infoVisualizer[9].modules = "*.tsnClock2.*.clock[0]" +*.visualizer.infoVisualizer[9].textColor = "red4" +*.visualizer.infoVisualizer[10].modules = "*.tsnClock2.*.clock[1]" +*.visualizer.infoVisualizer[10].textColor = "red1" +*.visualizer.infoVisualizer[5..10].placementHint = "bottom" + diff --git a/showcases/tsn/timesynchronization/index.rst b/showcases/tsn/timesynchronization/index.rst index 13627032b88..b84af2f15c9 100644 --- a/showcases/tsn/timesynchronization/index.rst +++ b/showcases/tsn/timesynchronization/index.rst @@ -13,3 +13,5 @@ The following showcases demonstrate topics related to time synchronization: clockdrift/doc/index gptp/doc/index + gptp_bmca/doc/index + gptp_hotstandby/doc/index diff --git a/src/inet/applications/clock/SimpleClockSynchronizer.cc b/src/inet/applications/clock/SimpleClockSynchronizer.cc index e60a8026257..1b3551395fe 100644 --- a/src/inet/applications/clock/SimpleClockSynchronizer.cc +++ b/src/inet/applications/clock/SimpleClockSynchronizer.cc @@ -57,7 +57,9 @@ void SimpleClockSynchronizer::synchronizeSlaveClock() ppm oscillatorCompensation = unit(getCurrentRelativeTickLength(slaveClock.get()) / getCurrentRelativeTickLength(masterClock.get()) * (1 + masterOscillatorBasedClock->getOscillatorCompensation().get()) * (1 + ppm(synchronizationOscillatorCompensationErrorParameter->doubleValue()).get()) - 1); - slaveClock->setClockTime(clockTime, oscillatorCompensation, true); + slaveClock->adjustClockTo(clockTime); + slaveClock->resetOscillator(); + slaveClock->setOscillatorCompensation(oscillatorCompensation); } void SimpleClockSynchronizer::scheduleSynchronizationTimer() diff --git a/src/inet/applications/clock/SimpleClockSynchronizer.h b/src/inet/applications/clock/SimpleClockSynchronizer.h index 0fea2cf4ba1..5d46348811c 100644 --- a/src/inet/applications/clock/SimpleClockSynchronizer.h +++ b/src/inet/applications/clock/SimpleClockSynchronizer.h @@ -11,7 +11,7 @@ #include #include "inet/applications/base/ApplicationBase.h" -#include "inet/clock/model/SettableClock.h" +#include "inet/clock/servo/ServoClockBase.h" #include "inet/common/ModuleRefByPar.h" namespace inet { @@ -21,7 +21,7 @@ class INET_API SimpleClockSynchronizer : public ApplicationBase protected: cMessage *synhronizationTimer = nullptr; ModuleRefByPar masterClock; - ModuleRefByPar slaveClock; + ModuleRefByPar slaveClock; cPar *synchronizationIntervalParameter = nullptr; cPar *synchronizationClockTimeErrorParameter = nullptr; cPar *synchronizationOscillatorCompensationErrorParameter = nullptr; diff --git a/src/inet/clock/base/ClockBase.cc b/src/inet/clock/base/ClockBase.cc index 754d2439ec9..ed2d9b670f7 100644 --- a/src/inet/clock/base/ClockBase.cc +++ b/src/inet/clock/base/ClockBase.cc @@ -11,6 +11,8 @@ namespace inet { simsignal_t ClockBase::timeChangedSignal = cComponent::registerSignal("timeChanged"); +simsignal_t ClockBase::timeDifferenceToReferenceSignal = cComponent::registerSignal("timeDifferenceToReference"); +simsignal_t ClockBase::timeJumpedSignal = cComponent::registerSignal("timeJumped"); void ClockBase::initialize(int stage) { @@ -24,7 +26,16 @@ void ClockBase::initialize(int stage) } else if (stage == INITSTAGE_LAST) { referenceClockModule.reference(this, "referenceClock", false); + // Subscribe + if (referenceClockModule != nullptr && dynamic_cast(referenceClockModule.get()) != nullptr && + this != referenceClockModule) + { + auto referenceClock = check_and_cast(referenceClockModule.get()); + this->subscribe(ClockBase::timeChangedSignal, this); + referenceClock->subscribe(ClockBase::timeChangedSignal, this); + } emit(timeChangedSignal, getClockTime().asSimTime()); + emitTimeDifferenceToReference(); } } @@ -32,17 +43,33 @@ void ClockBase::handleMessage(cMessage *msg) { if (msg == timer) { emit(timeChangedSignal, getClockTime().asSimTime()); + emitTimeDifferenceToReference(); scheduleAfter(emitClockTimeInterval, timer); } else throw cRuntimeError("Unknown message"); } -void ClockBase::finish() +void ClockBase::emitTimeDifferenceToReference() { - emit(timeChangedSignal, getClockTime().asSimTime()); + if (referenceClockModule == nullptr) + return; + auto referenceTime = referenceClockModule->getClockTime(); + auto timeDifference = getClockTime() - referenceTime; + emit(timeDifferenceToReferenceSignal, timeDifference.asSimTime()); } +void ClockBase::receiveSignal(cComponent *source, int signal, const simtime_t& time, cObject *details) +{ + if (signal == ClockBase::timeChangedSignal) { + emitTimeDifferenceToReference(); + } + else + throw cRuntimeError("Unknown signal"); +} + +void ClockBase::finish() { emit(timeChangedSignal, getClockTime().asSimTime()); } + clocktime_t ClockBase::getClockTime() const { diff --git a/src/inet/clock/base/ClockBase.h b/src/inet/clock/base/ClockBase.h index dbb9d741620..a6f32a71b90 100644 --- a/src/inet/clock/base/ClockBase.h +++ b/src/inet/clock/base/ClockBase.h @@ -16,14 +16,22 @@ namespace inet { -class INET_API ClockBase : public SimpleModule, public IClock +class INET_API ClockBase : public SimpleModule, public IClock, public cListener { public: - static simsignal_t timeChangedSignal; + struct ClockJumpDetails : public cObject { + clocktime_t oldClockTime; + clocktime_t newClockTime; + }; + + public: + static simsignal_t timeChangedSignal; // Called every time there is a change in the speed of the clock oscillator + static simsignal_t timeDifferenceToReferenceSignal; // Called every time there is a change in the speed of the clock oscillator + static simsignal_t timeJumpedSignal; // Only called when the clock performs an immediate jump to a new time + ModuleRefByPar referenceClockModule; protected: clocktime_t clockEventTime = -1; - ModuleRefByPar referenceClockModule; simtime_t emitClockTimeInterval; cMessage *timer = nullptr; @@ -32,6 +40,7 @@ class INET_API ClockBase : public SimpleModule, public IClock virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void handleMessage(cMessage *msg) override; + void emitTimeDifferenceToReference(); virtual void finish() override; cSimpleModule *getTargetModule() const { @@ -48,6 +57,7 @@ class INET_API ClockBase : public SimpleModule, public IClock virtual void scheduleClockEventAfter(clocktime_t time, ClockEvent *event) override; virtual ClockEvent *cancelClockEvent(ClockEvent *event) override; virtual void handleClockEvent(ClockEvent *event) override; + virtual void receiveSignal(cComponent *source, int signal, const simtime_t& time, cObject *details) override; virtual std::string resolveDirective(char directive) const override; }; diff --git a/src/inet/clock/base/ClockBase.ned b/src/inet/clock/base/ClockBase.ned index 6ee94844b45..8830beaf659 100644 --- a/src/inet/clock/base/ClockBase.ned +++ b/src/inet/clock/base/ClockBase.ned @@ -22,6 +22,8 @@ module ClockBase extends Module @class(ClockBase); @display("i=block/timer"); @signal[timeChanged](type=simtime_t); + @signal[timeDifferenceToReference](type=simtime_t); + @signal[timeJumped](); // Signal is emitted, when the clock performs a time jump. This is for example required for the gPTP module to work correctly. @statistic[timeChanged](title="Clock time"; record=vector; interpolationmode=linear); + @statistic[timeDifferenceToReference](title="Time difference to reference"; record=vector; interpolationmode=linear); } - diff --git a/src/inet/clock/model/MultiClock.cc b/src/inet/clock/model/MultiClock.cc index 2ae0703dfb9..a6a3779bb80 100644 --- a/src/inet/clock/model/MultiClock.cc +++ b/src/inet/clock/model/MultiClock.cc @@ -4,7 +4,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // - #include "inet/clock/model/MultiClock.h" #include "inet/clock/base/ClockBase.h" @@ -13,32 +12,57 @@ namespace inet { Define_Module(MultiClock); +simsignal_t MultiClock::activeClockIndexChangedSignal = cComponent::registerSignal("activeClockIndexChanged"); + void MultiClock::initialize(int stage) { if (stage == INITSTAGE_LOCAL) { activeClock = check_and_cast(getSubmodule("clock", par("activeClockIndex"))); subscribe(ClockBase::timeChangedSignal, this); + subscribe(ClockBase::timeJumpedSignal, this); + subscribe(ClockBase::timeDifferenceToReferenceSignal, this); + int activeClockIndex = par("activeClockIndex"); + emit(activeClockIndexChangedSignal, activeClockIndex); } } void MultiClock::handleParameterChange(const char *name) { if (!strcmp(name, "activeClockIndex")) { + int activeClockIndex = par("activeClockIndex"); emit(ClockBase::timeChangedSignal, getClockTime().asSimTime()); activeClock = check_and_cast(getSubmodule("clock", par("activeClockIndex"))); + emit(activeClockIndexChangedSignal, activeClockIndex); emit(ClockBase::timeChangedSignal, getClockTime().asSimTime()); } } -void MultiClock::receiveSignal(cComponent *source, int signal, const simtime_t& time, cObject *details) +void MultiClock::receiveSignal(cComponent *source, int signal, const simtime_t &time, cObject *details) { if (signal == ClockBase::timeChangedSignal) { - if (check_and_cast(source) == activeClock) + if (check_and_cast(source) == activeClock) { emit(ClockBase::timeChangedSignal, time, details); + } + } + else if (signal == ClockBase::timeDifferenceToReferenceSignal) { + // Might want to replace this with own signal and own reference clock + if (check_and_cast(source) == activeClock) { + emit(ClockBase::timeDifferenceToReferenceSignal, time, details); + } + } + else { + throw cRuntimeError("Unknown signal"); + } +} + +void MultiClock::receiveSignal(cComponent *source, int signal, cObject *obj, cObject *details) +{ + if (signal == ClockBase::timeJumpedSignal) { + if (check_and_cast(source) == activeClock) + emit(ClockBase::timeJumpedSignal, this, details); } else throw cRuntimeError("Unknown signal"); } } // namespace inet - diff --git a/src/inet/clock/model/MultiClock.h b/src/inet/clock/model/MultiClock.h index 7c47020e219..6b52d6cc50c 100644 --- a/src/inet/clock/model/MultiClock.h +++ b/src/inet/clock/model/MultiClock.h @@ -4,7 +4,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // - #ifndef __INET_MULTICLOCK_H #define __INET_MULTICLOCK_H @@ -12,6 +11,7 @@ #include "inet/clock/common/ClockEvent.h" #include "inet/clock/common/ClockTime.h" #include "inet/clock/contract/IClock.h" +#include "inet/common/ModuleRefByPar.h" namespace inet { @@ -26,17 +26,32 @@ class INET_API MultiClock : public Module, public virtual IClock, public cListen public: virtual clocktime_t getClockTime() const override { return activeClock->getClockTime(); } - virtual clocktime_t computeClockTimeFromSimTime(simtime_t time) const override { return activeClock->computeClockTimeFromSimTime(time); } - virtual simtime_t computeSimTimeFromClockTime(clocktime_t time) const override { return activeClock->computeSimTimeFromClockTime(time); } - virtual void scheduleClockEventAt(clocktime_t time, ClockEvent *event) override { activeClock->scheduleClockEventAt(time, event); } - virtual void scheduleClockEventAfter(clocktime_t delay, ClockEvent *event) override { activeClock->scheduleClockEventAfter(delay, event); } + virtual clocktime_t computeClockTimeFromSimTime(simtime_t time) const override + { + return activeClock->computeClockTimeFromSimTime(time); + } + virtual simtime_t computeSimTimeFromClockTime(clocktime_t time) const override + { + return activeClock->computeSimTimeFromClockTime(time); + } + virtual void scheduleClockEventAt(clocktime_t time, ClockEvent *event) override + { + activeClock->scheduleClockEventAt(time, event); + } + virtual void scheduleClockEventAfter(clocktime_t delay, ClockEvent *event) override + { + activeClock->scheduleClockEventAfter(delay, event); + } virtual ClockEvent *cancelClockEvent(ClockEvent *event) override { return activeClock->cancelClockEvent(event); } virtual void handleClockEvent(ClockEvent *event) override { activeClock->handleClockEvent(event); } - virtual void receiveSignal(cComponent *source, int signal, const simtime_t& time, cObject *details) override; + virtual void receiveSignal(cComponent *source, int signal, const simtime_t &time, cObject *details) override; + virtual void receiveSignal(cComponent *source, int signal, cObject *obj, cObject *details) override; + + public: + static simsignal_t activeClockIndexChangedSignal; }; } // namespace inet #endif - diff --git a/src/inet/clock/model/MultiClock.ned b/src/inet/clock/model/MultiClock.ned index 7b833707b65..7e1d92d923c 100644 --- a/src/inet/clock/model/MultiClock.ned +++ b/src/inet/clock/model/MultiClock.ned @@ -26,9 +26,14 @@ module MultiClock extends Module like IClock @display("i=block/timer"); @class(MultiClock); @signal[timeChanged](type=simtime_t); + @signal[timeJumped](); // Signal is emitted when the activeClock performs a timeJump or the activeClockIndex updates + @signal[timeDifferenceToReference](type=simtime_t); + @signal[activeClockIndexChanged](type=int); @statistic[timeChanged](title="Clock time"; source=localSignal(timeChanged); record=vector; interpolationmode=linear); + @statistic[timeDifferenceToReference](title="Time difference to reference"; source=localSignal(timeDifferenceToReference); record=vector; interpolationmode=linear); + @statistic[activeClockIndexChanged](title="Active clock index"; source=activeClockIndexChanged; record=vector; interpolationmode=sample-hold); submodules: - clock[numClocks]: like IClock { + clock[numClocks]: like IClock { @display("p=200,200,row,200"); } } diff --git a/src/inet/clock/model/OscillatorBasedClock.cc b/src/inet/clock/model/OscillatorBasedClock.cc index ac9d8cc8fb0..a4ad753036b 100644 --- a/src/inet/clock/model/OscillatorBasedClock.cc +++ b/src/inet/clock/model/OscillatorBasedClock.cc @@ -65,13 +65,24 @@ void OscillatorBasedClock::initialize(int stage) WATCH_PTRVECTOR(events); } else if (stage == INITSTAGE_CLOCK) { - originSimulationTime = simTime(); - originClockTime = par("initialClockTime"); + setNewOriginTime(simTime(), par("initialClockTime")); if (originClockTime.raw() % oscillator->getNominalTickLength().raw() != 0) throw cRuntimeError("Initial clock time must be a multiple of the oscillator nominal tick length"); } } +void OscillatorBasedClock::setNewOriginTime(const simtime_t &newOriginSimulationTime, + const clocktime_t &newOriginClockTime) +{ + if (clockEventTime != -1) { + auto currentClockTime = getClockTime(); + auto diff = newOriginClockTime - currentClockTime; + clockEventTime += diff; + } + originSimulationTime = newOriginSimulationTime; + originClockTime = newOriginClockTime; +} + clocktime_t OscillatorBasedClock::computeClockTimeFromSimTime(simtime_t t) const { ASSERT(t >= simTime()); @@ -133,8 +144,7 @@ void OscillatorBasedClock::receiveSignal(cComponent *source, int signal, cObject if (signal == IOscillator::preOscillatorStateChangedSignal) { // NOTE: the origin clock must be set first - originClockTime = getClockTime(); - originSimulationTime = simTime(); + setNewOriginTime(simTime(), getClockTime()); } else if (signal == IOscillator::postOscillatorStateChangedSignal) { simtime_t currentSimTime = simTime(); diff --git a/src/inet/clock/model/OscillatorBasedClock.h b/src/inet/clock/model/OscillatorBasedClock.h index c1c2a42ae94..5d1eeee8446 100644 --- a/src/inet/clock/model/OscillatorBasedClock.h +++ b/src/inet/clock/model/OscillatorBasedClock.h @@ -4,7 +4,6 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // - #ifndef __INET_OSCILLATORBASEDCLOCK_H #define __INET_OSCILLATORBASEDCLOCK_H @@ -16,7 +15,7 @@ namespace inet { using namespace units::values; -class INET_API OscillatorBasedClock : public ClockBase, public cListener +class INET_API OscillatorBasedClock : public ClockBase { protected: IOscillator *oscillator = nullptr; @@ -29,6 +28,7 @@ class INET_API OscillatorBasedClock : public ClockBase, public cListener protected: virtual void initialize(int stage) override; + void setNewOriginTime(const simtime_t &newOriginSimulationTime, const clocktime_t &newOriginClockTime); public: virtual ~OscillatorBasedClock(); @@ -52,4 +52,3 @@ class INET_API OscillatorBasedClock : public ClockBase, public cListener } // namespace inet #endif - diff --git a/src/inet/clock/model/SettableClock.cc b/src/inet/clock/model/SettableClock.cc deleted file mode 100644 index 92738429731..00000000000 --- a/src/inet/clock/model/SettableClock.cc +++ /dev/null @@ -1,111 +0,0 @@ -// -// Copyright (C) 2020 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -#include "inet/clock/model/SettableClock.h" - -#include "inet/clock/oscillator/ConstantDriftOscillator.h" -#include "inet/common/XMLUtils.h" - -namespace inet { - -Define_Module(SettableClock); - -void SettableClock::initialize(int stage) -{ - OscillatorBasedClock::initialize(stage); - if (stage == INITSTAGE_LOCAL) { - const char *text = par("defaultOverdueClockEventHandlingMode"); - if (!strcmp(text, "execute")) - defaultOverdueClockEventHandlingMode = EXECUTE; - else if (!strcmp(text, "skip")) - defaultOverdueClockEventHandlingMode = SKIP; - else if (!strcmp(text, "error")) - defaultOverdueClockEventHandlingMode = ERROR; - else - throw cRuntimeError("Unknown defaultOverdueClockEventHandlingMode parameter value"); - } -} - -OverdueClockEventHandlingMode SettableClock::getOverdueClockEventHandlingMode(ClockEvent *event) const -{ - auto mode = event->getOverdueClockEventHandlingMode(); - if (mode == UNSPECIFIED) - return defaultOverdueClockEventHandlingMode; - else - return mode; -} - -simtime_t SettableClock::handleOverdueClockEvent(ClockEvent *event, simtime_t t) -{ - switch (getOverdueClockEventHandlingMode(event)) { - case EXECUTE: - EV_WARN << "Scheduling overdue clock event " << event->getName() << " to current simulation time.\n"; - return t; - case SKIP: - EV_WARN << "Skipping overdue clock event " << event->getName() << ".\n"; - cancelClockEvent(event); - return -1; - case ERROR: - throw cRuntimeError("Clock event is overdue"); - default: - throw cRuntimeError("Unknown overdue clock event handling mode"); - } -} - -void SettableClock::setClockTime(clocktime_t newClockTime, ppm oscillatorCompensation, bool resetOscillator) -{ - Enter_Method("setClockTime"); - clocktime_t oldClockTime = getClockTime(); - if (newClockTime != oldClockTime) { - emit(timeChangedSignal, oldClockTime.asSimTime()); - if (resetOscillator) { - if (auto constantDriftOscillator = dynamic_cast(oscillator)) - constantDriftOscillator->setTickOffset(0); - } - simtime_t currentSimTime = simTime(); - EV_DEBUG << "Setting clock time from " << oldClockTime << " to " << newClockTime << " at simtime " << currentSimTime << ".\n"; - originSimulationTime = simTime(); - originClockTime = newClockTime; - this->oscillatorCompensation = oscillatorCompensation; - ASSERT(newClockTime == getClockTime()); - clocktime_t clockDelta = newClockTime - oldClockTime; - for (auto event : events) { - if (event->getRelative()) - // NOTE: the simulation time of event execution is not affected - event->setArrivalClockTime(event->getArrivalClockTime() + clockDelta); - else { - clocktime_t arrivalClockTime = event->getArrivalClockTime(); - bool isOverdue = arrivalClockTime < newClockTime; - simtime_t arrivalSimTime = isOverdue ? -1 : computeSimTimeFromClockTime(arrivalClockTime); - if (isOverdue || arrivalSimTime < currentSimTime) - arrivalSimTime = handleOverdueClockEvent(event, currentSimTime); - if (event->isScheduled()) { - cSimpleModule *targetModule = check_and_cast(event->getArrivalModule()); - cContextSwitcher contextSwitcher(targetModule); - targetModule->rescheduleAt(arrivalSimTime, event); - } - } - } - emit(timeChangedSignal, newClockTime.asSimTime()); - } -} - -void SettableClock::processCommand(const cXMLElement& node) -{ - Enter_Method("processCommand"); - if (!strcmp(node.getTagName(), "set-clock")) { - clocktime_t time = ClockTime::parse(xmlutils::getMandatoryFilledAttribute(node, "time")); - ppm oscillatorCompensation = ppm(xmlutils::getAttributeDoubleValue(&node, "oscillator-compensation", 0)); - bool resetOscillator = xmlutils::getAttributeBoolValue(&node, "reset-oscillator", true); - setClockTime(time, oscillatorCompensation, resetOscillator); - } - else - throw cRuntimeError("Invalid command: %s", node.getTagName()); -} - -} // namespace inet - diff --git a/src/inet/clock/model/SettableClock.h b/src/inet/clock/model/SettableClock.h deleted file mode 100644 index ee407263321..00000000000 --- a/src/inet/clock/model/SettableClock.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright (C) 2020 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -#ifndef __INET_SETTABLECLOCK_H -#define __INET_SETTABLECLOCK_H - -#include "inet/clock/model/OscillatorBasedClock.h" -#include "inet/common/scenario/IScriptable.h" - -namespace inet { - -class INET_API SettableClock : public OscillatorBasedClock, public IScriptable -{ - protected: - OverdueClockEventHandlingMode defaultOverdueClockEventHandlingMode = UNSPECIFIED; - ppm oscillatorCompensation = ppm(0); // 0 means no compensation, higher value means faster clock, e.g. 100 ppm value means the clock compensates 100 microseconds for every second in clock time - // 100 ppm value means the oscillator tick length is compensated to be smaller by a factor of (1 / (1 + 100 / 1E+6)) than the actual tick length measured in clock time - - protected: - virtual void initialize(int stage) override; - - virtual OverdueClockEventHandlingMode getOverdueClockEventHandlingMode(ClockEvent *event) const; - virtual simtime_t handleOverdueClockEvent(ClockEvent *event, simtime_t t); - - // IScriptable implementation - virtual void processCommand(const cXMLElement& node) override; - - public: - virtual ppm getOscillatorCompensation() const override { return oscillatorCompensation; } - - /** - * Sets the clock time immediately to the given value. Greater than 1 oscillator - * compensation factor means the clock measures time faster. - */ - virtual void setClockTime(clocktime_t time, ppm oscillatorCompensation, bool resetOscillator); -}; - -} // namespace inet - -#endif - diff --git a/src/inet/clock/model/SettableClock.ned b/src/inet/clock/model/SettableClock.ned deleted file mode 100644 index 156e4a5e9b3..00000000000 --- a/src/inet/clock/model/SettableClock.ned +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (C) 2020 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -package inet.clock.model; - -// -// Models a clock which can be set to a different clock time. The clock time -// can be set from C++ or using a command -// in a `ScenarioManager` script. -// -// @see ~ScenarioManager -// -module SettableClock extends OscillatorBasedClock -{ - parameters: - string defaultOverdueClockEventHandlingMode @enum("execute","skip","error") = default("execute"); - @class(SettableClock); -} - diff --git a/src/inet/clock/servo/InstantServoClock.cc b/src/inet/clock/servo/InstantServoClock.cc new file mode 100644 index 00000000000..78f0336ecc4 --- /dev/null +++ b/src/inet/clock/servo/InstantServoClock.cc @@ -0,0 +1,65 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "InstantServoClock.h" + +namespace inet { + +Define_Module(InstantServoClock); + +void InstantServoClock::initialize(int stage) +{ + ServoClockBase::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + adjustClock = par("adjustClock"); + adjustDrift = par("adjustDrift"); + if (!adjustClock && adjustDrift) { + throw cRuntimeError("Cannot adjust drift without adjusting clock"); + } + offsetPrev = -1; + localPrev = -1; + clockState = INIT; + } +} + +void InstantServoClock::resetClockState() +{ + clockState = INIT; + offsetPrev = -1; +} + +void InstantServoClock::adjustClockTo(clocktime_t newClockTime) +{ + Enter_Method("adjustClockTo"); + + clocktime_t oldClockTime = getClockTime(); + + if (newClockTime != oldClockTime && adjustClock) { + // At every clock jump we increase the clock time by offset + // For our drift estimation, we need to know to keep track of the local times without + // offsets and the offsets themselves + // This we subtract the offset from the local time to get the local time without the offset + // and accumulate the offsets + auto local = oldClockTime.inUnit(SIMTIME_NS) - offsetPrev; + auto offset = (newClockTime - oldClockTime).inUnit(SIMTIME_NS) + offsetPrev; + + jumpClockTo(newClockTime); + + if (adjustDrift && offsetPrev != -1) { + drift += ppm(1e6 * (offsetPrev - offset) / (localPrev - local)); + EV_INFO << "Drift: " << drift << "\n"; + setOscillatorCompensation(drift); + clockState = SYNCED; + } + + offsetPrev = offset; + localPrev = local; + } +} +// TODO: Add a mechanism that estimates the drift rate based on the previous and current local and received +// timestamps, similar to case 0 and 1 in PiServoClock + +} // namespace inet diff --git a/src/inet/clock/servo/InstantServoClock.h b/src/inet/clock/servo/InstantServoClock.h new file mode 100644 index 00000000000..a643dcb36c4 --- /dev/null +++ b/src/inet/clock/servo/InstantServoClock.h @@ -0,0 +1,31 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_INSTANTSERVOCLOCK_H +#define __INET_INSTANTSERVOCLOCK_H + +#include "ServoClockBase.h" + +namespace inet { + +class INET_API InstantServoClock : public ServoClockBase +{ + protected: + long offsetPrev = 0; + long localPrev = 0; + ppm drift = ppm(0); + bool adjustClock = true; + bool adjustDrift = true; + + public: + virtual void adjustClockTo(clocktime_t newClockTime) override; + virtual void initialize(int stage) override; + virtual void resetClockState() override; +}; + +} // namespace inet + +#endif diff --git a/src/inet/clock/servo/InstantServoClock.ned b/src/inet/clock/servo/InstantServoClock.ned new file mode 100644 index 00000000000..1282f2e2831 --- /dev/null +++ b/src/inet/clock/servo/InstantServoClock.ned @@ -0,0 +1,24 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.clock.servo; + +// As simple clock servo (previously known as SettableClock in INET), with three different modes: +// +// 1. No servo: Calls to adjustTime are ignored. (adjustClock = false; adjustDrift = false) +// +// 2. Jump only: Calls to adjustTime are used to jump the clock. (adjustClock = true; adjustDrift = false) +// +// 3. Jump and estimate drift: Calls to adjustTime are used to jump the clock and estimate the drift using the previous +// call to adjustTime. (adjustClock = true; adjustDrift = true) +module InstantServoClock extends ServoClockBase +{ + parameters: + bool adjustClock = default(true); // Whether to adjust the clock. + bool adjustDrift = default(true); // Whether to adjust the drift. + @class(InstantServoClock); +} diff --git a/src/inet/clock/servo/PiServoClock.cc b/src/inet/clock/servo/PiServoClock.cc new file mode 100644 index 00000000000..b95768bc28a --- /dev/null +++ b/src/inet/clock/servo/PiServoClock.cc @@ -0,0 +1,111 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "PiServoClock.h" + +#include + +#include "inet/clock/oscillator/ConstantDriftOscillator.h" +#include "inet/common/XMLUtils.h" + +namespace inet { + +Define_Module(PiServoClock); + +simsignal_t PiServoClock::kpSignal = cComponent::registerSignal("kp"); +simsignal_t PiServoClock::driftSignal = cComponent::registerSignal("drift"); + +void PiServoClock::initialize(int stage) +{ + ServoClockBase::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + offsetThreshold = &par("offsetThreshold"); + kp = par("kp"); + ki = par("ki"); + } +} + +void PiServoClock::adjustClockTo(clocktime_t newClockTime) +{ + Enter_Method("adjustClockTo"); + clocktime_t oldClockTime = getClockTime(); + + if (newClockTime != oldClockTime) { + emit(timeChangedSignal, oldClockTime.asSimTime()); + clocktime_t offsetNow = newClockTime - oldClockTime; + + int64_t offsetNsPrev, offsetNs, localNsPrev, localNs; + auto offsetUs = 1e-3 * offsetNow.inUnit(SIMTIME_NS); + + auto offsetThresholdUs = offsetThreshold->doubleValueInUnit("us"); + if (phase == 2 && (offsetThresholdUs != 0 && fabs(offsetUs) >= offsetThresholdUs)) { + EV_INFO << "Offset is too large, resetting phase\n"; + EV_INFO << "Offset: " << offsetUs << " maxOffset: " << offsetThresholdUs << "\n"; + phase = 0; + clockState = INIT; + } + + switch (phase) { + case 0: + // Store the offset and the local time + offset[0] = newClockTime - oldClockTime; + local[0] = oldClockTime; + // Do not update frequency or anything to estimate first frequency in next step + phase = 1; + break; + case 1: + offset[1] = newClockTime - oldClockTime; + local[1] = oldClockTime; + + offsetNsPrev = offset[0].inUnit(SIMTIME_NS); + offsetNs = offset[1].inUnit(SIMTIME_NS); + + localNsPrev = local[0].inUnit(SIMTIME_NS); + localNs = local[1].inUnit(SIMTIME_NS); + + drift += ppm(1e6 * (offsetNsPrev - offsetNs) / (localNsPrev - localNs)); + EV_INFO << "Drift: " << drift << "\n"; + + jumpClockTo(newClockTime); + + setOscillatorCompensation(drift); + + clockState = SYNCED; + phase = 2; + break; + case 2: + // offsetNanosecond_prev = offset[0].inUnit(SIMTIME_NS); + + // differenceOffsetNanosecond = offsetNanosecond - offsetNanosecond_prev; + + setNewOriginTime(simTime(), oldClockTime); + + // As our timestamps is in nanoseconds, to get ppm we need to multiply by 1e-3 + kpTerm = ppm(kp * offsetUs); + kiTerm = ppm(ki * offsetUs); + + EV_INFO << "kpTerm: " << kpTerm << " kiTerm: " << kiTerm << " offsetUs: " << offsetUs << " drift: " << drift + << "\n"; + + setOscillatorCompensation(kpTerm + kiTerm + drift); + + drift += kiTerm; + + emit(kpSignal, kpTerm.get()); + break; + } + } + + emit(driftSignal, drift.get()); +} + +void PiServoClock::resetClockState() +{ + ServoClockBase::resetClockState(); + phase = 0; +} + +} // namespace inet diff --git a/src/inet/clock/servo/PiServoClock.h b/src/inet/clock/servo/PiServoClock.h new file mode 100644 index 00000000000..232f9ac27ae --- /dev/null +++ b/src/inet/clock/servo/PiServoClock.h @@ -0,0 +1,44 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_PISERVOCLOCK_H +#define __INET_PISERVOCLOCK_H + +#include "ServoClockBase.h" + +namespace inet { + +class INET_API PiServoClock : public ServoClockBase +{ + protected: + static simsignal_t driftSignal; + static simsignal_t kpSignal; + + int phase = 0; + clocktime_t offset[2]; + clocktime_t local[2]; + + cPar *offsetThreshold; + + double kp; // proportional gain + double ki; // integral gain + + ppm kpTerm = ppm(0); + ppm kiTerm = ppm(0); + + ppm drift = ppm(0); + + protected: + virtual void initialize(int stage) override; + + public: + virtual void adjustClockTo(clocktime_t newClockTime) override; + virtual void resetClockState() override; +}; + +} // namespace inet + +#endif diff --git a/src/inet/clock/servo/PiServoClock.ned b/src/inet/clock/servo/PiServoClock.ned new file mode 100644 index 00000000000..63fa3ef96f1 --- /dev/null +++ b/src/inet/clock/servo/PiServoClock.ned @@ -0,0 +1,33 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.clock.servo; + +// A PI-based servo clock with adjustable proportional (kp) and integral (ki) gains. +// It synchronizes the clock with an external time source by estimating and compensating drift. +// The clock operates in three phases: +// +// 1. Initialize: Collects initial offset data without adjustments. +// +// 2. Estimate Drift: Computes drift based on the difference between consecutive offsets. +// +// 3. Synchronize: Adjusts the clock using proportional and integral terms to minimize offset. +// +// Note: The kp and ki parameters need to be adjusted, if the the syncInterval is adjusted from the default value. +module PiServoClock extends ServoClockBase +{ + parameters: + double offsetThreshold @unit(s) = default(0s); // Clock will perform a time jump if the offset is greater than this value, 0.0 disables this feature + double kp = default(8); // The proportional parameter of the PI controller + double ki = default(4); // The integral paramater of the PI Controller + @signal[kp](type=double); + @statistic[kp](title="kp value"; record=vector; interpolationmode=linear); + @signal[drift](type=double); + @statistic[drift](title="drift value"; record=vector; interpolationmode=linear); + @class(PiServoClock); +} + diff --git a/src/inet/clock/servo/ServoClockBase.cc b/src/inet/clock/servo/ServoClockBase.cc new file mode 100644 index 00000000000..4b56b20fdf3 --- /dev/null +++ b/src/inet/clock/servo/ServoClockBase.cc @@ -0,0 +1,145 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "ServoClockBase.h" + +#include + +namespace inet { + +Register_Abstract_Class(ServoClockBase); + +void ServoClockBase::initialize(int stage) +{ + OscillatorBasedClock::initialize(stage); + if (stage == INITSTAGE_LOCAL) { + minOscillatorCompensation = ppm(par("minOscillatorCompensation")); + maxOscillatorCompensation = ppm(par("maxOscillatorCompensation")); + + const char *text = par("defaultOverdueClockEventHandlingMode"); + if (!strcmp(text, "execute")) + defaultOverdueClockEventHandlingMode = EXECUTE; + else if (!strcmp(text, "skip")) + defaultOverdueClockEventHandlingMode = SKIP; + else if (!strcmp(text, "error")) + defaultOverdueClockEventHandlingMode = ERROR; + else + throw cRuntimeError("Unknown defaultOverdueClockEventHandlingMode parameter value"); + } +} + +void ServoClockBase::setOscillatorCompensation(ppm oscillatorCompensationValue) +{ + oscillatorCompensationValue = std::max(minOscillatorCompensation, std::min(maxOscillatorCompensation, oscillatorCompensationValue)); + this->oscillatorCompensation = oscillatorCompensationValue; +} + +void ServoClockBase::resetOscillator() const +{ + Enter_Method("resetOscillator"); + if (auto constantDriftOscillator = dynamic_cast(oscillator)) + constantDriftOscillator->setTickOffset(0); +} +void ServoClockBase::resetClockState() +{ + clockState = INIT; + EV_INFO << "Resetting clock state" << endl; +} + +void ServoClockBase::rescheduleClockEvents(clocktime_t oldClockTime, clocktime_t newClockTime) +{ + clocktime_t clockDelta = newClockTime - oldClockTime; + simtime_t currentSimTime = simTime(); + for (auto event : this->events) { + if (event->getRelative()) + // NOTE: the simulation time of event execution is not affected + event->setArrivalClockTime(event->getArrivalClockTime() + clockDelta); + else { + clocktime_t arrivalClockTime = event->getArrivalClockTime(); + bool isOverdue = arrivalClockTime < newClockTime; + simtime_t arrivalSimTime = isOverdue ? -1 : computeSimTimeFromClockTime(arrivalClockTime); + if (isOverdue || arrivalSimTime < currentSimTime) + arrivalSimTime = handleOverdueClockEvent(event, currentSimTime); + if (event->isScheduled()) { + auto *targetModule = check_and_cast(event->getArrivalModule()); + cContextSwitcher contextSwitcher(targetModule); + targetModule->rescheduleAt(arrivalSimTime, event); + } + } + } +} + +simtime_t ServoClockBase::handleOverdueClockEvent(ClockEvent *event, simtime_t t) +{ + auto mode = event->getOverdueClockEventHandlingMode(); + if (mode == UNSPECIFIED) + mode = defaultOverdueClockEventHandlingMode; + + switch (mode) { + case EXECUTE: + EV_WARN << "Scheduling overdue clock event " << event->getName() << " to current simulation time.\n"; + return t; + case SKIP: + EV_WARN << "Skipping overdue clock event " << event->getName() << ".\n"; + cancelClockEvent(event); + return -1; + case ERROR: + throw cRuntimeError("Clock event is overdue"); + default: + throw cRuntimeError("Unknown overdue clock event handling mode"); + } +} + +void ServoClockBase::processCommand(const cXMLElement &node) +{ + Enter_Method("processCommand"); + if (!strcmp(node.getTagName(), "adjust-clock")) { + clocktime_t time = ClockTime::parse(xmlutils::getMandatoryFilledAttribute(node, "time")); + adjustClockTo(time); + } + else if (!strcmp(node.getTagName(), "set-clock")) { + clocktime_t time = ClockTime::parse(xmlutils::getMandatoryFilledAttribute(node, "time")); + bool notifyListeners = xmlutils::getAttributeBoolValue(&node, "notifyListeners", true); + jumpClockTo(time, notifyListeners); + } + else if (!strcmp(node.getTagName(), "set-oscillator-compensation")) { + // TODO: Refactor to directly read ppm + const char *valueStr = xmlutils::getMandatoryFilledAttribute(node, "value"); + double valueDouble = std::atof(valueStr); // Convert string to double + ppm oscillatorCompensationValue = ppm(valueDouble); // Create ppm object from double + setOscillatorCompensation(oscillatorCompensationValue); + } + else if (!strcmp(node.getTagName(), "reset-oscillator")) { + resetOscillator(); + } + else + throw cRuntimeError("Invalid command: %s", node.getTagName()); +} + +void ServoClockBase::jumpClockTo(clocktime_t newClockTime, bool notifyListeners) +{ + auto oldClockTime = getClockTime(); + + if (newClockTime != oldClockTime) { + emit(timeChangedSignal, oldClockTime.asSimTime()); + simtime_t currentSimTime = simTime(); + EV_DEBUG << "Setting clock time from " << oldClockTime << " to " << newClockTime << " at simtime " + << currentSimTime << ".\n"; + setNewOriginTime(simTime(), newClockTime); + + ASSERT(newClockTime == getClockTime()); + rescheduleClockEvents(oldClockTime, newClockTime); + ClockJumpDetails timeJumpDetails = ClockJumpDetails(); + timeJumpDetails.oldClockTime = oldClockTime; + timeJumpDetails.newClockTime = newClockTime; + if (notifyListeners) { + emit(timeJumpedSignal, this, &timeJumpDetails); + } + emit(timeChangedSignal, newClockTime.asSimTime()); + } +} + +} // namespace inet diff --git a/src/inet/clock/servo/ServoClockBase.h b/src/inet/clock/servo/ServoClockBase.h new file mode 100644 index 00000000000..2c24d7a6cac --- /dev/null +++ b/src/inet/clock/servo/ServoClockBase.h @@ -0,0 +1,52 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_SERVOCLOCKBASE_H +#define __INET_SERVOCLOCKBASE_H + +#include "inet/clock/model/OscillatorBasedClock.h" +#include "inet/clock/oscillator/ConstantDriftOscillator.h" +#include "inet/common/scenario/IScriptable.h" + +namespace inet { + +class ServoClockBase : public OscillatorBasedClock, public IScriptable +{ + public: + enum ClockState { INIT, SYNCED }; + + protected: + OverdueClockEventHandlingMode defaultOverdueClockEventHandlingMode = UNSPECIFIED; + ppm oscillatorCompensation = ppm(0); // 0 means no compensation, higher value means faster clock, e.g. 100 ppm value + // means the clock compensates 100 microseconds for every second in clock time + // 100 ppm value means the oscillator tick length is compensated to be smaller by a factor of (1 / (1 + 100 / 1E+6)) + // than the actual tick length measured in clock time + + ppm minOscillatorCompensation = ppm(-std::numeric_limits::infinity()); + ppm maxOscillatorCompensation = ppm(std::numeric_limits::infinity()); + + ClockState clockState = INIT; + + protected: + virtual void rescheduleClockEvents(clocktime_t oldClockTime, clocktime_t newClockTime); + virtual simtime_t handleOverdueClockEvent(ClockEvent *event, simtime_t t); + virtual void initialize(int stage) override; + virtual void processCommand(const cXMLElement &node) override; + + public: + virtual ppm getOscillatorCompensation() const override { return oscillatorCompensation; } + + virtual void adjustClockTo(clocktime_t newClockTime) = 0; + virtual void jumpClockTo(clocktime_t newClockTime, bool notifyListeners = true); + virtual void setOscillatorCompensation(ppm oscillatorCompensationValue); + virtual void resetOscillator() const; + virtual void resetClockState(); + virtual ClockState getClockState() const { return clockState; } +}; + +} /* namespace inet */ + +#endif diff --git a/src/inet/clock/servo/ServoClockBase.ned b/src/inet/clock/servo/ServoClockBase.ned new file mode 100644 index 00000000000..215c8729a7c --- /dev/null +++ b/src/inet/clock/servo/ServoClockBase.ned @@ -0,0 +1,23 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.clock.servo; + +import inet.clock.model.OscillatorBasedClock; + +// A base module for servo clocks that extends the OscillatorBasedClock. +// This module serves as a foundation for implementing servo clock mechanisms, +// providing a structured approach for handling clock adjustments and overdue events. +module ServoClockBase extends OscillatorBasedClock +{ + parameters: + string defaultOverdueClockEventHandlingMode @enum("execute","skip","error") = default("error"); + double minOscillatorCompensation @unit(ppm) = default(-inf ppm); // Expressed as a ratio in parts per million + double maxOscillatorCompensation @unit(ppm) = default(inf ppm); // Expressed as a ratio in parts per million + @class(ServoClockBase); +} + diff --git a/src/inet/common/clock/ClockUserModuleBase.h b/src/inet/common/clock/ClockUserModuleBase.h index dae11db377f..8459ffbd2f5 100644 --- a/src/inet/common/clock/ClockUserModuleBase.h +++ b/src/inet/common/clock/ClockUserModuleBase.h @@ -9,6 +9,7 @@ #define __INET_CLOCKUSERMODULEBASE_H #include "inet/common/clock/ClockUserModuleMixin.h" +#include "inet/common/SimpleModule.h" namespace inet { diff --git a/src/inet/linklayer/ieee8021as/Gptp.cc b/src/inet/linklayer/ieee8021as/Gptp.cc index 92a8cd1b3da..7e65afe8951 100644 --- a/src/inet/linklayer/ieee8021as/Gptp.cc +++ b/src/inet/linklayer/ieee8021as/Gptp.cc @@ -4,21 +4,24 @@ // Peter Danielis // University of Rostock, Germany // +// Lucas Haug +// Lun-Yu Yuan +// Chunzhi Guo +// University of Stuttgart, Deterministic6G -#include "inet/linklayer/ieee8021as/Gptp.h" +#include "Gptp.h" -#include "inet/clock/model/SettableClock.h" -#include "inet/common/clock/ClockUserModuleBase.h" +#include "GptpPacket_m.h" +#include "inet/clock/servo/ServoClockBase.h" #include "inet/common/IProtocolRegistrationListener.h" +#include "inet/common/clock/ClockUserModuleBase.h" #include "inet/common/packet/dissector/PacketDissector.h" #include "inet/linklayer/common/InterfaceTag_m.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/common/MacAddressTag_m.h" #include "inet/linklayer/ethernet/common/Ethernet.h" #include "inet/linklayer/ethernet/common/EthernetMacHeader_m.h" -#include "inet/linklayer/ieee8021as/GptpPacket_m.h" #include "inet/networklayer/common/NetworkInterface.h" -#include "inet/physicallayer/wired/ethernet/EthernetPhyHeader_m.h" namespace inet { @@ -26,23 +29,45 @@ Define_Module(Gptp); simsignal_t Gptp::localTimeSignal = cComponent::registerSignal("localTime"); simsignal_t Gptp::timeDifferenceSignal = cComponent::registerSignal("timeDifference"); -simsignal_t Gptp::rateRatioSignal = cComponent::registerSignal("rateRatio"); +simsignal_t Gptp::gmRateRatioSignal = cComponent::registerSignal("gmRateRatio"); +simsignal_t Gptp::receivedRateRatioSignal = cComponent::registerSignal("receivedRateRatio"); +simsignal_t Gptp::neighborRateRatioSignal = cComponent::registerSignal("neighborRateRatio"); simsignal_t Gptp::peerDelaySignal = cComponent::registerSignal("peerDelay"); +simsignal_t Gptp::residenceTimeSignal = cComponent::registerSignal("residenceTime"); +simsignal_t Gptp::correctionFieldIngressSignal = cComponent::registerSignal("correctionFieldIngress"); +simsignal_t Gptp::correctionFieldEgressSignal = cComponent::registerSignal("correctionFieldEgress"); +simsignal_t Gptp::gptpSyncStateChanged = cComponent::registerSignal("gptpSyncStateChanged"); +simsignal_t Gptp::gmIdSignal = cComponent::registerSignal("gmId"); // MAC address: -// 01-80-C2-00-00-02 for TimeSync (ieee 802.1as-2020, 13.3.1.2) -// 01-80-C2-00-00-0E for Announce and Signaling messages, for Sync, Follow_Up, Pdelay_Req, Pdelay_Resp, and Pdelay_Resp_Follow_Up messages +// 01-80-C2-00-00-0E for Announce and Signaling messages, for Sync, Follow_Up, +// Pdelay_Req, Pdelay_Resp, and Pdelay_Resp_Follow_Up messages (ieee 802.1as-2020, 10.5.3 and 11.3.4) const MacAddress Gptp::GPTP_MULTICAST_ADDRESS("01:80:C2:00:00:0E"); +std::map inet::Gptp::clockIdentityToFullPath; // EtherType: // 0x8809 for TimeSync (ieee 802.1as-2020, 13.3.1.2) -// 0x88F7 for Announce and Signaling messages, for Sync, Follow_Up, Pdelay_Req, Pdelay_Resp, and Pdelay_Resp_Follow_Up messages +// 0x88F7 for Announce and Signaling messages, for Sync, Follow_Up, +// Pdelay_Req, Pdelay_Resp, and Pdelay_Resp_Follow_Up messages Gptp::~Gptp() { - cancelAndDeleteClockEvent(selfMsgDelayReq); - cancelAndDeleteClockEvent(selfMsgSync); - cancelAndDeleteClockEvent(requestMsg); + if (selfMsgDelayReq) + cancelAndDeleteClockEvent(selfMsgDelayReq); + if (selfMsgSync) + cancelAndDeleteClockEvent(selfMsgSync); + if (selfMsgAnnounce) + cancelAndDeleteClockEvent(selfMsgAnnounce); + for (auto &reqAnswerEvent : reqAnswerEvents) + cancelAndDeleteClockEvent(reqAnswerEvent); + for (auto &announce : receivedAnnounces) + delete announce.second; + for (auto &announceTimeout : announceTimeouts) + cancelAndDeleteClockEvent(announceTimeout.second); + if (selfMsgSyncTimeout) + cancelAndDeleteClockEvent(selfMsgSyncTimeout); + + delete bestAnnounce; } void Gptp::initialize(int stage) @@ -51,113 +76,212 @@ void Gptp::initialize(int stage) if (stage == INITSTAGE_LOCAL) { gptpNodeType = static_cast(cEnum::get("GptpNodeType", "inet")->resolve(par("gptpNodeType"))); + useNrr = par("useNrr"); + gmRateRatioCalculationMethod = static_cast( + cEnum::get("GmRateRatioCalculationMethod", "inet")->resolve(par("gmRateRatioCalculationMethod"))); + if (!useNrr && gmRateRatioCalculationMethod == GmRateRatioCalculationMethod::NRR) { + throw cRuntimeError( + "Parameter inconsistency: useNrr=false and gmRateRatioCalculationMethod=NEIGHBOR_RATE_RATIO"); + } + domainNumber = par("domainNumber"); syncInterval = par("syncInterval"); pDelayReqProcessingTime = par("pDelayReqProcessingTime"); std::hash strHash; clockIdentity = strHash(getFullPath()); + clockIdentityToFullPath[std::to_string(clockIdentity)] = getFullPath(); + + if (gptpNodeType == BMCA_NODE) { + initBmca(); + } + sendAnnounceImmediately = par("sendAnnounceImmediately"); } if (stage == INITSTAGE_LINK_LAYER) { - peerDelay = 0; - receivedTimeSync = CLOCKTIME_ZERO; + meanLinkDelay = 0; + syncIngressTimestampLast = -1; + correctionField = par("correctionField"); + gmRateRatio = 1.0; + registerProtocol(Protocol::gptp, gate("socketOut"), gate("socketIn")); interfaceTable.reference(this, "interfaceTableModule", true); - const char *str = par("slavePort"); - if (*str) { - if (gptpNodeType == MASTER_NODE) - throw cRuntimeError("Parameter inconsistency: MASTER_NODE with slave port"); - auto nic = CHK(interfaceTable->findInterfaceByName(str)); - slavePortId = nic->getInterfaceId(); - nic->subscribe(transmissionEndedSignal, this); - nic->subscribe(receptionEndedSignal, this); - } - else - if (gptpNodeType != MASTER_NODE) - throw cRuntimeError("Parameter error: Missing slave port for %s", par("gptpNodeType").stringValue()); - - auto v = check_and_cast(par("masterPorts").objectValue())->asStringVector(); - if (v.empty() and gptpNodeType != SLAVE_NODE) - throw cRuntimeError("Parameter error: Missing any master port for %s", par("gptpNodeType").stringValue()); - for (const auto& p : v) { - auto nic = CHK(interfaceTable->findInterfaceByName(p.c_str())); - int portId = nic->getInterfaceId(); - if (portId == slavePortId) - throw cRuntimeError("Parameter error: the port '%s' specified both master and slave port", p.c_str()); - masterPortIds.insert(portId); - nic->subscribe(transmissionEndedSignal, this); - nic->subscribe(receptionEndedSignal, this); - } - - if (slavePortId != -1) { - auto networkInterface = interfaceTable->getInterfaceById(slavePortId); - if (!networkInterface->matchesMulticastMacAddress(GPTP_MULTICAST_ADDRESS)) - networkInterface->addMulticastMacAddress(GPTP_MULTICAST_ADDRESS); + initPorts(); + if (gptpNodeType != BMCA_NODE) { + scheduleMessageOnTopologyChange(); } - for (auto id: masterPortIds) { - auto networkInterface = interfaceTable->getInterfaceById(id); - if (!networkInterface->matchesMulticastMacAddress(GPTP_MULTICAST_ADDRESS)) - networkInterface->addMulticastMacAddress(GPTP_MULTICAST_ADDRESS); + else { + selfMsgAnnounce = new ClockEvent("selfMsgAnnounce", GPTP_SELF_MSG_ANNOUNCE); + announceInterval = par("announceInterval"); + scheduleClockEventAfter(par("announceInitialOffset"), selfMsgAnnounce); } - correctionField = par("correctionField"); + // We cast to cComponent, because clock can also be a MultiClock which does not extend from ClockBase, + // but also implements the clockJumpSignal + auto servoClock = check_and_cast(clock.get()); + servoClock->subscribe(ClockBase::timeJumpedSignal, this); - gmRateRatio = 1.0; + WATCH(meanLinkDelay); + } +} - registerProtocol(Protocol::gptp, gate("socketOut"), gate("socketIn")); +void Gptp::initPorts() +{ + const char *str = par("slavePort"); + if (*str) { + if (gptpNodeType == MASTER_NODE) + throw cRuntimeError("Parameter inconsistency: MASTER_NODE with slave port"); + if (gptpNodeType == BMCA_NODE) + throw cRuntimeError("Parameter inconsistency: BMCA_NODE with slave port"); + auto nic = CHK(interfaceTable->findInterfaceByName(str)); + slavePortId = nic->getInterfaceId(); + nic->subscribe(transmissionStartedSignal, this); + nic->subscribe(receptionStartedSignal, this); + nic->subscribe(receptionEndedSignal, this); + } + else if (gptpNodeType != MASTER_NODE && gptpNodeType != BMCA_NODE) + throw cRuntimeError("Parameter error: Missing slave port for %s", par("gptpNodeType").stringValue()); + + auto masterPortsVector = check_and_cast(par("masterPorts").objectValue())->asStringVector(); + auto bmcaPortsVector = check_and_cast(par("bmcaPorts").objectValue())->asStringVector(); + if (masterPortsVector.empty() && gptpNodeType != SLAVE_NODE && gptpNodeType != BMCA_NODE) + throw cRuntimeError("Parameter error: Missing any master port for %s", par("gptpNodeType").stringValue()); + if (!masterPortsVector.empty() && (gptpNodeType == BMCA_NODE || gptpNodeType == SLAVE_NODE)) + throw cRuntimeError("Parameter inconsistency: %s with master ports", par("gptpNodeType").stringValue()); + + if (bmcaPortsVector.empty() && gptpNodeType == BMCA_NODE) + throw cRuntimeError("Parameter error: Missing any bmca port for %s", par("gptpNodeType").stringValue()); + if (!bmcaPortsVector.empty() && gptpNodeType != BMCA_NODE) + throw cRuntimeError("Parameter inconsistency: %s with bmca ports", par("gptpNodeType").stringValue()); + + for (const auto &p : masterPortsVector) { + auto nic = CHK(interfaceTable->findInterfaceByName(p.c_str())); + int portId = nic->getInterfaceId(); + if (portId == slavePortId) + throw cRuntimeError("Parameter error: the port '%s' specified both " + "master and slave port", + p.c_str()); + masterPortIds.insert(portId); + nic->subscribe(transmissionStartedSignal, this); + nic->subscribe(receptionStartedSignal, this); + nic->subscribe(receptionEndedSignal, this); + } - /* Only grandmaster in the domain can initialize the synchronization message periodically - * so below condition checks whether it is grandmaster and then schedule first sync message */ - if(gptpNodeType == MASTER_NODE) - { - // Schedule Sync message to be sent - selfMsgSync = new ClockEvent("selfMsgSync", GPTP_SELF_MSG_SYNC); + for (const auto &p : bmcaPortsVector) { + auto nic = CHK(interfaceTable->findInterfaceByName(p.c_str())); + int portId = nic->getInterfaceId(); + bmcaPortIds.insert(portId); + nic->subscribe(transmissionStartedSignal, this); + nic->subscribe(receptionStartedSignal, this); + nic->subscribe(receptionEndedSignal, this); + } - clocktime_t scheduleSync = par("syncInitialOffset"); - originTimestamp = clock->getClockTime() + scheduleSync; - scheduleClockEventAfter(scheduleSync, selfMsgSync); - } - if(slavePortId != -1) - { - requestMsg = new ClockEvent("requestToSendSync", GPTP_REQUEST_TO_SEND_SYNC); + if (slavePortId != -1) { + auto networkInterface = interfaceTable->getInterfaceById(slavePortId); + if (!networkInterface->matchesMulticastMacAddress(GPTP_MULTICAST_ADDRESS)) + networkInterface->addMulticastMacAddress(GPTP_MULTICAST_ADDRESS); + } + for (auto id : masterPortIds) { + auto networkInterface = interfaceTable->getInterfaceById(id); + if (!networkInterface->matchesMulticastMacAddress(GPTP_MULTICAST_ADDRESS)) + networkInterface->addMulticastMacAddress(GPTP_MULTICAST_ADDRESS); + } + for (auto id : bmcaPortIds) { + auto networkInterface = interfaceTable->getInterfaceById(id); + if (!networkInterface->matchesMulticastMacAddress(GPTP_MULTICAST_ADDRESS)) + networkInterface->addMulticastMacAddress(GPTP_MULTICAST_ADDRESS); + } +} - // Schedule Pdelay_Req message is sent by slave port - // without depending on node type which is grandmaster or bridge - selfMsgDelayReq = new ClockEvent("selfMsgPdelay", GPTP_SELF_MSG_PDELAY_REQ); - pdelayInterval = par("pdelayInterval"); - scheduleClockEventAfter(par("pdelayInitialOffset"), selfMsgDelayReq); +void Gptp::scheduleMessageOnTopologyChange() +{ + if (gptpNodeType == BMCA_NODE) { + // Cancel old sync and pdelay timers first + if (selfMsgSync) { + cancelAndDeleteClockEvent(selfMsgSync); + selfMsgSync = nullptr; + } + if (selfMsgDelayReq) { + cancelAndDeleteClockEvent(selfMsgDelayReq); + selfMsgDelayReq = nullptr; } - WATCH(peerDelay); + if (selfMsgSyncTimeout) { + cancelAndDeleteClockEvent(selfMsgSyncTimeout); + selfMsgSyncTimeout = nullptr; + } + } + + /* Only grandmaster in the domain can initialize the synchronization message + * periodically so below condition checks whether it is grandmaster and then + * schedule first sync message */ + if (isGM()) { + // Schedule Sync message to be sent + selfMsgSync = new ClockEvent("selfMsgSync", GPTP_SELF_MSG_SYNC); + + clocktime_t scheduleSync = par("syncInitialOffset"); + preciseOriginTimestamp = clock->getClockTime() + scheduleSync; + scheduleClockEventAfter(scheduleSync, selfMsgSync); + changeSyncState(SYNCED); + } + else { + selfMsgSyncTimeout = new ClockEvent("selfMsgSyncTimeout", GPTP_SELF_MSG_SYNC_TIMEOUT); + scheduleClockEventAfter(par("syncTimeout"), selfMsgSyncTimeout); + } + if (slavePortId != -1) { + // Schedule Pdelay_Req message is sent by slave port + // without depending on node type which is grandmaster or bridge + selfMsgDelayReq = new ClockEvent("selfMsgPdelay", GPTP_SELF_MSG_PDELAY_REQ); + pdelayInterval = par("pdelayInterval"); + scheduleClockEventAfter(par("pdelayInitialOffset"), selfMsgDelayReq); } } -void Gptp::handleSelfMessage(cMessage *msg) +void Gptp::initBmca() { - switch(msg->getKind()) { - case GPTP_SELF_MSG_SYNC: - // masterport: - ASSERT(selfMsgSync == msg); - sendSync(); + localPriorityVector.grandmasterPriority1 = par("grandmasterPriority1"); + localPriorityVector.grandmasterClockQuality.clockClass = par("clockClass"); + localPriorityVector.grandmasterClockQuality.clockAccuracy = par("clockAccuracy"); + localPriorityVector.grandmasterClockQuality.offsetScaledLogVariance = par("offsetScaledLogVariance"); + localPriorityVector.grandmasterPriority2 = par("grandmasterPriority2"); + localPriorityVector.grandmasterIdentity = clockIdentity; + localPriorityVector.stepsRemoved = 0; +} - /* Schedule next Sync message at next sync interval - * Grand master always works at simulation time */ - scheduleClockEventAfter(syncInterval, selfMsgSync); - break; +void Gptp::handleSelfMessage(cMessage *msg) +{ + switch (msg->getKind()) { + case GPTP_SELF_MSG_SYNC: + // masterport: + ASSERT(selfMsgSync == msg); + sendSync(); + + /* Schedule next Sync message at next sync interval + * Grand master always works at simulation time */ + scheduleClockEventAfter(syncInterval, selfMsgSync); + break; - case GPTP_SELF_REQ_ANSWER_KIND: - // masterport: - sendPdelayResp(check_and_cast(msg)); - delete msg; - break; + case GPTP_SELF_REQ_ANSWER_KIND: + // masterport: + sendPdelayResp(check_and_cast(msg)); + delete msg; + break; - case GPTP_SELF_MSG_PDELAY_REQ: + case GPTP_SELF_MSG_PDELAY_REQ: // slaveport: - sendPdelayReq(); //TODO on slaveports only - scheduleClockEventAfter(pdelayInterval, selfMsgDelayReq); - break; - - default: - throw cRuntimeError("Unknown self message (%s)%s, kind=%d", msg->getClassName(), msg->getName(), msg->getKind()); + sendPdelayReq(); + scheduleClockEventAfter(pdelayInterval, selfMsgDelayReq); + break; + case GPTP_SELF_MSG_ANNOUNCE: + sendAnnounce(); + break; + case GPTP_SELF_MSG_ANNOUNCE_TIMEOUT: + handleAnnounceTimeout(msg); + break; + case GPTP_SELF_MSG_SYNC_TIMEOUT: + handleSyncTimeout(msg); + break; + default: + throw cRuntimeError("Unknown self message (%s)%s, kind=%d", msg->getClassName(), msg->getName(), + msg->getKind()); } } @@ -174,46 +298,63 @@ void Gptp::handleMessage(cMessage *msg) int incomingDomainNumber = gptp->getDomainNumber(); if (incomingDomainNumber != domainNumber) { - EV_ERROR << "Message " << msg->getClassAndFullName() << " arrived with foreign domainNumber " << incomingDomainNumber << ", dropped\n"; + EV_ERROR << "Message " << msg->getClassAndFullName() << " arrived with foreign domainNumber " + << incomingDomainNumber << ", dropped\n"; PacketDropDetails details; details.setReason(NOT_ADDRESSED_TO_US); emit(packetDroppedSignal, packet, &details); } + else if (gptpMessageType == GPTPTYPE_ANNOUNCE && bmcaPortIds.find(incomingNicId) != bmcaPortIds.end()) { + processAnnounce(packet, check_and_cast(gptp.get())); + } else if (incomingNicId == slavePortId) { // slave port switch (gptpMessageType) { - case GPTPTYPE_SYNC: - processSync(packet, check_and_cast(gptp.get())); - break; - case GPTPTYPE_FOLLOW_UP: - processFollowUp(packet, check_and_cast(gptp.get())); - // Send a request to send Sync message - // through other gptp Ethernet interfaces - if(gptpNodeType == BRIDGE_NODE) - sendSync(); - break; - case GPTPTYPE_PDELAY_RESP: - processPdelayResp(packet, check_and_cast(gptp.get())); - break; - case GPTPTYPE_PDELAY_RESP_FOLLOW_UP: - processPdelayRespFollowUp(packet, check_and_cast(gptp.get())); - break; - default: + case GPTPTYPE_SYNC: + processSync(packet, check_and_cast(gptp.get())); + break; + case GPTPTYPE_FOLLOW_UP: + processFollowUp(packet, check_and_cast(gptp.get())); + break; + case GPTPTYPE_PDELAY_RESP: + processPdelayResp(packet, check_and_cast(gptp.get())); + break; + case GPTPTYPE_PDELAY_RESP_FOLLOW_UP: + processPdelayRespFollowUp(packet, check_and_cast(gptp.get())); + break; + default: + if (gptpNodeType != BMCA_NODE) { throw cRuntimeError("Unknown gPTP packet type: %d", (int)(gptpMessageType)); + } + else { + // In BMCA mode, packets might be still in transmission when switching port state + // Ignore and throw warning + EV_WARN << "Message " << msg->getClassAndFullName() << " arrived on slave port " << incomingNicId + << ", dropped\n"; + } } } else if (masterPortIds.find(incomingNicId) != masterPortIds.end()) { // master port - if(gptpMessageType == GPTPTYPE_PDELAY_REQ) { + if (gptpMessageType == GPTPTYPE_PDELAY_REQ) { processPdelayReq(packet, check_and_cast(gptp.get())); } else { - throw cRuntimeError("Unaccepted gPTP type: %d", (int)(gptpMessageType)); + if (gptpNodeType != BMCA_NODE) { + throw cRuntimeError("Unaccepted gPTP type: %d", (int)(gptpMessageType)); + } + else { + // In BMCA mode, packets might be still in transmission when switching port state + // Ignore and throw warning + EV_WARN << "Message " << msg->getClassAndFullName() << " arrived on master port " << incomingNicId + << ", dropped\n"; + } } } else { // passive port - EV_ERROR << "Message " << msg->getClassAndFullName() << " arrived on passive port " << incomingNicId << ", dropped\n"; + EV_ERROR << "Message " << msg->getClassAndFullName() << " arrived on passive port " << incomingNicId + << ", dropped\n"; } delete msg; } @@ -234,6 +375,44 @@ void Gptp::sendPacketToNIC(Packet *packet, int portId) send(packet, "socketOut"); } +void Gptp::sendAnnounce() +{ + for (auto port : bmcaPortIds) { + if (port == slavePortId || passivePortIds.find(port) != passivePortIds.end()) + continue; + + auto packet = new Packet("GptpAnnounce"); + packet->addTag()->setDestAddress(GPTP_MULTICAST_ADDRESS); + + auto gptp = makeShared(); + gptp->setDomainNumber(domainNumber); + gptp->setOriginTimestamp(clock->getClockTime()); + gptp->setSequenceId(sequenceId++); + + if (bestAnnounce == nullptr || + bestAnnounce->getPriorityVector().grandmasterIdentity == localPriorityVector.grandmasterIdentity) + { + gptp->setPriorityVector(localPriorityVector); + } + else { + auto newPriorityVector = bestAnnounce->getPriorityVector(); + newPriorityVector.stepsRemoved++; + gptp->setPriorityVector(newPriorityVector); + } + + PortIdentity portId; + portId.clockIdentity = clockIdentity; + portId.portNumber = port; + + gptp->setSourcePortIdentity(portId); + packet->insertAtFront(gptp); + + sendPacketToNIC(packet, port); + } + + rescheduleClockEventAfter(announceInterval, selfMsgAnnounce); +} + void Gptp::sendSync() { auto packet = new Packet("GptpSync"); @@ -241,70 +420,406 @@ void Gptp::sendSync() auto gptp = makeShared(); gptp->setDomainNumber(domainNumber); /* OriginTimestamp always get Sync departure time from grand master */ - if (gptpNodeType == MASTER_NODE) { - originTimestamp = clock->getClockTime(); + if (isGM()) { + preciseOriginTimestamp = clock->getClockTime(); } - //gptp->setOriginTimestamp(CLOCKTIME_ZERO); - gptp->setSequenceId(sequenceId++); + // gptp->setOriginTimestamp(CLOCKTIME_ZERO); - sentTimeSyncSync = clock->getClockTime(); + gptp->setSequenceId(sequenceId++); + // Correction field for Sync message is zero for two-step mode + // See Table 11-6 in IEEE 802.1AS-2020 + // Change when implementing CMLDS + gptp->setCorrectionField(CLOCKTIME_ZERO); packet->insertAtFront(gptp); - for (auto port: masterPortIds) + for (auto port : masterPortIds) sendPacketToNIC(packet->dup(), port); delete packet; // The sendFollowUp(portId) called by receiveSignal(), when GptpSync sent } -void Gptp::sendFollowUp(int portId, const GptpSync *sync, clocktime_t syncTxEndTimestamp) +void Gptp::sendFollowUp(int portId, const GptpSync *sync, const clocktime_t &syncEgressTimestampOwn) { auto packet = new Packet("GptpFollowUp"); packet->addTag()->setDestAddress(GPTP_MULTICAST_ADDRESS); auto gptp = makeShared(); gptp->setDomainNumber(domainNumber); - gptp->setPreciseOriginTimestamp(originTimestamp); + gptp->setPreciseOriginTimestamp(preciseOriginTimestamp); gptp->setSequenceId(sync->getSequenceId()); - if (gptpNodeType == MASTER_NODE) { - gptp->setCorrectionField(syncTxEndTimestamp - originTimestamp); + clocktime_t residenceTime; + if (isGM()) { + residenceTime = syncEgressTimestampOwn - preciseOriginTimestamp; + gptp->setCorrectionField(residenceTime); } - else if (gptpNodeType == BRIDGE_NODE) - { - /**************** Correction field calculation ********************************************* - * It is calculated by adding peer delay, residence time and packet transmission time * - * correctionField(i)=correctionField(i-1)+peerDelay+(timeReceivedSync-timeSentSync)*(1-f) * - *******************************************************************************************/ - gptp->setCorrectionField(syncTxEndTimestamp - originTimestamp); + else if (gptpNodeType == BRIDGE_NODE || gptpNodeType == BMCA_NODE) { + if (syncIngressTimestamp == -1) { + // There is a rare case when a SYNC message is in transmission, while BMCA selects a new GM, the followup + // message contains a wrong timestamp. + // In this case we simply drop the message. + delete packet; + return; + } + + residenceTime = syncEgressTimestampOwn - syncIngressTimestamp; + // meanLinkDelay and residence time are in the local time base + // In the correctionField we need to express it in the grandmaster's time base + // Thus, we need to multiply the meanLinkDelay and residenceTime with the gmRateRatio + auto newCorrectionField = correctionField + gmRateRatio * (meanLinkDelay + residenceTime); + gptp->setCorrectionField(newCorrectionField); } + emit(residenceTimeSignal, residenceTime.asSimTime()); + emit(correctionFieldEgressSignal, gptp->getCorrectionField().asSimTime()); gptp->getFollowUpInformationTLVForUpdate().setRateRatio(gmRateRatio); packet->insertAtFront(gptp); + + EV_INFO << "############## SEND FOLLOW_UP ################################" << endl; + EV_INFO << "Correction Field - " << gptp->getCorrectionField() << endl; + EV_INFO << "gmRateRatio - " << gmRateRatio << endl; + EV_INFO << "meanLinkDelay - " << meanLinkDelay << endl; + EV_INFO << "residenceTime - " << residenceTime << endl; + sendPacketToNIC(packet, portId); } -void Gptp::sendPdelayResp(GptpReqAnswerEvent* req) +void Gptp::processAnnounce(Packet *packet, const GptpAnnounce *announce) +{ + if (announce->getPriorityVector().stepsRemoved >= 255) { + // IEEE 1588-2019 9.3.2.5 d) + EV_WARN << "Announce message dropped because stepsRemoved is 255" << endl; + return; + } + if (announce->getPriorityVector().grandmasterIdentity == clockIdentity) { + EV_WARN << "Announce message dropped because grandmasterIdentity is own clockIdentity - " + "why does this even happen?" + << endl; + return; + } + + auto incomingNicId = packet->getTag()->getInterfaceId(); + if (receivedAnnounces.find(incomingNicId) != receivedAnnounces.end()) { + delete receivedAnnounces[incomingNicId]; + } + receivedAnnounces[incomingNicId] = announce->dup(); + + clocktime_t gptpAnnounceTimeoutTime = par("announceTimeout"); + if (announceTimeouts.find(incomingNicId) != announceTimeouts.end()) { + rescheduleClockEventAfter(gptpAnnounceTimeoutTime, announceTimeouts[incomingNicId]); + } + else { + auto announceTimeoutEvent = new ClockEvent("gptpAnnounceTimeout", GPTP_SELF_MSG_ANNOUNCE_TIMEOUT); + auto nicIdPar = announceTimeoutEvent->addPar("nicId").setLongValue(incomingNicId); + announceTimeouts[incomingNicId] = announceTimeoutEvent; + scheduleClockEventAfter(gptpAnnounceTimeoutTime, announceTimeoutEvent); + } + + executeBmca(); +} + +void Gptp::executeBmca() +{ + PortIdentity ownPortIdentity; + ownPortIdentity.clockIdentity = clockIdentity; + ownPortIdentity.portNumber = 0; + + // IEEE 1588-2019 Table 29 + auto ownAnnounce = new GptpAnnounce(); + ownAnnounce->setPriorityVector(localPriorityVector); + ownAnnounce->setSourcePortIdentity(ownPortIdentity); + + auto bestAnnounceCurr = ownAnnounce; + auto bestAnnounceReceiverIdentityCurr = ownPortIdentity; + + for (const auto &announceEntry : receivedAnnounces) { + auto a = announceEntry.second; + auto aReceiverIdentity = PortIdentity(); + aReceiverIdentity.clockIdentity = clockIdentity; + aReceiverIdentity.portNumber = announceEntry.first; + + switch (compareAnnounceMessages(a, bestAnnounceCurr, aReceiverIdentity, bestAnnounceReceiverIdentityCurr)) { + case A_BETTER_THAN_B_BY_TOPOLOGY: + case A_BETTER_THAN_B: + // If a is better or better by topology, then a becomes the new best Announce + bestAnnounceCurr = announceEntry.second; + bestAnnounceReceiverIdentityCurr = aReceiverIdentity; + break; + case B_BETTER_THAN_A_BY_TOPOLOGY: + case B_BETTER_THAN_A: + // If bestAnnounceCurr is better or better by topology, then nothing changes + break; + } + } + + auto fullGmPath = + clockIdentityToFullPath[std::to_string(bestAnnounceCurr->getPriorityVector().grandmasterIdentity)]; + + EV_INFO << "############## BMCA ################################" << endl; + EV_INFO << "Choosing from " << receivedAnnounces.size() << " Announces" << endl; + EV_INFO << "Selected Grandmaster Identity - " << bestAnnounceCurr->getPriorityVector().grandmasterIdentity + << endl; + EV_INFO << "Selected Grandmaster Name - " + << clockIdentityToFullPath[std::to_string(bestAnnounceCurr->getPriorityVector().grandmasterIdentity)] + << endl; + EV_INFO << "Selected Grandmaster Priority1 - " + << (int)bestAnnounceCurr->getPriorityVector().grandmasterPriority1 << endl; + EV_INFO << "Selected Slave Port Identity - " << bestAnnounceReceiverIdentityCurr.portNumber << endl; + + if (bestAnnounce == bestAnnounceCurr) { + // Received an Announce message, but best clock is still the same + } + else if (bestAnnounce && bestAnnounce->getPriorityVector() == bestAnnounceCurr->getPriorityVector() && + slavePortId == bestAnnounceReceiverIdentityCurr.portNumber) + { + // Same priority vector but newer Announce, no topology change because priority vector is the same + // Store the new Announce + delete bestAnnounce; + bestAnnounce = bestAnnounceCurr->dup(); + } + else { + // Topology change, update ports and send Announce + bool newInfo = true; + + if (bestAnnounceCurr->getPriorityVector().grandmasterIdentity != clockIdentity) { + // We are not the GM + auto oldSlavePortId = slavePortId; + + slavePortId = bestAnnounceReceiverIdentityCurr.portNumber; + masterPortIds.clear(); + passivePortIds.clear(); + for (const auto &portId : bmcaPortIds) { + if (portId != slavePortId) { + masterPortIds.insert(portId); + } + } + auto masterName = interfaceTable->getInterfaceById(slavePortId) + ->getRxTransmissionChannel() + ->getSourceGate() + ->getOwnerModule() + ->getFullName(); + getContainingNode(this)->bubble(("My master is " + std::string(masterName)).c_str()); + + auto gmId = getModuleByPath(fullGmPath.c_str())->getId(); + emit(gmIdSignal, gmId); + + // In this case we have a new master we need to reset stuff for everything to function properly + resetGptpAfterMasterChange(); + } + else { + // We are the GM: + getContainingNode(this)->bubble("I'm GM"); + if (slavePortId == -1) { + // We were already GM, no need to send Announce again + // Should only happen in the bootup phase + // (otherwise priority vector would be the same and earlier case would apply) + newInfo = true; + } + masterPortIds = bmcaPortIds; + passivePortIds.clear(); + slavePortId = -1; + + emit(gmIdSignal, getId()); + } + + delete bestAnnounce; + bestAnnounce = bestAnnounceCurr->dup(); + + if (newInfo && sendAnnounceImmediately) { + sendAnnounce(); + } + scheduleMessageOnTopologyChange(); + } + + // Calculate ports states (based on IEEE 1588-2019 Figure 33) + masterPortIds.clear(); + passivePortIds.clear(); + for (const auto &portId : bmcaPortIds) { + if (portId != slavePortId) { + masterPortIds.insert(portId); + } + } + if (slavePortId != -1) { + // Don't do this calculation if we're selected as the GM => We assume all gPTP enabled ports are masters + for (const auto &portId : masterPortIds) { + GptpAnnounce *portAnnounce = nullptr; + if (auto it = receivedAnnounces.find(portId); it != receivedAnnounces.end()) { + portAnnounce = it->second; + } + + PortIdentity receiverPortIdentity; + receiverPortIdentity.clockIdentity = clockIdentity; + receiverPortIdentity.portNumber = portId; + + // Standard 1588-2019 defines 1 to 127, but 0 is reserved and unused anyway. + // The standard thus does not define the behavior for 0, so we just use <= 127 + if (localPriorityVector.grandmasterClockQuality.clockClass <= 127) { + if (portAnnounce == nullptr) { + continue; + } + auto compareLocalToPort = + compareAnnounceMessages(ownAnnounce, portAnnounce, ownPortIdentity, receiverPortIdentity); + + if (compareLocalToPort == B_BETTER_THAN_A || compareLocalToPort == B_BETTER_THAN_A_BY_TOPOLOGY) { + passivePortIds.insert(portId); + } + continue; + } + + // Clock class >= 128 + auto compareLocalToBest = + compareAnnounceMessages(ownAnnounce, bestAnnounce, ownPortIdentity, bestAnnounceReceiverIdentityCurr); + + if (compareLocalToBest == A_BETTER_THAN_B || compareLocalToBest == A_BETTER_THAN_B_BY_TOPOLOGY) { + // Local better than best => Master + continue; + } + + if (bestAnnounceReceiverIdentityCurr.portNumber == portId) { + // => Slave (already set above) + continue; + } + + if (portAnnounce == nullptr) { + continue; + } + + auto compareBestToPort = compareAnnounceMessages(bestAnnounce, portAnnounce, + bestAnnounceReceiverIdentityCurr, receiverPortIdentity); + if (compareBestToPort == A_BETTER_THAN_B_BY_TOPOLOGY) { + passivePortIds.insert(portId); + } + } + for (const auto &passivePortId : passivePortIds) { + masterPortIds.erase(passivePortId); + } + } + + delete ownAnnounce; +} + +Gptp::BmcaPriorityVectorComparisonResult Gptp::compareAnnounceMessages(GptpAnnounce *a, GptpAnnounce *b, + PortIdentity aReceiverIdentity, + PortIdentity bReceiverIdentity) +{ + auto aVec = a->getPriorityVector(); + auto bVec = b->getPriorityVector(); + + // IEEE 1588-2019 Figure 34 + if (aVec.grandmasterIdentity == bVec.grandmasterIdentity) { + // Special cases Figure 35 + + // Compare steps removed (|A-B| >=2) + if (aVec.stepsRemoved > bVec.stepsRemoved + 1) { + return B_BETTER_THAN_A; + } + + else if (aVec.stepsRemoved + 1 < bVec.stepsRemoved) { + return A_BETTER_THAN_B; + } + + // Compare steps removed |A-B| <= 1 + if (aVec.stepsRemoved > bVec.stepsRemoved) { + if (aReceiverIdentity == a->getSourcePortIdentity()) { + throw cRuntimeError("Error-1: Indicates that one of the PTP messages was " + "transmitted and received on the same PTP Port.\n" + "(See IEEE 1588-2019 Figure 35 NOTE 2)"); + } + else if (aReceiverIdentity < a->getSourcePortIdentity()) { + return B_BETTER_THAN_A; + } + else { + // aReceiverIdentity > a->getSourcePortIdentity() + return B_BETTER_THAN_A_BY_TOPOLOGY; + } + } + else if (aVec.stepsRemoved < bVec.stepsRemoved) { + if (bReceiverIdentity == b->getSourcePortIdentity()) { + throw cRuntimeError("Error-1: Indicates that one of the PTP messages was " + "transmitted and received on the same PTP Port.\n" + "(See IEEE 1588-2019 Figure 35 NOTE 2)"); + } + else if (bReceiverIdentity < b->getSourcePortIdentity()) { + return A_BETTER_THAN_B; + } + else { + return A_BETTER_THAN_B_BY_TOPOLOGY; + } + } + + // Compare Identities of senders + if (a->getSourcePortIdentity() > b->getSourcePortIdentity()) { + return B_BETTER_THAN_A_BY_TOPOLOGY; + } + else if (a->getSourcePortIdentity() < b->getSourcePortIdentity()) { + return A_BETTER_THAN_B_BY_TOPOLOGY; + } + + // Compare port number of receivers + if (aReceiverIdentity.portNumber > bReceiverIdentity.portNumber) { + return B_BETTER_THAN_A_BY_TOPOLOGY; + } + else if (aReceiverIdentity.portNumber < bReceiverIdentity.portNumber) { + return A_BETTER_THAN_B_BY_TOPOLOGY; + } + else { + throw cRuntimeError("Error-2: indicates that the PTP messages are duplicates or that they are " + "an earlier and later PTP message from the same Grandmaster PTP Instance.\n" + "(See IEEE 1588-2019 Figure 35 NOTE 2)"); + } + } + else if (aVec > bVec) { + return B_BETTER_THAN_A; + } + else if (aVec < bVec) { + return A_BETTER_THAN_B; + } + else { + throw cRuntimeError("Unknown case in BMCA -- should never happen"); + } +} + +void Gptp::sendPdelayResp(GptpReqAnswerEvent *req) { + reqAnswerEvents.erase(req); + int portId = req->getPortId(); auto packet = new Packet("GptpPdelayResp"); packet->addTag()->setDestAddress(GPTP_MULTICAST_ADDRESS); auto gptp = makeShared(); + + // Correction field for Pdelay_Resp contains fractional nanoseconds + // according to the standard, we do not need this in INET. + // See Table 11-6 in IEEE 802.1AS-2020 + gptp->setCorrectionField(CLOCKTIME_ZERO); + gptp->setDomainNumber(domainNumber); gptp->setRequestingPortIdentity(req->getSourcePortIdentity()); gptp->setSequenceId(req->getSequenceId()); - gptp->setRequestReceiptTimestamp(req->getIngressTimestamp()); + gptp->setRequestReceiptTimestamp(req->getIngressTimestamp()); // t2 packet->insertAtFront(gptp); sendPacketToNIC(packet, portId); - // The sendPdelayRespFollowUp(portId) called by receiveSignal(), when GptpPdelayResp sent + // The sendPdelayRespFollowUp(portId) called by receiveSignal(), when + // GptpPdelayResp sent } -void Gptp::sendPdelayRespFollowUp(int portId, const GptpPdelayResp* resp) +void Gptp::sendPdelayRespFollowUp(int portId, const GptpPdelayResp *resp) { auto packet = new Packet("GptpPdelayRespFollowUp"); packet->addTag()->setDestAddress(GPTP_MULTICAST_ADDRESS); auto gptp = makeShared(); - auto now = clock->getClockTime(); gptp->setDomainNumber(domainNumber); - gptp->setResponseOriginTimestamp(now); + + // Correction field for Pdelay_Resp contains fractional nanoseconds + // according to the standard, we do not need this in INET. + // See Table 11-6 in IEEE 802.1AS-2020 + gptp->setCorrectionField(CLOCKTIME_ZERO); + + // We can set this to now, because this function is called directly + // after the transmissionStarted signal for the pdelayResp packet + // is received + auto now = clock->getClockTime(); + gptp->setResponseOriginTimestamp(now); // t3 + gptp->setRequestingPortIdentity(resp->getRequestingPortIdentity()); gptp->setSequenceId(resp->getSequenceId()); packet->insertAtFront(gptp); @@ -318,8 +833,12 @@ void Gptp::sendPdelayReq() packet->addTag()->setDestAddress(GPTP_MULTICAST_ADDRESS); auto gptp = makeShared(); gptp->setDomainNumber(domainNumber); + + // Correction field for Pdelay_Req message is zero for two-step mode + // See Table 11-6 in IEEE 802.1AS-2020 gptp->setCorrectionField(CLOCKTIME_ZERO); - //save and send IDs + + // save and send IDs PortIdentity portId; portId.clockIdentity = clockIdentity; portId.portNumber = slavePortId; @@ -327,22 +846,19 @@ void Gptp::sendPdelayReq() lastSentPdelayReqSequenceId = sequenceId++; gptp->setSequenceId(lastSentPdelayReqSequenceId); packet->insertAtFront(gptp); - pdelayReqEventEgressTimestamp = clock->getClockTime(); rcvdPdelayResp = false; + // sendReqStartTimestamp = clock->getClockTime(); sendPacketToNIC(packet, slavePortId); } - -void Gptp::processSync(Packet *packet, const GptpSync* gptp) +void Gptp::processSync(Packet *packet, const GptpSync *gptp) { rcvdGptpSync = true; lastReceivedGptpSyncSequenceId = gptp->getSequenceId(); - - // peerSentTimeSync = gptp->getOriginTimestamp(); // TODO this is unfilled in two-step mode syncIngressTimestamp = packet->getTag()->getArrivalClockTime(); } -void Gptp::processFollowUp(Packet *packet, const GptpFollowUp* gptp) +void Gptp::processFollowUp(Packet *packet, const GptpFollowUp *gptp) { // check: is received the GptpSync for this GptpFollowUp? if (!rcvdGptpSync) { @@ -355,97 +871,186 @@ void Gptp::processFollowUp(Packet *packet, const GptpFollowUp* gptp) return; } - originTimestamp = gptp->getPreciseOriginTimestamp(); + preciseOriginTimestamp = gptp->getPreciseOriginTimestamp(); correctionField = gptp->getCorrectionField(); - peerSentTimeSync = gptp->getPreciseOriginTimestamp() + gptp->getCorrectionField(); receivedRateRatio = gptp->getFollowUpInformationTLV().getRateRatio(); + emit(correctionFieldIngressSignal, correctionField.asSimTime()); + synchronize(); - EV_INFO << "############## FOLLOW_UP ################################"<< endl; - EV_INFO << "RECEIVED TIME AFTER SYNC - " << newLocalTimeAtTimeSync << endl; - EV_INFO << "ORIGIN TIME SYNC - " << originTimestamp << endl; - EV_INFO << "CORRECTION FIELD - " << correctionField << endl; - EV_INFO << "PROPAGATION DELAY - " << peerDelay << endl; - EV_INFO << "peerSentTimeSync - " << peerSentTimeSync << endl; - EV_INFO << "receivedRateRatio - " << receivedRateRatio << endl; + EV_INFO << "############## FOLLOW_UP ################################" << endl; + EV_INFO << "RECEIVED TIME AFTER SYNC - " << newLocalTimeAtTimeSync << endl; + EV_INFO << "ORIGIN TIME SYNC - " << preciseOriginTimestamp << endl; + EV_INFO << "CORRECTION FIELD - " << correctionField << endl; + EV_INFO << "PROPAGATION DELAY - " << meanLinkDelay << endl; + EV_INFO << "receivedRateRatio - " << receivedRateRatio << endl; rcvdGptpSync = false; + + // Send a request to send Sync message + // through other gptp Ethernet interfaces + if (gptpNodeType == BRIDGE_NODE || gptpNodeType == BMCA_NODE) + sendSync(); } void Gptp::synchronize() { - simtime_t now = simTime(); - clocktime_t oldLocalTimeAtTimeSync = clock->getClockTime(); - clocktime_t residenceTime = oldLocalTimeAtTimeSync - syncIngressTimestamp; - - emit(timeDifferenceSignal, CLOCKTIME_AS_SIMTIME(oldLocalTimeAtTimeSync) - now); - /************** Time synchronization ***************************************** * Local time is adjusted using peer delay, correction field, residence time * * and packet transmission time based departure time of Sync message from GM * *****************************************************************************/ - clocktime_t newTime = originTimestamp + correctionField + peerDelay + residenceTime; + simtime_t now = simTime(); + clocktime_t oldLocalTimeAtTimeSync = clock->getClockTime(); + emit(timeDifferenceSignal, CLOCKTIME_AS_SIMTIME(oldLocalTimeAtTimeSync) - now); - ASSERT(gptpNodeType != MASTER_NODE); + clocktime_t residenceTime = oldLocalTimeAtTimeSync - syncIngressTimestamp; - // TODO validate the following expression with the standard - if (oldPeerSentTimeSync == -1) - gmRateRatio = 1; - else - gmRateRatio = (peerSentTimeSync - oldPeerSentTimeSync) / (syncIngressTimestamp - receivedTimeSync); + ASSERT(!isGM()); + + calculateGmRatio(); + + // preciseOriginTimestamp and correctionField are in the grandmaster's time base + // meanLinkDelay and residence time are in the local time base + // Thus, we need to multiply the meanLinkDelay and residenceTime with the gmRateRatio + clocktime_t newTime = preciseOriginTimestamp + correctionField + gmRateRatio * (meanLinkDelay + residenceTime); - auto settableClock = check_and_cast(clock.get()); - ppm newOscillatorCompensation = unit(gmRateRatio * (1 + settableClock->getOscillatorCompensation().get()) - 1); - settableClock->setClockTime(newTime, newOscillatorCompensation, true); + auto servoClock = check_and_cast(clock.get()); + servoClock->adjustClockTo(newTime); + // EV_INFO << "newOscillatorCompensation " << newOscillatorCompensation << endl; newLocalTimeAtTimeSync = clock->getClockTime(); - timeDiffAtTimeSync = newLocalTimeAtTimeSync - oldLocalTimeAtTimeSync; - receivedTimeSync = syncIngressTimestamp; - // adjust local timestamps, too - adjustLocalTimestamp(pdelayRespEventIngressTimestamp); - adjustLocalTimestamp(pdelayReqEventEgressTimestamp); - adjustLocalTimestamp(receivedTimeSync); + updateSyncStateAndRescheduleSyncTimeout(servoClock); /************** Rate ratio calculation ************************************* * It is calculated based on interval between two successive Sync messages * ***************************************************************************/ - EV_INFO << "############## SYNC #####################################"<< endl; + EV_INFO << "############## SYNC #####################################" << endl; EV_INFO << "LOCAL TIME BEFORE SYNC - " << oldLocalTimeAtTimeSync << endl; EV_INFO << "LOCAL TIME AFTER SYNC - " << newLocalTimeAtTimeSync << endl; + EV_INFO << "CALCULATED NEW TIME - " << newTime << endl; + if (servoClock->referenceClockModule != nullptr) { + auto referenceClockTime = servoClock->referenceClockModule->getClockTime(); + auto diffReferenceToOldLocal = oldLocalTimeAtTimeSync - referenceClockTime; + auto diffReferenceToNewTime = newTime - referenceClockTime; + EV_INFO << "REFERENCE CLOCK TIME - " << referenceClockTime << endl; + EV_INFO << "DIFF REFERENCE TO OLD TIME - " << diffReferenceToOldLocal << endl; + EV_INFO << "DIFF REFERENCE TO NEW TIME - " << diffReferenceToNewTime << endl; + } EV_INFO << "CURRENT SIMTIME - " << now << endl; - EV_INFO << "ORIGIN TIME SYNC - " << peerSentTimeSync << endl; - EV_INFO << "PREV ORIGIN TIME SYNC - " << oldPeerSentTimeSync << endl; + EV_INFO << "ORIGIN TIME SYNC - " << preciseOriginTimestamp << endl; + EV_INFO << "PREV ORIGIN TIME SYNC - " << preciseOriginTimestampLast << endl; + EV_INFO << "SYNC INGRESS TIME - " << syncIngressTimestamp << endl; + EV_INFO << "SYNC INGRESS TIME LAST - " << syncIngressTimestampLast << endl; EV_INFO << "RESIDENCE TIME - " << residenceTime << endl; EV_INFO << "CORRECTION FIELD - " << correctionField << endl; - EV_INFO << "PROPAGATION DELAY - " << peerDelay << endl; + EV_INFO << "PROPAGATION DELAY - " << meanLinkDelay << endl; EV_INFO << "TIME DIFFERENCE TO SIMTIME - " << CLOCKTIME_AS_SIMTIME(newLocalTimeAtTimeSync) - now << endl; + EV_INFO << "NEIGHBOR RATE RATIO - " << neighborRateRatio << endl; + EV_INFO << "RECIEVED RATE RATIO - " << receivedRateRatio << endl; EV_INFO << "GM RATE RATIO - " << gmRateRatio << endl; - oldPeerSentTimeSync = peerSentTimeSync; + syncIngressTimestampLast = syncIngressTimestamp; + preciseOriginTimestampLast = preciseOriginTimestamp; - emit(rateRatioSignal, gmRateRatio); + emit(receivedRateRatioSignal, receivedRateRatio); + emit(gmRateRatioSignal, gmRateRatio); emit(localTimeSignal, CLOCKTIME_AS_SIMTIME(newLocalTimeAtTimeSync)); emit(timeDifferenceSignal, CLOCKTIME_AS_SIMTIME(newLocalTimeAtTimeSync) - now); } -void Gptp::processPdelayReq(Packet *packet, const GptpPdelayReq* gptp) +void Gptp::updateSyncStateAndRescheduleSyncTimeout(const ServoClockBase *servoClock) +{ + switch (servoClock->getClockState()) { + case ServoClockBase::INIT: + changeSyncState(UNSYNCED); + break; + case ServoClockBase::SYNCED: + changeSyncState(SYNCED); + break; + } + + if (selfMsgSyncTimeout->isScheduled()) { + rescheduleClockEventAfter(par("syncTimeout"), selfMsgSyncTimeout); + } + else if (!isGM()) { + scheduleClockEventAfter(par("syncTimeout"), selfMsgSyncTimeout); + } +} + +void Gptp::changeSyncState(SyncState state, bool resetClock) +{ + auto prevSyncState = syncState; + syncState = state; + if (prevSyncState != syncState) { + emit(gptpSyncStateChanged, syncState); + } + + auto servoClock = dynamic_cast(clock.get()); + if (resetClock && state == UNSYNCED && servoClock != nullptr && par("resetClockStateOnSyncLoss").boolValue()) { + servoClock->resetClockState(); + } +} + +void Gptp::calculateGmRatio() +{ + switch (gmRateRatioCalculationMethod) { + case NONE: + gmRateRatio = 1.0; + break; + case NRR: + gmRateRatio = receivedRateRatio * neighborRateRatio; + break; + case DIRECT: + peerSentTimeSync = preciseOriginTimestamp + correctionField; + if (syncIngressTimestampLast != -1) { + gmRateRatio = (peerSentTimeSync - peerSentTimeSyncLast) / (syncIngressTimestamp - syncIngressTimestampLast); + } + peerSentTimeSyncLast = peerSentTimeSync; + break; + } +} + +void Gptp::resetGptpAfterMasterChange() +{ + // Reset GPTP variables + neighborRateRatio = 1.0; + rcvdGptpSync = false; + rcvdPdelayResp = false; + meanLinkDelay = CLOCKTIME_ZERO; + syncIngressTimestamp = -1; + syncIngressTimestampLast = -1; + + changeSyncState(UNSYNCED, true); +} + +void Gptp::processPdelayReq(Packet *packet, const GptpPdelayReq *gptp) { auto resp = new GptpReqAnswerEvent("selfMsgPdelayResp", GPTP_SELF_REQ_ANSWER_KIND); resp->setPortId(packet->getTag()->getInterfaceId()); - resp->setIngressTimestamp(packet->getTag()->getArrivalClockTime()); + auto ingressTimestamp = packet->getTag()->getArrivalClockTime(); // t1 + if (ingressTimestamp == CLOCKTIME_ZERO) { + throw cRuntimeError("No ingress timestamp available," + "ensure that your interface emits the transmissionStartedSignal" + "(e.g. use the EthernetStreamingPhyLayer)"); + } + resp->setIngressTimestamp(packet->getTag()->getArrivalClockTime()); // t2 resp->setSourcePortIdentity(gptp->getSourcePortIdentity()); resp->setSequenceId(gptp->getSequenceId()); + reqAnswerEvents.insert(resp); + scheduleClockEventAfter(pDelayReqProcessingTime, resp); } -void Gptp::processPdelayResp(Packet *packet, const GptpPdelayResp* gptp) +void Gptp::processPdelayResp(Packet *packet, const GptpPdelayResp *gptp) { // verify IDs - if (gptp->getRequestingPortIdentity().clockIdentity != clockIdentity || gptp->getRequestingPortIdentity().portNumber != slavePortId) { + if (gptp->getRequestingPortIdentity().clockIdentity != clockIdentity || + gptp->getRequestingPortIdentity().portNumber != slavePortId) + { EV_WARN << "GptpPdelayResp arrived with invalid PortIdentity, dropped"; return; } @@ -455,19 +1060,25 @@ void Gptp::processPdelayResp(Packet *packet, const GptpPdelayResp* gptp) } rcvdPdelayResp = true; - pdelayRespEventIngressTimestamp = packet->getTag()->getArrivalClockTime(); - peerRequestReceiptTimestamp = gptp->getRequestReceiptTimestamp(); - peerResponseOriginTimestamp = CLOCKTIME_ZERO; + + if (pDelayRespIngressTimestampSetStart == -1) { + pDelayRespIngressTimestampSetStart = pDelayRespIngressTimestamp; // t4 last + } + + pDelayRespIngressTimestamp = packet->getTag()->getArrivalClockTime(); // t4 now + pDelayReqIngressTimestamp = gptp->getRequestReceiptTimestamp(); // t2 } -void Gptp::processPdelayRespFollowUp(Packet *packet, const GptpPdelayRespFollowUp* gptp) +void Gptp::processPdelayRespFollowUp(Packet *packet, const GptpPdelayRespFollowUp *gptp) { if (!rcvdPdelayResp) { EV_WARN << "GptpPdelayRespFollowUp arrived without GptpPdelayResp, dropped"; return; } // verify IDs - if (gptp->getRequestingPortIdentity().clockIdentity != clockIdentity || gptp->getRequestingPortIdentity().portNumber != slavePortId) { + if (gptp->getRequestingPortIdentity().clockIdentity != clockIdentity || + gptp->getRequestingPortIdentity().portNumber != slavePortId) + { EV_WARN << "GptpPdelayRespFollowUp arrived with invalid PortIdentity, dropped"; return; } @@ -476,19 +1087,57 @@ void Gptp::processPdelayRespFollowUp(Packet *packet, const GptpPdelayRespFollowU return; } - peerResponseOriginTimestamp = gptp->getResponseOriginTimestamp(); + if (pDelayRespEgressTimestampSetStart == -1) { + pDelayRespEgressTimestampSetStart = pDelayRespEgressTimestamp; // t3 last + } - // computePropTime(): - peerDelay = (gmRateRatio * (pdelayRespEventIngressTimestamp - pdelayReqEventEgressTimestamp) - (peerResponseOriginTimestamp - peerRequestReceiptTimestamp)) / 2.0; + pDelayRespEgressTimestamp = gptp->getResponseOriginTimestamp(); // t3 now + + // Note, that the standard defines the usage of the correction field + // for the following two calculations. + // However, these contain fractional nanoseconds, which we do not + // use in INET. + // See 11.2.19.3.3 computePdelayRateRatio() in IEEE 802.1AS-2020 + clocktime_t prevRespRegress = -1; + clocktime_t prevRespIngress = -1; + + if (nrrCalculationSetCurrent == nrrCalculationSetMaximum) { + neighborRateRatio = (pDelayRespEgressTimestamp - pDelayRespEgressTimestampSetStart) / + (pDelayRespIngressTimestamp - pDelayRespIngressTimestampSetStart); + pDelayRespEgressTimestampSetStart = -1; + pDelayRespIngressTimestampSetStart = -1; + nrrCalculationSetCurrent = 0; + } + else { + nrrCalculationSetCurrent++; + } - EV_INFO << "RATE RATIO - " << gmRateRatio << endl; - EV_INFO << "pdelayReqEventEgressTimestamp - " << pdelayReqEventEgressTimestamp << endl; - EV_INFO << "peerResponseOriginTimestamp - " << peerResponseOriginTimestamp << endl; - EV_INFO << "pdelayRespEventIngressTimestamp - " << pdelayRespEventIngressTimestamp << endl; - EV_INFO << "peerRequestReceiptTimestamp - " << peerRequestReceiptTimestamp << endl; - EV_INFO << "PEER DELAY - " << peerDelay << endl; + if (!useNrr) { + neighborRateRatio = 1.0; + } + + // See 11.2.19.3.4 computePropTime() and Figure11-1 in IEEE 802.1AS-2020 + auto t4 = pDelayRespIngressTimestamp; + auto t1 = pDelayReqEgressTimestamp; + + auto t2 = pDelayReqIngressTimestamp; + auto t3 = pDelayRespEgressTimestamp; + + auto meanLinkDelayInResponderTimebase = (neighborRateRatio * (t4 - t1) - (t3 - t2)) / 2; + + // Regarding NOTE 1 in 11.2.19.3.4, we need to device by the nrr to get the meanLinkDelay in the current time base + meanLinkDelay = meanLinkDelayInResponderTimebase / neighborRateRatio; - emit(peerDelaySignal, CLOCKTIME_AS_SIMTIME(peerDelay)); + EV_INFO << "GM RATE RATIO - " << gmRateRatio << endl; + EV_INFO << "NEIGHBOR RATE RATIO - " << neighborRateRatio << endl; + EV_INFO << "pDelayReqEgressTimestamp - " << pDelayReqEgressTimestamp << endl; + EV_INFO << "pDelayReqIngressTimestamp - " << pDelayReqIngressTimestamp << endl; + EV_INFO << "pDelayRespEgressTimestamp - " << pDelayRespEgressTimestamp << endl; + EV_INFO << "pDelayRespIngressTimestamp - " << pDelayRespIngressTimestamp << endl; + EV_INFO << "PEER DELAY - " << meanLinkDelay << endl; + + emit(neighborRateRatioSignal, neighborRateRatio); + emit(peerDelaySignal, CLOCKTIME_AS_SIMTIME(meanLinkDelay)); } const GptpBase *Gptp::extractGptpHeader(Packet *packet) @@ -496,7 +1145,7 @@ const GptpBase *Gptp::extractGptpHeader(Packet *packet) PacketDissector::ChunkFinder chunkFinder(&Protocol::gptp); PacketDissector packetDissector(ProtocolDissectorRegistry::getInstance(), chunkFinder); packetDissector.dissectPacket(packet); - const auto& chunk = staticPtrCast(chunkFinder.getChunk()); + const auto &chunk = staticPtrCast(chunkFinder.getChunk()); return chunk != nullptr ? chunk.get() : nullptr; } @@ -504,7 +1153,15 @@ void Gptp::receiveSignal(cComponent *source, simsignal_t simSignal, cObject *obj { Enter_Method("%s", cComponent::getSignalName(simSignal)); - if (simSignal != receptionEndedSignal && simSignal != transmissionEndedSignal) + auto clockBase = check_and_cast(clock.get()); + + if (simSignal == ClockBase::timeJumpedSignal && obj == clockBase) { + auto clockJumpDetails = check_and_cast(details); + handleClockJump(clockJumpDetails); + } + + if (simSignal != receptionStartedSignal && simSignal != transmissionStartedSignal && + simSignal != receptionEndedSignal) return; auto ethernetSignal = check_and_cast(obj); @@ -519,34 +1176,139 @@ void Gptp::receiveSignal(cComponent *source, simsignal_t simSignal, cObject *obj if (gptp->getDomainNumber() != domainNumber) return; - if (simSignal == receptionEndedSignal) - packet->addTagIfAbsent()->setArrivalClockTime(clock->getClockTime()); - else if (simSignal == transmissionEndedSignal) - handleDelayOrSendFollowUp(gptp, source); + if (simSignal == receptionStartedSignal) { + auto transmissionId = ethernetSignal->getTransmissionId(); + auto ingressTime = clock->getClockTime(); + ingressTimeMap[transmissionId] = ingressTime; + // Save ingress time in map + } + else if (simSignal == receptionEndedSignal) { + packet->addTagIfAbsent()->setArrivalClockTime( + ingressTimeMap[ethernetSignal->getTransmissionId()]); + ingressTimeMap.erase(ethernetSignal->getTransmissionId()); + // Read ingress time from map + // Ad tag to packet + } + else if (simSignal == transmissionStartedSignal) + handleTransmissionStartedSignal(gptp, source); +} + +void Gptp::handleAnnounceTimeout(cMessage *pMessage) +{ + int nicId = pMessage->par("nicId").longValue(); + auto it = receivedAnnounces.find(nicId); + if (it == receivedAnnounces.end()) { + throw cRuntimeError("Received announce timeout for unknown NIC %d", nicId); + } + + delete it->second; + receivedAnnounces.erase(it); + executeBmca(); } -void Gptp::handleDelayOrSendFollowUp(const GptpBase *gptp, cComponent *source) +void Gptp::handleSyncTimeout(cMessage *pMessage) { changeSyncState(UNSYNCED, true); } + +void Gptp::handleTransmissionStartedSignal(const GptpBase *gptp, cComponent *source) { - int portId = getContainingNicModule(check_and_cast(source))->getInterfaceId(); + int portId = getContainingNicModule(check_and_cast(source))->getInterfaceId(); switch (gptp->getMessageType()) { case GPTPTYPE_PDELAY_RESP: { - auto gptpResp = check_and_cast(gptp); + auto gptpResp = check_and_cast(gptp); sendPdelayRespFollowUp(portId, gptpResp); break; } case GPTPTYPE_SYNC: { - auto gptpSync = check_and_cast(gptp); + auto gptpSync = check_and_cast(gptp); sendFollowUp(portId, gptpSync, clock->getClockTime()); break; } case GPTPTYPE_PDELAY_REQ: - if (portId == slavePortId) - pdelayReqEventEgressTimestamp = clock->getClockTime(); + if (portId == slavePortId) { + pDelayReqEgressTimestamp = clock->getClockTime(); + } break; default: break; } } -} // namespace inet +void Gptp::handleClockJump(ServoClockBase::ClockJumpDetails *clockJumpDetails) +{ + EV_INFO << "############## Adjust local timestamps #################################" << endl; + EV_INFO << "BEFORE:" << endl; + EV_INFO << "SYNC INGRESS TIME - " << syncIngressTimestamp << endl; + EV_INFO << "SYNC INGRESS TIME LAST - " << syncIngressTimestampLast << endl; + EV_INFO << "PDELAY REQ EGRESS TIME - " << pDelayReqEgressTimestamp << endl; + EV_INFO << "PDELAY RESP INGRESS TIME - " << pDelayRespIngressTimestamp << endl; + EV_INFO << "PDELAY RESP INGRESS TIME SET- " << pDelayRespIngressTimestampSetStart << endl; + + auto timeDiff = clockJumpDetails->newClockTime - clockJumpDetails->oldClockTime; + adjustLocalTimestamp(syncIngressTimestamp, timeDiff); + adjustLocalTimestamp(syncIngressTimestampLast, timeDiff); + adjustLocalTimestamp(pDelayReqEgressTimestamp, timeDiff); + adjustLocalTimestamp(pDelayRespIngressTimestamp, timeDiff); + adjustLocalTimestamp(pDelayRespIngressTimestampSetStart, timeDiff); + // NOTE: Do not pDelayReqIngressTimestamp and pDelayRespEgressTimestamp, because they are based on neighbor clock + + EV_INFO << "AFTER:" << endl; + EV_INFO << "SYNC INGRESS TIME - " << syncIngressTimestamp << endl; + EV_INFO << "SYNC INGRESS TIME LAST - " << syncIngressTimestampLast << endl; + EV_INFO << "PDELAY REQ EGRESS TIME - " << pDelayReqEgressTimestamp << endl; + EV_INFO << "PDELAY RESP INGRESS TIME - " << pDelayRespIngressTimestamp << endl; + EV_INFO << "PDELAY RESP INGRESS TIME SET- " << pDelayRespIngressTimestampSetStart << endl; + + // We also need to adjust the timestamps of scheduled pDelayResp packets + for (auto &reqAnswerEvent : reqAnswerEvents) { + auto ingressTimestamp = reqAnswerEvent->getIngressTimestamp(); + adjustLocalTimestamp(ingressTimestamp, timeDiff); + reqAnswerEvent->setIngressTimestamp(ingressTimestamp); + } + + // This is a very special case, that only occurs when a clock jump occurs between the receptionStarted and + // receptionEnded signal. + // + // You can see this in action in the SteppingClock showcase (At t=9s we do not notify the gPTP module about the + // clock jump, this leads to an incorrect peer delay calculation). We want to allow the gPTP module to still work + // correctly in this case, so we make this adjustment here. + for (auto &entry : ingressTimeMap) { + EV_INFO << " Before Ingress time: " << entry.first << " - " << entry.second << endl; + adjustLocalTimestamp(entry.second, timeDiff); + EV_INFO << " After Ingress time: " << entry.first << " - " << entry.second << endl; + } + // NOTE: There is a special case this does not solve. If the time jump would occur between the reception ended + // and the processing of the packet in the gPTP module the same problem as described above would occur. + // However, this would require a processing delay between the reception ended and the processing of the packet in + // the gPTP module, which to my knowledge is currently not configurable in the INET modules. + // Should this behavior change in the future, this timestamping adjustment mechanism needs to be changed as well to + // somehow also adjust the timestamps already attaches as tags to the gPTP packets. +} +void Gptp::handleParameterChange(const char *name) +{ + // TODO: Maybe one could make more paremeters mutable (e.g. syncInterval might be interesting for some people) + + // Compare if parameter is grandmasterPriority1 + if (!strcmp(name, "grandmasterPriority1")) { + // Update local priority vector + localPriorityVector.grandmasterPriority1 = par("grandmasterPriority1"); + executeBmca(); + } + else if (!strcmp(name, "clockClass")) { + localPriorityVector.grandmasterClockQuality.clockClass = par("clockClass"); + executeBmca(); + } + else if (!strcmp(name, "clockAccuracy")) { + localPriorityVector.grandmasterClockQuality.clockAccuracy = par("clockAccuracy"); + executeBmca(); + } + else if (!strcmp(name, "offsetScaledLogVariance")) { + localPriorityVector.grandmasterClockQuality.offsetScaledLogVariance = par("offsetScaledLogVariance"); + executeBmca(); + } + else if (!strcmp(name, "priority2")) { + localPriorityVector.grandmasterPriority2 = par("priority2"); + executeBmca(); + } +} + +} // namespace inet \ No newline at end of file diff --git a/src/inet/linklayer/ieee8021as/Gptp.h b/src/inet/linklayer/ieee8021as/Gptp.h index af4ade20e8c..b685aec0915 100644 --- a/src/inet/linklayer/ieee8021as/Gptp.h +++ b/src/inet/linklayer/ieee8021as/Gptp.h @@ -4,114 +4,223 @@ // Peter Danielis // University of Rostock, Germany // +// Lucas Haug +// Lun-Yu Yuan +// Chunzhi Guo +// University of Stuttgart, Deterministic6G #ifndef __INET_GPTP_H #define __INET_GPTP_H -#include "inet/clock/contract/ClockTime.h" +#include + #include "inet/clock/common/ClockTime.h" -#include "inet/clock/model/SettableClock.h" +#include "inet/clock/contract/ClockTime.h" +#include "inet/clock/servo/ServoClockBase.h" #include "inet/common/INETDefs.h" #include "inet/common/ModuleRefByPar.h" #include "inet/common/clock/ClockUserModuleBase.h" #include "inet/common/packet/Packet.h" -#include "inet/networklayer/contract/IInterfaceTable.h" #include "inet/linklayer/ieee8021as/GptpPacket_m.h" +#include "inet/networklayer/contract/IInterfaceTable.h" namespace inet { class INET_API Gptp : public ClockUserModuleBase, public cListener { - //parameters: + public: + static std::map clockIdentityToFullPath; + + protected: + enum BmcaPriorityVectorComparisonResult { + A_BETTER_THAN_B, + A_BETTER_THAN_B_BY_TOPOLOGY, + B_BETTER_THAN_A, + B_BETTER_THAN_A_BY_TOPOLOGY, + }; + + protected: + // parameters: ModuleRefByPar interfaceTable; - GptpNodeType gptpNodeType; + // Configuration + GptpNodeType gptpNodeType = GptpNodeType::BMCA_NODE; int domainNumber = -1; - int slavePortId = -1; // interface ID of slave port - std::set masterPortIds; // interface IDs of master ports - clocktime_t correctionField; + int slavePortId = -1; // interface ID of slave port + std::set masterPortIds; // interface IDs of master ports + std::set bmcaPortIds; // interface IDs of bmca ports + std::set passivePortIds; // interface IDs of passive ports (only relevant for BMCA) uint64_t clockIdentity = 0; + clocktime_t syncInterval; + clocktime_t pdelayInterval; + clocktime_t announceInterval; + clocktime_t pDelayReqProcessingTime; // processing time between arrived + // PDelayReq and send of PDelayResp + bool useNrr = false; // use neighbor rate ratio + GmRateRatioCalculationMethod gmRateRatioCalculationMethod = GmRateRatioCalculationMethod::NONE; + // Rate Ratios double gmRateRatio = 1.0; double receivedRateRatio = 1.0; + double neighborRateRatio = 1.0; - clocktime_t originTimestamp; // last outgoing timestamp - - clocktime_t receivedTimeSync; - - clocktime_t syncInterval; - clocktime_t pdelayInterval; + SyncState syncState = SyncState::UNSYNCED; uint16_t sequenceId = 0; - /* Slave port - Variables is used for Peer Delay Measurement */ - uint16_t lastSentPdelayReqSequenceId = 0; - clocktime_t peerDelay; - clocktime_t peerRequestReceiptTimestamp; // pdelayReqIngressTimestamp from peer (received in GptpPdelayResp) - clocktime_t peerResponseOriginTimestamp; // pdelayRespEgressTimestamp from peer (received in GptpPdelayRespFollowUp) - clocktime_t pdelayRespEventIngressTimestamp; // receiving time of last GptpPdelayResp - clocktime_t pdelayReqEventEgressTimestamp; // sending time of last GptpPdelayReq - clocktime_t pDelayReqProcessingTime; // processing time between arrived PDelayReq and send of PDelayResp + // == Propagation Delay Measurement Procedure == + // Timestamps corresponding to the PDelayRequest and PDelayResponse mechanism + clocktime_t pDelayReqEgressTimestamp = -1; // egress time of pdelay_req at initiator (this node) + clocktime_t pDelayReqIngressTimestamp = -1; // ingress time of pdelay_req at responder + clocktime_t pDelayRespEgressTimestamp = + -1; // egress time of pdelay_resp at responder (received in PDelayRespFollowUp) + clocktime_t pDelayRespEgressTimestampSetStart = + -1; // egress time of previous pdelay_resp at responder (received in PDelayRespFollowUp) + clocktime_t pDelayRespIngressTimestamp = -1; // ingress time of pdelay_resp at initiator (this node) + clocktime_t pDelayRespIngressTimestampSetStart = + -1; // ingress time of previous pdelay_resp at initiator (this node) + int nrrCalculationSetMaximum = 1; // TODO: Make this a settable parameter + int nrrCalculationSetCurrent = 0; + bool rcvdPdelayResp = false; + uint16_t lastSentPdelayReqSequenceId = 0; - clocktime_t sentTimeSyncSync; + clocktime_t meanLinkDelay = CLOCKTIME_ZERO; // mean propagation delay between this node and the responder - /* Slave port - Variables is used for Rate Ratio. All times are drifted based on constant drift */ - clocktime_t newLocalTimeAtTimeSync; - clocktime_t timeDiffAtTimeSync; // new local time - old local time - clocktime_t peerSentTimeSync; // sending time of last received GptpSync - clocktime_t oldPeerSentTimeSync = -1; // sending time of previous received GptpSync - clocktime_t syncIngressTimestamp; // receiving time of last incoming GptpSync + // == Sync Procedure == + + // Holds the (received) correction field value, used for the next FollowUp message + // Unsure why this is also configurable with a parameter (TODO: Check) + clocktime_t correctionField = CLOCKTIME_ZERO; + + clocktime_t preciseOriginTimestamp = -1; // timestamp when the last sync message was generated at the GM + clocktime_t preciseOriginTimestampLast = -1; // timestamp when the last sync message was generated at the GM + + clocktime_t peerSentTimeSync = -1; // egress time of Sync at master (this node) + clocktime_t peerSentTimeSyncLast = -1; // egress time of previous Sync at master (this node) + + clocktime_t syncIngressTimestamp = -1; // ingress time of Sync at slave (this node) + clocktime_t syncIngressTimestampLast = -1; // ingress time of previous Sync at slave (this node) bool rcvdGptpSync = false; uint16_t lastReceivedGptpSyncSequenceId = 0xffff; + clocktime_t newLocalTimeAtTimeSync; + + // BMCA + BmcaPriorityVector localPriorityVector; + GptpAnnounce *bestAnnounce = nullptr; + std::map announceTimeouts; + std::map receivedAnnounces; + bool sendAnnounceImmediately = false; + // self timers: - ClockEvent* selfMsgSync = nullptr; - ClockEvent* selfMsgDelayReq = nullptr; - ClockEvent* requestMsg = nullptr; + ClockEvent *selfMsgSync = nullptr; + ClockEvent *selfMsgDelayReq = nullptr; + ClockEvent *selfMsgAnnounce = nullptr; + ClockEvent *selfMsgSyncTimeout = nullptr; + + std::set reqAnswerEvents; // Statistics information: // TODO remove, and replace with emit() calls static simsignal_t localTimeSignal; static simsignal_t timeDifferenceSignal; - static simsignal_t rateRatioSignal; + static simsignal_t gmRateRatioSignal; + static simsignal_t receivedRateRatioSignal; + static simsignal_t neighborRateRatioSignal; static simsignal_t peerDelaySignal; + static simsignal_t residenceTimeSignal; + static simsignal_t correctionFieldIngressSignal; + static simsignal_t correctionFieldEgressSignal; + static simsignal_t gmIdSignal; + + // Packet receive signals: + std::map ingressTimeMap; // domainNumber; }; + clocktime_t getSyncInterval() const { return syncInterval; }; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } + virtual void initialize(int stage) override; + + virtual void initBmca(); + virtual void handleMessage(cMessage *msg) override; virtual void handleSelfMessage(cMessage *msg); - void handleDelayOrSendFollowUp(const GptpBase *gptp, omnetpp::cComponent *source); + + virtual void handleParameterChange(const char *name) override; + + virtual void handleClockJump(ServoClockBase::ClockJumpDetails *clockJumpDetails); + + void handleTransmissionStartedSignal(const GptpBase *gptp, omnetpp::cComponent *source); + const GptpBase *extractGptpHeader(Packet *packet); public: virtual ~Gptp(); + protected: void sendPacketToNIC(Packet *packet, int portId); - void sendSync(); - void sendFollowUp(int portId, const GptpSync *sync, clocktime_t preciseOriginTimestamp); + virtual void sendAnnounce(); + + virtual void sendSync(); + + virtual void sendFollowUp(int portId, const GptpSync *sync, const clocktime_t &syncEgressTimestampOwn); + void sendPdelayReq(); + void sendPdelayResp(GptpReqAnswerEvent *req); - void sendPdelayRespFollowUp(int portId, const GptpPdelayResp* resp); - void processSync(Packet *packet, const GptpSync* gptp); - void processFollowUp(Packet *packet, const GptpFollowUp* gptp); - void processPdelayReq(Packet *packet, const GptpPdelayReq* gptp); - void processPdelayResp(Packet *packet, const GptpPdelayResp* gptp); - void processPdelayRespFollowUp(Packet *packet, const GptpPdelayRespFollowUp* gptp); + void sendPdelayRespFollowUp(int portId, const GptpPdelayResp *resp); + + void processAnnounce(Packet *pPacket, const GptpAnnounce *pAnnounce); + + virtual void processSync(Packet *packet, const GptpSync *gptp); - clocktime_t getCalculatedDrift(IClock *clock, clocktime_t value) { return CLOCKTIME_ZERO; } - void synchronize(); - inline void adjustLocalTimestamp(clocktime_t& time) { time += timeDiffAtTimeSync; } + virtual void processFollowUp(Packet *packet, const GptpFollowUp *gptp); + + void processPdelayReq(Packet *packet, const GptpPdelayReq *gptp); + + void processPdelayResp(Packet *packet, const GptpPdelayResp *gptp); + + void processPdelayRespFollowUp(Packet *packet, const GptpPdelayRespFollowUp *gptp); + + virtual void synchronize(); + + inline void adjustLocalTimestamp(clocktime_t ×tamp, clocktime_t difference) + { + if (timestamp != -1) { + timestamp += difference; + return; + } + else { + EV_INFO << "Timestamp is -1, cannot adjust it." << endl; + } + } virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *obj, cObject *details) override; + virtual void calculateGmRatio(); + virtual void resetGptpAfterMasterChange(); + virtual void executeBmca(); + virtual void initPorts(); + virtual void scheduleMessageOnTopologyChange(); + void handleAnnounceTimeout(cMessage *pMessage); + bool isGM() const { return gptpNodeType == MASTER_NODE || (gptpNodeType == BMCA_NODE && slavePortId == -1); }; + static BmcaPriorityVectorComparisonResult compareAnnounceMessages(GptpAnnounce *a, GptpAnnounce *b, + PortIdentity aReceiverIdentity, + PortIdentity bReceiverIdentity); + void changeSyncState(SyncState state, bool resetClock = false); + void handleSyncTimeout(cMessage *pMessage); + void updateSyncStateAndRescheduleSyncTimeout(const ServoClockBase *servoClock); }; -} +} // namespace inet #endif - diff --git a/src/inet/linklayer/ieee8021as/Gptp.ned b/src/inet/linklayer/ieee8021as/Gptp.ned index 0398ce0d9ac..d78b8ad72e9 100644 --- a/src/inet/linklayer/ieee8021as/Gptp.ned +++ b/src/inet/linklayer/ieee8021as/Gptp.ned @@ -4,6 +4,10 @@ // Peter Danielis // University of Rostock, Germany // +// Lucas Haug +// Lun-Yu Yuan +// Chunzhi Guo +// University of Stuttgart, Deterministic6G package inet.linklayer.ieee8021as; @@ -11,44 +15,85 @@ import inet.common.SimpleModule; import inet.linklayer.contract.IGptp; // -// Implements the IEEE 802.1as protocol also known as gPTP. It +// Implements the IEEE 802.1AS protocol also known as gPTP. It // measures link delays to neighboring gPTP network nodes periodically. The // slave and master ports specify where the connected gPTP network nodes // and their roles in the time synchronization domain. The time synchronization // is done periodically and the clock module is set. // +// It can either be statically configured, by setting the slavePort and masterPorts parameter. +// In case of a static configuration the gPTP node type is MASTER_NODE, BRIDGE_NODE or SLAVE_NODE. +// +// Alternatively the Best Master Clock Algorithm (BMCA) can be used to dynamically select a GM based on +// the BMCA parameters. +// In this case the gPTP node type is set to BMCA_NODE. +// All ports that should be used for BMCA must be set in the bmcaPorts parameter. +// +// The implementation also supports the HotStandby ammendment IEEE 802.1ASdm, see MultiDomainGptp. simple Gptp extends SimpleModule like IGptp { parameters: @class(Gptp); string clockModule = default(""); // Relative path of a module that implements IClock; optional string interfaceTableModule; // Relative path of the interface table module - string gptpNodeType; // @enum("GptpNodeType"): MASTER_NODE, BRIDGE_NODE, SLAVE_NODE + string gptpNodeType @enum("MASTER_NODE","BRIDGE_NODE","SLAVE_NODE","BMCA_NODE"); // @enum("GptpNodeType"): MASTER_NODE, BRIDGE_NODE, SLAVE_NODE, BMCA_NODE + bool useNrr = default(false); // Use neighbor rate ratio + + // How to calculate the gmRateRatio. NRR is the method used by the gPTP standard. + // DIRECT is another method previously used by the INET implementation. + // It calculates the gmRateRatio directly by using the preciseOriginTimestamp and correctionField + string gmRateRatioCalculationMethod @enum("NONE","NRR","DIRECT") = default("NONE"); + int domainNumber = default(0); // Specifies the time domain number used in gPTP messages - string slavePort = default(""); // Port for receiving time (empty for MASTER_NODE) - object masterPorts = default([]); // Ports for sending out time (empty for SLAVE_NODE) + string slavePort = default(""); // Port for receiving time (empty for MASTER_NODE and BMCA_NODE) + object masterPorts = default([]); // Ports for sending out time (empty for SLAVE_NODE and BMCA_NODE) + object bmcaPorts = default([]); // Ports for sending out time (only fill when BMCA_NODE is used) double correctionField @unit(s) = default(0s); // Time correction for link delay measurements double syncInterval @unit(s) = default(0.125s); // Time interval between SYNC messages double pdelayInterval @unit(s) = default(1s); // Frequency of link delay measurements + double announceInterval @unit(s) = default(1s); // Frequency of Announce messages (BMCA mode only) double syncInitialOffset @unit(s) = default(syncInterval); // Time of first SYNC message double pdelayInitialOffset @unit(s) = default(0s); // Time of first link delay measurement + double announceInitialOffset @unit(s) = default(0s); // Time of first Announce message (BMCA mode only) + double announceTimeout @unit(s) = default(announceInterval * 3); // Timeout for Announce messages (BMCA mode only) + double syncTimeout @unit(s) = default(syncInterval * 3); // When to assume we are out of sync + bool resetClockStateOnSyncLoss = default(true); // Resets the clock state to INIT after sync timeout + bool sendAnnounceImmediately = default(false); // Send Announce message immediately after entering executing BMCA (otherwise wait until next announceInterval) + + int grandmasterPriority1 @mutable = default(255); // Priority1 value for BMCA + int clockClass @mutable = default(248); // ClockClass value for BMCA + int clockAccuracy @mutable = default(254); // ClockAccuracy value for BMCA + int offsetScaledLogVariance @mutable = default(17258); // OffsetScaledLogVariance value for BMCA + int grandmasterPriority2 @mutable = default(248); // Priority2 value for BMCA // following parameters are used to schedule follow_up and pdelay_resp messages. // These numbers must be large enough to prevent creating a queue in the MAC layer. // It means they should be larger than the transmission time of the message sent before double pDelayReqProcessingTime @unit(s) = default(8us); // Processing time between the arrival of PDelayReq and the sending of PDelayResp - double followUpInterval @unit(s) = default(7us); @display("i=block/timer"); @signal[localTime](type=simtime_t); // As clocktime_t @signal[timeDifference](type=simtime_t); + @signal[gmRateRatio](type=double); @signal[rateRatio](type=double); + @signal[receivedRateRatio](type=double); + @signal[neighborRateRatio](type=double); @signal[peerDelay](type=simtime_t); + @signal[residenceTime](type=simtime_t); + @signal[correctionFieldIngress](type=simtime_t); + @signal[correctionFieldEgress](type=simtime_t); + @signal[gptpSyncStateChanged](type=int); @signal[packetDropped](type=inet::Packet); + @signal[gmId](type=int); @statistic[localTime](record=vector; interpolationmode=linear); @statistic[timeDifference](record=vector; interpolationmode=linear); @statistic[rateRatio](record=vector; interpolationmode=sample-hold); @statistic[peerDelay](record=vector; interpolationmode=sample-hold); + @statistic[residenceTime](record=vector;interpolationmode=none); + @statistic[correctionFieldIngress](record=vector; interpolationmode=none); + @statistic[correctionFieldEgress](record=vector; interpolationmode=none); + @statistic[gmId](record=vector; interpolationmode=sample-hold); + @statistic[gptpSyncStateChanged](record=vector; interpolationmode=sample-hold); @statistic[packetDropNotAddressedToUs](title="packet drop: not addressed to us"; source=packetDropReasonIsNotAddressedToUs(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @selfMessageKinds(inet::GptpSelfMsgKind); diff --git a/src/inet/linklayer/ieee8021as/GptpDomainClassifier.cc b/src/inet/linklayer/ieee8021as/GptpDomainClassifier.cc new file mode 100644 index 00000000000..286ceed1d3d --- /dev/null +++ b/src/inet/linklayer/ieee8021as/GptpDomainClassifier.cc @@ -0,0 +1,48 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#include "inet/linklayer/ieee8021as/GptpDomainClassifier.h" + +#include "Gptp.h" + +namespace inet { + +Define_Module(GptpDomainClassifier); + +void GptpDomainClassifier::initialize(int stage) +{ + PacketClassifierBase::initialize(stage); + if (stage == INITSTAGE_LINK_LAYER) { + // Iterate over all gates and find the ones that are connected to Gptp modules + for (int i = 0; i < gateSize("out"); i++) { + auto outGate = gate("out", i); + auto gptp = dynamic_cast(outGate->getPathEndGate()->getOwnerModule()); + if (gptp != nullptr) { + auto domainNumber = gptp->getDomainNumber(); + gptpDomainToGateIndex[domainNumber] = i; + } + } + } +} + +int GptpDomainClassifier::classifyPacket(Packet *packet) +{ + const auto& header = packet->peekAtFront(); + + if (gptpDomainToGateIndex.find(header->getDomainNumber()) == gptpDomainToGateIndex.end()) { + EV_ERROR << "Message " << packet->getFullName() << " arrived with foreign domainNumber " + << header->getDomainNumber() << ", dropped\n"; + // Frame should be dropped in the gPTP with a not addressed to us error + // There might be better ways to handle this error, but dropping this packet here is not possible + return 0; + } + + return gptpDomainToGateIndex[header->getDomainNumber()]; +} + +} // namespace inet + diff --git a/src/inet/linklayer/ieee8021as/GptpDomainClassifier.h b/src/inet/linklayer/ieee8021as/GptpDomainClassifier.h new file mode 100644 index 00000000000..17b13fdb49f --- /dev/null +++ b/src/inet/linklayer/ieee8021as/GptpDomainClassifier.h @@ -0,0 +1,28 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_GPTPDOMAINCLASSIFIER_H +#define __INET_GPTPDOMAINCLASSIFIER_H + +#include "inet/queueing/base/PacketClassifierBase.h" + +namespace inet { + +class INET_API GptpDomainClassifier : public queueing::PacketClassifierBase +{ + protected: + std::map gptpDomainToGateIndex; + + protected: + virtual void initialize(int stage) override; + virtual int classifyPacket(Packet *packet) override; +}; + +} // namespace inet + +#endif + diff --git a/src/inet/linklayer/ieee8021as/GptpDomainClassifier.ned b/src/inet/linklayer/ieee8021as/GptpDomainClassifier.ned new file mode 100644 index 00000000000..4e663887176 --- /dev/null +++ b/src/inet/linklayer/ieee8021as/GptpDomainClassifier.ned @@ -0,0 +1,20 @@ +// +// Copyright (C) 2020 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +package inet.linklayer.ieee8021as; + +import inet.queueing.base.PacketClassifierBase; +import inet.queueing.contract.IPacketClassifier; + +// +// This module classifies packets based on the domain number in the gPTP header +// +simple GptpDomainClassifier extends PacketClassifierBase like IPacketClassifier +{ + parameters: + @class(GptpDomainClassifier); +} diff --git a/src/inet/linklayer/ieee8021as/GptpEndstation.ned b/src/inet/linklayer/ieee8021as/GptpEndstation.ned index 896864f242c..698ff06b334 100644 --- a/src/inet/linklayer/ieee8021as/GptpEndstation.ned +++ b/src/inet/linklayer/ieee8021as/GptpEndstation.ned @@ -17,7 +17,7 @@ import inet.node.inet.StandardHost; module GptpEndstation extends StandardHost { parameters: - clock.typename = default("SettableClock"); + clock.typename = default("PiServoClock"); submodules: gptp: like IApp if typename != "" { @display("p=700.11755,75.166"); diff --git a/src/inet/linklayer/ieee8021as/GptpPacket.msg b/src/inet/linklayer/ieee8021as/GptpPacket.msg index cfcc05f84dc..e58021e5c52 100644 --- a/src/inet/linklayer/ieee8021as/GptpPacket.msg +++ b/src/inet/linklayer/ieee8021as/GptpPacket.msg @@ -10,6 +10,10 @@ // Peter Danielis // University of Rostock, Germany // +// Lucas Haug +// Lun-Yu Yuan +// Chunzhi Guo +// University of Stuttgart, Deterministic6G import inet.clock.common.ClockEvent; import inet.clock.contract.ClockTime; @@ -33,6 +37,7 @@ const B GPTP_FOLLOW_UP_PACKET_SIZE = GPTP_HEADER_SIZE + B(10) + GPTP_TLV_SIZE + const B GPTP_PDELAY_REQ_PACKET_SIZE = GPTP_HEADER_SIZE + B(20); const B GPTP_PDELAY_RESP_PACKET_SIZE = GPTP_HEADER_SIZE + B(20); const B GPTP_PDELAY_RESP_FOLLOW_UP_PACKET_SIZE = GPTP_HEADER_SIZE + B(20); +const B GPTP_ANNOUNCE_PACKET_SIZE = GPTP_HEADER_SIZE + B(30); } }} @@ -46,8 +51,15 @@ enum GptpNodeType MASTER_NODE = 11; BRIDGE_NODE = 12; SLAVE_NODE = 13; + BMCA_NODE = 14; } +enum GmRateRatioCalculationMethod { + NONE = 0; + NRR = 1; + DIRECT = 2; +}; + enum GptpPortType { MASTER_PORT = 2; @@ -62,13 +74,21 @@ enum GptpMessageType GPTPTYPE_PDELAY_REQ = 0x2; GPTPTYPE_PDELAY_RESP = 0x3; GPTPTYPE_PDELAY_RESP_FOLLOW_UP = 0xA; + GPTPTYPE_ANNOUNCE = 0xB; } enum GptpSelfMsgKind { GPTP_SELF_REQ_ANSWER_KIND = 101; - GPTP_SELF_MSG_SYNC = 103; - GPTP_REQUEST_TO_SEND_SYNC = 104; - GPTP_SELF_MSG_PDELAY_REQ = 105; + GPTP_SELF_MSG_SYNC = 102; + GPTP_SELF_MSG_PDELAY_REQ = 103; + GPTP_SELF_MSG_ANNOUNCE = 104; + GPTP_SELF_MSG_ANNOUNCE_TIMEOUT = 105; + GPTP_SELF_MSG_SYNC_TIMEOUT = 106; +} + +enum SyncState { + UNSYNCED = 0; + SYNCED = 1; } // ieee802.1AS-2020 10.6.2.2.8: flags (Octet2) @@ -108,12 +128,35 @@ struct PortIdentity uint16_t portNumber; }; -//struct ClockQuality -//{ -// UInteger8 clockClass; -// Enumeration8 clockAccuracy; -// UInteger16 offsetScaledLogVariance; -//}; +cplusplus(PortIdentity) {{ + bool operator==(const PortIdentity& rhs) const { + return clockIdentity == rhs.clockIdentity && + portNumber == rhs.portNumber; + } + + bool operator!=(const PortIdentity& rhs) const { + return !(*this == rhs); + } + + bool operator<(const PortIdentity& rhs) const { + if (clockIdentity < rhs.clockIdentity) return true; + if (clockIdentity > rhs.clockIdentity) return false; + return portNumber < rhs.portNumber; + } + + bool operator>(const PortIdentity& rhs) const { + return rhs < *this; + } + + bool operator<=(const PortIdentity& rhs) const { + return !(rhs < *this); + } + + bool operator>=(const PortIdentity& rhs) const { + return !(*this < rhs); + } +}} + message GptpReqAnswerEvent extends ClockEvent { @@ -244,6 +287,108 @@ class GptpPdelayRespFollowUp extends GptpBase PortIdentity requestingPortIdentity; } +struct ClockQuality +{ + @packetData; + uint8_t clockClass; + uint8_t clockAccuracy; + uint16_t offsetScaledLogVariance; +}; + +cplusplus(ClockQuality) {{ + bool operator==(const ClockQuality& rhs) const { + return clockClass == rhs.clockClass && + clockAccuracy == rhs.clockAccuracy && + offsetScaledLogVariance == rhs.offsetScaledLogVariance; + } + + bool operator!=(const ClockQuality& rhs) const { + return !(*this == rhs); + } + + bool operator<(const ClockQuality& rhs) const { + if (clockClass < rhs.clockClass) return true; + if (clockClass > rhs.clockClass) return false; + if (clockAccuracy < rhs.clockAccuracy) return true; + if (clockAccuracy > rhs.clockAccuracy) return false; + return offsetScaledLogVariance < rhs.offsetScaledLogVariance; + } + + bool operator>(const ClockQuality& rhs) const { + return rhs < *this; + } + + bool operator<=(const ClockQuality& rhs) const { + return !(rhs < *this); + } + + bool operator>=(const ClockQuality& rhs) const { + return !(*this < rhs); + } +}} + +struct BmcaPriorityVector { + @packetData; + uint8_t grandmasterPriority1; + ClockQuality grandmasterClockQuality; + uint8_t grandmasterPriority2; + uint64_t grandmasterIdentity; + uint16_t stepsRemoved; +} + +cplusplus(BmcaPriorityVector) {{ + // Important: These comparison functions only compare the values as in IEEE 1588-2019 Figure 34 + // For a full selection made by BCMA tiebreakers need to be implemented as in Figure 35 + + bool operator==(const BmcaPriorityVector& rhs) const { + return grandmasterPriority1 == rhs.grandmasterPriority1 && + grandmasterClockQuality == rhs.grandmasterClockQuality && + grandmasterPriority2 == rhs.grandmasterPriority2 && + grandmasterIdentity == rhs.grandmasterIdentity && + stepsRemoved == rhs.stepsRemoved; + } + + bool operator!=(const BmcaPriorityVector& rhs) const { + return !(*this == rhs); + } + + bool operator<(const BmcaPriorityVector& rhs) const { + if (grandmasterPriority1 < rhs.grandmasterPriority1) return true; + if (grandmasterPriority1 > rhs.grandmasterPriority1) return false; + if (grandmasterClockQuality < rhs.grandmasterClockQuality) return true; + if (grandmasterClockQuality > rhs.grandmasterClockQuality) return false; + if (grandmasterPriority2 < rhs.grandmasterPriority2) return true; + if (grandmasterPriority2 > rhs.grandmasterPriority2) return false; + if (grandmasterIdentity < rhs.grandmasterIdentity) return true; + if (grandmasterIdentity > rhs.grandmasterIdentity) return false; + return stepsRemoved < rhs.stepsRemoved; + } + + bool operator>(const BmcaPriorityVector& rhs) const { + return rhs < *this; + } + + bool operator<=(const BmcaPriorityVector& rhs) const { + return !(rhs < *this); + } + + bool operator>=(const BmcaPriorityVector& rhs) const { + return !(*this < rhs); + } +}} + +// IEEE 1588-2019 Table 43 +class GptpAnnounce extends GptpBase { + messageType = GPTPTYPE_ANNOUNCE; + chunkLength = GPTP_ANNOUNCE_PACKET_SIZE; + messageLengthField = GPTP_ANNOUNCE_PACKET_SIZE.get(); + clocktime_t originTimestamp; + int16_t currentUtcOffset; // Not important for simulation purposes right now + uint8_t reserved; + BmcaPriorityVector priorityVector; + uint8_t timeSource; // Not important for simulation purposes right now +} + class GptpIngressTimeInd extends TagBase { clocktime_t arrivalClockTime; diff --git a/src/inet/linklayer/ieee8021as/GptpPacketClassifierFunction.cc b/src/inet/linklayer/ieee8021as/GptpPacketClassifierFunction.cc deleted file mode 100644 index 6fd1f9a1de3..00000000000 --- a/src/inet/linklayer/ieee8021as/GptpPacketClassifierFunction.cc +++ /dev/null @@ -1,22 +0,0 @@ -// -// Copyright (C) 2020 OpenSim Ltd. -// -// SPDX-License-Identifier: LGPL-3.0-or-later -// - - -#include "inet/linklayer/ieee8021as/GptpPacket_m.h" -#include "inet/queueing/function/PacketClassifierFunction.h" - -namespace inet { - -static int classifyPacketByGptpDomainNumber(Packet *packet) -{ - const auto& header = packet->peekAtFront(); - return header->getDomainNumber(); -} - -Register_Packet_Classifier_Function(GptpDomainNumberClassifier, classifyPacketByGptpDomainNumber); - -} // namespace inet - diff --git a/src/inet/linklayer/ieee8021as/HotStandby.cc b/src/inet/linklayer/ieee8021as/HotStandby.cc new file mode 100644 index 00000000000..29beb0367ed --- /dev/null +++ b/src/inet/linklayer/ieee8021as/HotStandby.cc @@ -0,0 +1,95 @@ +// +// Copyright (C) 2018 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "HotStandby.h" + +#include "Gptp.h" + +namespace inet { + +Define_Module(HotStandby); + +// TODO: Fix: Domain mumber isn't neccessarily the same as the clock index +void HotStandby::initialize(int stage) +{ + ClockUserModuleBase::initialize(stage); + if (stage == INITSTAGE_LINK_LAYER) { + auto parent = getParentModule(); + auto numDomains = parent->par("numDomains").intValue(); + + multiClock = check_and_cast(clock.get()); + + for (int i = 0; i < numDomains; i++) { + auto gptp = check_and_cast(parent->getSubmodule("domain", i)); + auto domainNumber = gptp->getDomainNumber(); + gptp->subscribe(Gptp::gptpSyncStateChanged, this); + ModuleRefByPar currentClock; + currentClock.reference(gptp, "clockModule", true); + // Check if the currentClock is a ClockBase + // (otherwise it could be a link to the multiClock or an external) + // In that case we just ignore this clock in the HotStandby context + if (dynamic_cast(currentClock.get()) == nullptr) { + EV_WARN << "Clock module " << currentClock->getFullPath() << " for domain " << domainNumber + << "is not a ClockBase, ignoring" << endl; + continue; + } + auto clockBase = check_and_cast(currentClock.get()); + auto clockParent = clockBase->getParentModule(); + if (clockParent != multiClock) { + EV_WARN + << "Clock module is not a child of MultiClock, cannot determine clock index and ignoring this clock" + << endl; + } + else { + domainNumberToClockIndex[domainNumber] = clockBase->getIndex(); + } + } + } +} + +void HotStandby::receiveSignal(cComponent *source, simsignal_t simSignal, const intval_t t, cObject *details) +{ + Enter_Method("%s", cComponent::getSignalName(simSignal)); + if (simSignal == Gptp::gptpSyncStateChanged) { + auto gptp = check_and_cast(source); + auto syncState = static_cast(t); + handleSyncStateChanged(gptp, syncState); + } +} + +void HotStandby::handleSyncStateChanged(const Gptp *gptp, const SyncState syncState) +{ + // store sync state into array + auto domainNumber = gptp->getDomainNumber(); + syncStates[domainNumber] = syncState; + + auto currentActiveIndex = multiClock->par("activeClockIndex").intValue(); + // TODO: We assume lower domain is always preferable + // In the future, domain preference should be configurable + if (domainNumber > currentActiveIndex || (domainNumber == currentActiveIndex && syncState == SYNCED)) { + // if the domain is higher than the current active domain, ignore it + return; + } + + int bestDomainNumber = std::numeric_limits::max(); + for (auto &entry : syncStates) { + if (entry.second == SYNCED && entry.first < bestDomainNumber) { + bestDomainNumber = entry.first; + } + } + + if (bestDomainNumber == std::numeric_limits::max()) { + EV_WARN << "All domains out of sync, cannot switch to backup domain" << endl; + return; + } + + auto clockIndex = domainNumberToClockIndex[bestDomainNumber]; + + EV_INFO << "Switching to domain " << bestDomainNumber << " - clock index " << clockIndex << endl; + multiClock->par("activeClockIndex").setIntValue(clockIndex); +} + +} // namespace inet diff --git a/src/inet/linklayer/ieee8021as/HotStandby.h b/src/inet/linklayer/ieee8021as/HotStandby.h new file mode 100644 index 00000000000..a576282b8d0 --- /dev/null +++ b/src/inet/linklayer/ieee8021as/HotStandby.h @@ -0,0 +1,35 @@ +// +// Copyright (C) 2018 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_HOTSTANDBY_H +#define __INET_HOTSTANDBY_H + +#include + +#include "inet/clock/model/MultiClock.h" +#include "inet/common/clock/ClockUserModuleBase.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee8021as/Gptp.h" + +namespace inet { + +class INET_API HotStandby : public ClockUserModuleBase, public cListener +{ + protected: + std::map syncStates; + std::map domainNumberToClockIndex; + MultiClock *multiClock = nullptr; + + protected: + virtual void initialize(int stage) override; + virtual void receiveSignal(cComponent *source, simsignal_t simSignal, intval_t t, cObject *details) override; + void handleSyncStateChanged(const Gptp *gptp, SyncState syncState); + virtual int numInitStages() const override { return NUM_INIT_STAGES; }; +}; + +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee8021as/HotStandy.ned b/src/inet/linklayer/ieee8021as/HotStandy.ned new file mode 100644 index 00000000000..9d83f9cb27f --- /dev/null +++ b/src/inet/linklayer/ieee8021as/HotStandy.ned @@ -0,0 +1,29 @@ +// +// @authors: Lucas Haug +// Lun-Yu Yuan +// Chunzhi Guo +// University of Stuttgart, Deterministic6G + +package inet.linklayer.ieee8021as; + +import inet.common.SimpleModule; + + +// This module is a basic implementation of the IEEE 802.1ASdm HotStandby amendment. +// It is part of the MultiDomainGptp module and keeps track of the synchronization state of contained +// gPTP modules. +// +// In case one gPTP module loses synchronization, it will switch the activeClockIndex of the multiClock +// to the clock of a gPTP instance still in sync. +// +// Note: This module is not a complete implementation of the HotStandby amendment and only provides +// the basic functionality of switching the active clock if one gPTP instance loses sync. +// For example, a smooth transition when switching between synchronization instances is not implemented. +simple HotStandby extends SimpleModule +{ + parameters: + @display("i=block/blackboard"); + @class(HotStandby); + @selfMessageKinds(inet::GptpSelfMsgKind); + string clockModule = default(""); // Path to a multiClock +} diff --git a/src/inet/linklayer/ieee8021as/MultiDomainGptp.ned b/src/inet/linklayer/ieee8021as/MultiDomainGptp.ned index 4943bb6d356..894e870aa6c 100644 --- a/src/inet/linklayer/ieee8021as/MultiDomainGptp.ned +++ b/src/inet/linklayer/ieee8021as/MultiDomainGptp.ned @@ -8,8 +8,9 @@ package inet.linklayer.ieee8021as; import inet.linklayer.contract.IGptp; -import inet.queueing.classifier.PacketClassifier; import inet.queueing.common.PacketMultiplexer; +import inet.queueing.contract.IPacketClassifier; + // // Combines multiple ~Gptp modules, one per time domain into a multi @@ -17,7 +18,10 @@ import inet.queueing.common.PacketMultiplexer; // configured to use the corresponding subclock of the clock passed into this // module. // +// The module also provides a basic implementation of the IEEE 802.1ASdm ~HotStandby amendment +// // @see ~MultiClock +// @see ~HotStandby // module MultiDomainGptp like IGptp { @@ -26,6 +30,7 @@ module MultiDomainGptp like IGptp string interfaceTableModule; // Relative module path of the interface table int numDomains; // Number of time synchronization domains string gptpNodeType; // @enum("GptpNodeType"): MASTER_NODE, BRIDGE_NODE, SLAVE_NODE + bool hasHotStandby = default(true); // Defines if the HotStandby should be used. @display("i=block/app"); gates: input socketIn; @@ -41,13 +46,16 @@ module MultiDomainGptp like IGptp multiplexer: PacketMultiplexer { @display("p=150,350"); } - classifier: PacketClassifier { - classifierClass = default("inet::GptpDomainNumberClassifier"); + classifier: like IPacketClassifier { @display("p=300,350"); } + hotstandby: HotStandby if hasHotStandby { + clockModule = default(absPath(parent.clockModule)); + @display("p=450,350"); + } + connections: - for i=0..numDomains-1 - { + for i=0..numDomains-1 { domain[i].socketOut --> multiplexer.in++; classifier.out++ --> domain[i].socketIn; } diff --git a/src/inet/node/ethernet/EthernetSwitch.ned b/src/inet/node/ethernet/EthernetSwitch.ned index b357269a1f9..c0a3fb6d754 100644 --- a/src/inet/node/ethernet/EthernetSwitch.ned +++ b/src/inet/node/ethernet/EthernetSwitch.ned @@ -91,7 +91,7 @@ module EthernetSwitch like IEthernetNetworkNode status: NodeStatus if hasStatus { @display("p=100,400;is=s"); } - clock: like IClock if typename != "" { + clock: like IClock if typename != "" { @display("p=100,500;is=s"); } pcapRecorder[numPcapRecorders]: PcapRecorder { diff --git a/src/inet/node/tsn/TsnClock.ned b/src/inet/node/tsn/TsnClock.ned index 8576f0acbc2..72070805c41 100644 --- a/src/inet/node/tsn/TsnClock.ned +++ b/src/inet/node/tsn/TsnClock.ned @@ -31,6 +31,6 @@ module TsnClock extends GptpMaster clock.typename = default("OscillatorBasedClock"); // Master clocks cannot be set ethernet.typename = default("EthernetLayer"); // Use the Ethernet protocol layer outside network interfaces eth[*].typename = default("LayeredEthernetInterface"); // Switch to modular Ethernet interface - eth[*].phyLayer.typename = default(hasCutthroughSwitching ? "EthernetStreamingPhyLayer" : "EthernetPhyLayer"); // Use packet streaming when cut-through switching is enabled + eth[*].phyLayer.typename = default("EthernetStreamingPhyLayer"); // We need packet streaming to use the receptionStartedSignal @display("i=device/card"); // Change icon to emphasize hardware device } diff --git a/src/inet/node/tsn/TsnDevice.ned b/src/inet/node/tsn/TsnDevice.ned index 6411a9a9843..487819b4aea 100644 --- a/src/inet/node/tsn/TsnDevice.ned +++ b/src/inet/node/tsn/TsnDevice.ned @@ -45,12 +45,12 @@ module TsnDevice extends StandardHost bool hasFramePreemption = default(false); // Enable IEEE 802.1 Qbu frame preemption bool hasCutthroughSwitching = default(false); // Enable cut-through switching support bool hasBridging = default(hasIncomingStreams || hasOutgoingStreams || hasStreamRedundancy || hasIngressTrafficFiltering || hasEgressTrafficFiltering); - clock.typename = default(hasTimeSynchronization ? "SettableClock" : ""); // Enable explicit local clock model + clock.typename = default(hasTimeSynchronization ? "PiServoClock" : ""); // Enable explicit local clock model ethernet.typename = default("EthernetLayer"); // Use Ethernet protocol layer outside of network interfaces eth[*].typename = default("LayeredEthernetInterface"); // Switch to modular Ethernet interface eth[*].macLayer.typename = default(hasFramePreemption ? "EthernetPreemptingMacLayer" : "EthernetMacLayer"); eth[*].macLayer.queue.typename = default(hasEgressTrafficShaping ? "Ieee8021qTimeAwareShaper" : (hasFramePreemption ? "" : "PacketQueue")); // Use priority queue having multiple subqueues controlled by separate gates - eth[*].phyLayer.typename = default(hasCutthroughSwitching ? "EthernetStreamingPhyLayer" : (hasFramePreemption ? "EthernetPreemptingPhyLayer" : "EthernetPhyLayer")); // Use packet streaming when cut-through switching is enabled + eth[*].phyLayer.typename = default(hasCutthroughSwitching ? "EthernetStreamThroughPhyLayer" : (hasFramePreemption ? "EthernetPreemptingPhyLayer" : (hasTimeSynchronization ? "EthernetStreamingPhyLayer" : "EthernetPhyLayer"))); // use packet streaming when cut-through switching is enabled bridging.typename = default(hasBridging ? "BridgingLayer" : ""); // Switch to modular bridging bridging.interfaceRelay.typename = default(""); // Disable frame relaying bridging.streamIdentifier.typename = default(hasOutgoingStreams || hasStreamRedundancy ? "StreamIdentifierLayer" : ""); // Enable stream identification when stream redundancy is enabled diff --git a/src/inet/node/tsn/TsnSwitch.ned b/src/inet/node/tsn/TsnSwitch.ned index cc59f674194..b29a3fff0c6 100644 --- a/src/inet/node/tsn/TsnSwitch.ned +++ b/src/inet/node/tsn/TsnSwitch.ned @@ -44,12 +44,12 @@ module TsnSwitch extends EthernetSwitch hasGptp = default(hasTimeSynchronization); // Enable gPTP protocol gptp.gptpNodeType = default("BRIDGE_NODE"); // Configure gPTP bridge node type gptp.slavePort = default("eth0"); // Configure default gPTP bridge slave port - clock.typename = default(hasTimeSynchronization ? "SettableClock" : ""); // Enable explicit local clock model when time synchronization is enabled + clock.typename = default(hasTimeSynchronization ? "PiServoClock" : ""); // Enable explicit local clock model when time synchronization is enabled ethernet.typename = default("EthernetLayer"); // Use Ethernet protocol layer outside of network interfaces eth[*].typename = default(hasCutthroughSwitching ? "EthernetCutthroughInterface" : "LayeredEthernetInterface"); // Switch to modular Ethernet interface eth[*].macLayer.typename = default(hasFramePreemption ? "EthernetPreemptingMacLayer" : "EthernetMacLayer"); eth[*].macLayer.queue.typename = default(hasEgressTrafficShaping ? "Ieee8021qTimeAwareShaper" : "PacketQueue"); // Use compound priority queue having multiple subqueues controlled by separate gates when egress traffic shaping is enabled - eth[*].phyLayer.typename = default(hasCutthroughSwitching ? "EthernetStreamThroughPhyLayer" : (hasFramePreemption ? "EthernetPreemptingPhyLayer" : "EthernetPhyLayer")); // Use packet streaming when cut-through switching is enabled + eth[*].phyLayer.typename = default(hasCutthroughSwitching ? "EthernetStreamThroughPhyLayer" : (hasFramePreemption ? "EthernetPreemptingPhyLayer" : (hasTimeSynchronization ? "EthernetStreamingPhyLayer" : "EthernetPhyLayer"))); // Use packet streaming when cut-through switching is enabled bridging.typename = default("BridgingLayer"); // Switch to modular bridging bridging.directionReverser.cutthroughBarrier.typename = default(hasCutthroughSwitching ? "EthernetCutthroughBarrier" : ""); // Enable cut-through barrier when cut-through switching is enabled bridging.streamIdentifier.typename = default(hasOutgoingStreams || hasStreamRedundancy ? "StreamIdentifierLayer" : ""); // Enable stream identification when stream redundancy is enabled diff --git a/src/inet/protocolelement/transceiver/base/PacketTransmitterBase.cc b/src/inet/protocolelement/transceiver/base/PacketTransmitterBase.cc index c7b52fee95c..c2930cfe89f 100644 --- a/src/inet/protocolelement/transceiver/base/PacketTransmitterBase.cc +++ b/src/inet/protocolelement/transceiver/base/PacketTransmitterBase.cc @@ -4,13 +4,12 @@ // SPDX-License-Identifier: LGPL-3.0-or-later // - #include "inet/protocolelement/transceiver/base/PacketTransmitterBase.h" -#include "inet/common/ModuleAccess.h" #include "inet/common/PacketEventTag.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/TimeTag.h" +#include "inet/linklayer/ieee8021as/GptpPacket_m.h" namespace inet {