Files
m241Neelima/components/ip_net2-6.9/ipptp/src/bmc.c
T
2026-07-30 11:19:44 +05:30

743 lines
23 KiB
C

/*-
* Copyright (c) 2016 Wind River Systems, Inc,
* Copyright (c) 2011-2012 George V. Neville-Neil,
* Steven Kreuzer,
* Martin Burnicki,
* Jan Breuer,
* Gael Mace,
* Alexandre Van Kempen,
* Inaqui Delgado,
* Rick Ratzel,
* National Instruments.
* Copyright (c) 2009-2010 George V. Neville-Neil,
* Steven Kreuzer,
* Martin Burnicki,
* Jan Breuer,
* Gael Mace,
* Alexandre Van Kempen
*
* Copyright (c) 2005-2008 Kendall Correll, Aidan Williams
*
* All Rights Reserved
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
modification history
--------------------
13jun16,xxy Add PTP one-step hardware Tx timeStamp
02feb16,h_l Init version.
*/
/**
* @file bmc.c
* @date Wed Jun 23 09:36:09 2010
*
* @brief Best master clock selection code.
*
* The functions in this file are used by the daemon to select the
* best master clock from any number of possibilities.
*/
#include "ptpd.h"
/* Init ptpClock with run time values (initialization constants are in constants.h)*/
void initData(RunTimeOpts *rtOpts, PtpClock *ptpClock)
{
int i, j;
j = 0;
DBG("initData\n");
/* Default data set */
ptpClock->twoStepFlag = TWO_STEP_FLAG;
if (rtOpts->oneStepMode == TRUE)
{
ptpClock->twoStepFlag = FALSE; /*use one step mode*/
}
/*
* init clockIdentity with MAC address and 0xFF and 0xFE. see
* spec 7.5.2.2.2
*/
for (i = 0; i < CLOCK_IDENTITY_LENGTH; i++)
{
if (i == 3)
{
ptpClock->clockIdentity[i] = (Octet)0xFF;
}
else if (i == 4)
{
ptpClock->clockIdentity[i] = (Octet)0xFE;
}
else
{
ptpClock->clockIdentity[i] = ptpClock->netPath.interfaceID[j];
j++;
}
}
ptpClock->numberPorts = NUMBER_PORTS;
ptpClock->clockQuality.clockAccuracy =
rtOpts->clockQuality.clockAccuracy;
ptpClock->clockQuality.clockClass = rtOpts->clockQuality.clockClass;
ptpClock->clockQuality.offsetScaledLogVariance =
rtOpts->clockQuality.offsetScaledLogVariance;
ptpClock->priority1 = rtOpts->priority1;
ptpClock->priority2 = rtOpts->priority2;
ptpClock->domainNumber = rtOpts->domainNumber;
if (rtOpts->slaveOnly)
{
ptpClock->slaveOnly = TRUE;
rtOpts->clockQuality.clockClass = SLAVE_ONLY_CLOCK_CLASS;
}
/* Port configuration data set */
/*
* PortIdentity Init (portNumber = 1 for an ardinary clock spec
* 7.5.2.3)
*/
copyClockIdentity(ptpClock->portIdentity.clockIdentity,
ptpClock->clockIdentity);
ptpClock->portIdentity.portNumber = NUMBER_PORTS;
/* select the initial rate of delayreqs until we receive the first announce message */
ptpClock->logMinDelayReqInterval = (Integer8)rtOpts->initial_delayreq;
clearTime(&ptpClock->peerMeanPathDelay);
ptpClock->logAnnounceInterval = rtOpts->announceInterval;
ptpClock->announceReceiptTimeout = rtOpts->announceReceiptTimeout;
ptpClock->logSyncInterval = rtOpts->syncInterval;
ptpClock->delayMechanism = rtOpts->delayMechanism;
ptpClock->logMinPdelayReqInterval = rtOpts->logMinPdelayReqInterval;
ptpClock->versionNumber = VERSION_PTP;
/*
* Initialize random number generator using same method as ptpv1:
* seed is now initialized from the last bytes of our mac addres (collected in net.c:findIface())
*/
srand((ptpClock->netPath.interfaceID[PTP_UUID_LENGTH - 1] << 8) +
ptpClock->netPath.interfaceID[PTP_UUID_LENGTH - 2]);
/*Init other stuff*/
ptpClock->number_foreign_records = 0;
ptpClock->max_foreign_records = rtOpts->max_foreign_records;
}
/*Local clock is becoming Master. Table 13 (9.3.5) of the spec.*/
void m1(const RunTimeOpts *rtOpts, PtpClock *ptpClock)
{
/*Current data set update*/
ptpClock->stepsRemoved = 0;
clearTime(&ptpClock->offsetFromMaster);
clearTime(&ptpClock->meanPathDelay);
copyClockIdentity(ptpClock->parentPortIdentity.clockIdentity,
ptpClock->clockIdentity);
ptpClock->parentPortIdentity.portNumber = ptpClock->numberPorts;
ptpClock->parentStats = DEFAULT_PARENTS_STATS;
ptpClock->observedParentClockPhaseChangeRate = 0;
ptpClock->observedParentOffsetScaledLogVariance = 0;
copyClockIdentity(ptpClock->grandmasterIdentity,
ptpClock->clockIdentity);
ptpClock->grandmasterClockQuality.clockAccuracy =
ptpClock->clockQuality.clockAccuracy;
ptpClock->grandmasterClockQuality.clockClass =
ptpClock->clockQuality.clockClass;
ptpClock->grandmasterClockQuality.offsetScaledLogVariance =
ptpClock->clockQuality.offsetScaledLogVariance;
ptpClock->grandmasterPriority1 = ptpClock->priority1;
ptpClock->grandmasterPriority2 = ptpClock->priority2;
ptpClock->logMinDelayReqInterval = (Integer8)rtOpts->subsequent_delayreq;
/*Time Properties data set*/
ptpClock->timePropertiesDS.currentUtcOffsetValid = rtOpts->timeProperties.currentUtcOffsetValid;
ptpClock->timePropertiesDS.currentUtcOffset = rtOpts->timeProperties.currentUtcOffset;
ptpClock->timePropertiesDS.timeTraceable = rtOpts->timeProperties.timeTraceable;
ptpClock->timePropertiesDS.frequencyTraceable = rtOpts->timeProperties.frequencyTraceable;
ptpClock->timePropertiesDS.ptpTimescale = rtOpts->timeProperties.ptpTimescale;
ptpClock->timePropertiesDS.timeSource = rtOpts->timeProperties.timeSource;
}
/* first cut on a passive mode specific BMC actions */
void p1(PtpClock *ptpClock, const RunTimeOpts *rtOpts)
{
/* make sure we revert to ARB timescale in Passive mode*/
if (ptpClock->portState == PTP_PASSIVE)
{
ptpClock->timePropertiesDS.currentUtcOffsetValid = rtOpts->timeProperties.currentUtcOffsetValid;
ptpClock->timePropertiesDS.currentUtcOffset = rtOpts->timeProperties.currentUtcOffset;
}
}
/*Local clock is synchronized to Ebest Table 16 (9.3.5) of the spec*/
void s1(MsgHeader *header, MsgAnnounce *announce, PtpClock *ptpClock, const RunTimeOpts *rtOpts)
{
Boolean previousLeap59 = FALSE, previousLeap61 = FALSE;
Integer16 previousUtcOffset = 0;
if (ptpClock->portState == PTP_SLAVE || ptpClock->portState == PTP_PASSIVE)
{
previousLeap59 = ptpClock->timePropertiesDS.leap59;
previousLeap61 = ptpClock->timePropertiesDS.leap61;
previousUtcOffset = ptpClock->timePropertiesDS.currentUtcOffset;
}
/* Current DS */
ptpClock->stepsRemoved = (UInteger16)(announce->stepsRemoved + 1);
/* Parent DS */
copyClockIdentity(ptpClock->parentPortIdentity.clockIdentity,
header->sourcePortIdentity.clockIdentity);
ptpClock->parentPortIdentity.portNumber =
header->sourcePortIdentity.portNumber;
copyClockIdentity(ptpClock->grandmasterIdentity,
announce->grandmasterIdentity);
ptpClock->grandmasterClockQuality.clockAccuracy =
announce->grandmasterClockQuality.clockAccuracy;
ptpClock->grandmasterClockQuality.clockClass =
announce->grandmasterClockQuality.clockClass;
ptpClock->grandmasterClockQuality.offsetScaledLogVariance =
announce->grandmasterClockQuality.offsetScaledLogVariance;
ptpClock->grandmasterPriority1 = announce->grandmasterPriority1;
ptpClock->grandmasterPriority2 = announce->grandmasterPriority2;
ptpClock->logAnnounceInterval = header->logMessageInterval;
/* Timeproperties DS */
ptpClock->timePropertiesDS.currentUtcOffset = announce->currentUtcOffset;
if (ptpClock->portState != PTP_PASSIVE && ptpClock->timePropertiesDS.currentUtcOffsetValid &&
!IS_SET(header->flagField1, UTCV))
{
if (rtOpts->alwaysRespectUtcOffset)
{
WARNING("UTC Offset no longer valid and ptpengine:always_respect_utc_offset is set: continuing as normal\n");
}
else
{
WARNING("UTC Offset no longer valid - clock jump expected\n");
}
}
/* "Valid" is bit 2 in second octet of flagfield */
ptpClock->timePropertiesDS.currentUtcOffsetValid = IS_SET(header->flagField1, UTCV);
/* set PTP_PASSIVE-specific state */
p1(ptpClock, rtOpts);
/* only set leap state in slave mode */
if (ptpClock->portState == PTP_SLAVE)
{
ptpClock->timePropertiesDS.leap59 = IS_SET(header->flagField1, LI59);
ptpClock->timePropertiesDS.leap61 = IS_SET(header->flagField1, LI61);
}
ptpClock->timePropertiesDS.timeTraceable = IS_SET(header->flagField1, TTRA);
ptpClock->timePropertiesDS.frequencyTraceable = IS_SET(header->flagField1, FTRA);
ptpClock->timePropertiesDS.ptpTimescale = IS_SET(header->flagField1, PTPT);
ptpClock->timePropertiesDS.timeSource = announce->timeSource;
/* Leap second handling */
if (ptpClock->portState == PTP_SLAVE)
{
if (ptpClock->timePropertiesDS.leap59 && ptpClock->timePropertiesDS.leap61)
{
DBG("Both Leap59 and Leap61 flags set!\n");
ptpClock->counters.protocolErrors++;
return;
}
/* one of the leap second flags has suddenly been unset */
if (ptpClock->leapSecondPending &&
!ptpClock->leapSecondInProgress &&
((previousLeap59 != ptpClock->timePropertiesDS.leap59) ||
(previousLeap61 != ptpClock->timePropertiesDS.leap61)))
{
WARNING("Leap second event aborted by GM!");
ptpClock->leapSecondPending = FALSE;
ptpClock->leapSecondInProgress = FALSE;
timerStop(LEAP_SECOND_PAUSE_TIMER, ptpClock->itimer);
#ifdef HAVE_SYS_TIMEX_H
unsetTimexFlags(STA_INS | STA_DEL, TRUE);
#endif /* HAVE_SYS_TIMEX_H */
}
/*
* one of the leap second flags has been set
* or flags are lit but we have no event pending
*/
if ( (ptpClock->timePropertiesDS.leap59 || ptpClock->timePropertiesDS.leap61) && (
(!ptpClock->leapSecondPending &&
!ptpClock->leapSecondInProgress ) ||
((!previousLeap59 && ptpClock->timePropertiesDS.leap59) ||
(!previousLeap61 && ptpClock->timePropertiesDS.leap61))))
{
#ifdef HAVE_SYS_TIMEX_H
WARNING("Leap second pending! Setting kernel to %s "
"one second at midnight\n",
ptpClock->timePropertiesDS.leap61 ? "add" : "delete");
if (!checkTimexFlags(ptpClock->timePropertiesDS.leap61 ? STA_INS : STA_DEL))
{
unsetTimexFlags(ptpClock->timePropertiesDS.leap61 ? STA_DEL : STA_INS,
TRUE);
setTimexFlags(ptpClock->timePropertiesDS.leap61 ? STA_INS : STA_DEL,
FALSE);
}
#else
WARNING("Leap second pending! No kernel leap second "
"API support - expect a clock jump at "
"midnight!\n");
#endif /* HAVE_SYS_TIMEX_H */
/* only set the flag, the rest happens in doState() */
ptpClock->leapSecondPending = TRUE;
}
if ((previousUtcOffset != ptpClock->timePropertiesDS.currentUtcOffset) &&
!ptpClock->leapSecondPending &&
!ptpClock->leapSecondInProgress )
{
WARNING("UTC offset changed from %d to %d with "
"no leap second pending!\n",
previousUtcOffset, ptpClock->timePropertiesDS.currentUtcOffset);
}
else if ( previousUtcOffset != ptpClock->timePropertiesDS.currentUtcOffset)
{
WARNING("UTC offset changed from %d to %d\n",
previousUtcOffset, ptpClock->timePropertiesDS.currentUtcOffset);
}
}
}
/*Copy local data set into header and announce message. 9.3.4 table 12*/
static void
copyD0(MsgHeader *header, MsgAnnounce *announce, PtpClock *ptpClock)
{
announce->grandmasterPriority1 = ptpClock->priority1;
copyClockIdentity(announce->grandmasterIdentity,
ptpClock->clockIdentity);
announce->grandmasterClockQuality.clockClass =
ptpClock->clockQuality.clockClass;
announce->grandmasterClockQuality.clockAccuracy =
ptpClock->clockQuality.clockAccuracy;
announce->grandmasterClockQuality.offsetScaledLogVariance =
ptpClock->clockQuality.offsetScaledLogVariance;
announce->grandmasterPriority2 = ptpClock->priority2;
announce->stepsRemoved = 0;
copyClockIdentity(header->sourcePortIdentity.clockIdentity,
ptpClock->clockIdentity);
/* Copy TimePropertiesDS into FlagField1 */
header->flagField1 = (Octet)(ptpClock->timePropertiesDS.leap61 << 0);
header->flagField1 |= (Octet)(ptpClock->timePropertiesDS.leap59 << 1);
header->flagField1 |= (Octet)(ptpClock->timePropertiesDS.currentUtcOffsetValid << 2);
header->flagField1 |= (Octet)(ptpClock->timePropertiesDS.ptpTimescale << 3);
header->flagField1 |= (Octet)(ptpClock->timePropertiesDS.timeTraceable << 4);
header->flagField1 |= (Octet)(ptpClock->timePropertiesDS.frequencyTraceable << 5);
}
/*Data set comparison bewteen two foreign masters (9.3.4 fig 27)
* return similar to memcmp() */
static Integer8
bmcDataSetComparison(const MsgHeader *headerA, const MsgAnnounce *announceA,
const MsgHeader *headerB, const MsgAnnounce *announceB,
const PtpClock *ptpClock, const RunTimeOpts *rtOpts)
{
DBGV("Data set comparison \n");
int comp = 0;
/*Identity comparison*/
comp = memcmp(announceA->grandmasterIdentity, announceB->grandmasterIdentity, CLOCK_IDENTITY_LENGTH);
if (comp != 0)
{
goto dataset_comp_part_1;
}
/* Algorithm part2 Fig 28 */
if (announceA->stepsRemoved > announceB->stepsRemoved + 1)
{
return 1;
}
if (announceA->stepsRemoved + 1 < announceB->stepsRemoved)
{
return -1;
}
/* A within 1 of B */
if (announceA->stepsRemoved > announceB->stepsRemoved)
{
comp = memcmp(headerA->sourcePortIdentity.clockIdentity, ptpClock->parentPortIdentity.clockIdentity, CLOCK_IDENTITY_LENGTH);
if (comp < 0)
{
return -1;
}
if (comp > 0)
{
return 1;
}
DBG("Sender=Receiver : Error -1");
return 0;
}
if (announceA->stepsRemoved < announceB->stepsRemoved)
{
comp = memcmp(headerB->sourcePortIdentity.clockIdentity, ptpClock->parentPortIdentity.clockIdentity, CLOCK_IDENTITY_LENGTH);
if (comp < 0)
{
return -1;
}
if (comp > 0)
{
return 1;
}
DBG("Sender=Receiver : Error -1");
return 0;
}
/* steps removed A = steps removed B */
comp = memcmp(headerA->sourcePortIdentity.clockIdentity, headerB->sourcePortIdentity.clockIdentity, CLOCK_IDENTITY_LENGTH);
if (comp < 0)
{
return -1;
}
if (comp > 0)
{
return 1;
}
/* identity A = identity B */
if (headerA->sourcePortIdentity.portNumber < headerB->sourcePortIdentity.portNumber)
{
return -1;
}
if (headerA->sourcePortIdentity.portNumber > headerB->sourcePortIdentity.portNumber)
{
return 1;
}
DBG("Sender=Receiver : Error -2");
return 0;
/* Algorithm part 1 Fig 27 */
dataset_comp_part_1:
/* Compare GM priority1 */
if (announceA->grandmasterPriority1 < announceB->grandmasterPriority1)
{
return -1;
}
if (announceA->grandmasterPriority1 > announceB->grandmasterPriority1)
{
return 1;
}
/* non-standard BMC extension to prioritise GMs with UTC valid */
if (rtOpts->preferUtcValid)
{
Boolean utcA = IS_SET(headerA->flagField1, UTCV);
Boolean utcB = IS_SET(headerB->flagField1, UTCV);
if (utcA > utcB)
{
return -1;
}
if (utcA < utcB)
{
return 1;
}
}
/* Compare GM class */
if (announceA->grandmasterClockQuality.clockClass <
announceB->grandmasterClockQuality.clockClass)
{
return -1;
}
if (announceA->grandmasterClockQuality.clockClass >
announceB->grandmasterClockQuality.clockClass)
{
return 1;
}
/* Compare GM accuracy */
if (announceA->grandmasterClockQuality.clockAccuracy <
announceB->grandmasterClockQuality.clockAccuracy)
{
return -1;
}
if (announceA->grandmasterClockQuality.clockAccuracy >
announceB->grandmasterClockQuality.clockAccuracy)
{
return 1;
}
/* Compare GM offsetScaledLogVariance */
if (announceA->grandmasterClockQuality.offsetScaledLogVariance <
announceB->grandmasterClockQuality.offsetScaledLogVariance)
{
return -1;
}
if (announceA->grandmasterClockQuality.offsetScaledLogVariance >
announceB->grandmasterClockQuality.offsetScaledLogVariance)
{
return 1;
}
/* Compare GM priority2 */
if (announceA->grandmasterPriority2 < announceB->grandmasterPriority2)
{
return -1;
}
if (announceA->grandmasterPriority2 > announceB->grandmasterPriority2)
{
return 1;
}
/* Compare GM identity */
if (comp < 0)
{
return -1;
}
else
{
return 1;
}
}
/*State decision algorithm 9.3.3 Fig 26*/
static UInteger8
bmcStateDecision(MsgHeader *header, MsgAnnounce *announce,
const RunTimeOpts *rtOpts, PtpClock *ptpClock)
{
Integer8 comp;
Boolean newBM;
newBM = ((memcmp(header->sourcePortIdentity.clockIdentity,
ptpClock->parentPortIdentity.clockIdentity, CLOCK_IDENTITY_LENGTH)) ||
(header->sourcePortIdentity.portNumber != ptpClock->parentPortIdentity.portNumber));
if (ptpClock->slaveOnly)
{
s1(header, announce, ptpClock, rtOpts);
if (newBM)
{
if (rtOpts->debug_level > PTP_DEBUG_LEVEL_0)
displayPortIdentity(&header->sourcePortIdentity,
"New best master selected:");
ptpClock->counters.masterChanges++;
if (ptpClock->portState == PTP_SLAVE)
{
displayStatus(rtOpts, ptpClock, "State: ");
}
}
return PTP_SLAVE;
}
if ((!ptpClock->number_foreign_records) &&
(ptpClock->portState == PTP_LISTENING))
{
return PTP_LISTENING;
}
copyD0(&ptpClock->msgTmpHeader, &ptpClock->msgTmp.announce, ptpClock);
DBGV("local clockQuality.clockClass: %d \n", ptpClock->clockQuality.clockClass);
comp = bmcDataSetComparison(&ptpClock->msgTmpHeader, &ptpClock->msgTmp.announce, header, announce, ptpClock, rtOpts);
if (ptpClock->clockQuality.clockClass < 128)
{
if (comp < 0)
{
m1(rtOpts, ptpClock);
return PTP_MASTER;
}
else if (comp > 0)
{
s1(header, announce, ptpClock, rtOpts);
if (newBM)
{
if (rtOpts->debug_level > PTP_DEBUG_LEVEL_0)
displayPortIdentity(&header->sourcePortIdentity,
"New best master selected:");
ptpClock->counters.masterChanges++;
if (ptpClock->portState == PTP_PASSIVE)
{
displayStatus(rtOpts, ptpClock, "State: ");
}
}
return PTP_PASSIVE;
}
else
{
DBG("Error in bmcDataSetComparison..\n");
}
}
else
{
if (comp < 0)
{
m1(rtOpts, ptpClock);
return PTP_MASTER;
}
else if (comp > 0)
{
s1(header, announce, ptpClock, rtOpts);
if (newBM)
{
if (rtOpts->debug_level > PTP_DEBUG_LEVEL_0)
displayPortIdentity(&header->sourcePortIdentity,
"New best master selected:");
ptpClock->counters.masterChanges++;
if (ptpClock->portState == PTP_SLAVE)
{
displayStatus(rtOpts, ptpClock, "State: ");
}
}
return PTP_SLAVE;
}
else
{
DBG("Error in bmcDataSetComparison..\n");
}
}
ptpClock->counters.protocolErrors++;
/* MB: Is this the return code below correct? */
/* Anyway, it's a valid return code. */
return PTP_FAULTY;
}
UInteger8
bmc(ForeignMasterRecord *foreignMaster,
const RunTimeOpts *rtOpts, PtpClock *ptpClock)
{
Integer16 i, best;
DBGV("number_foreign_records : %d \n", ptpClock->number_foreign_records);
if (!ptpClock->number_foreign_records)
if (ptpClock->portState == PTP_MASTER)
{
m1(rtOpts, ptpClock);
return ptpClock->portState;
}
for (i = 1, best = 0; i < ptpClock->number_foreign_records; i++)
if ((bmcDataSetComparison(&foreignMaster[i].header,
&foreignMaster[i].announce,
&foreignMaster[best].header,
&foreignMaster[best].announce,
ptpClock, rtOpts)) < 0)
{
best = i;
}
DBGV("Best record : %d \n", best);
ptpClock->foreign_record_best = best;
return (bmcStateDecision(&foreignMaster[best].header,
&foreignMaster[best].announce,
rtOpts, ptpClock));
}
/*
13.3.2.6, page 126
PTPv2 valid flags per packet type:
ALL:
.... .0.. .... .... = PTP_UNICAST
SYNC+Pdelay Resp:
.... ..0. .... .... = PTP_TWO_STEP
Announce only:
.... .... ..0. .... = FREQUENCY_TRACEABLE
.... .... ...0 .... = TIME_TRACEABLE
.... .... .... 0... = PTP_TIMESCALE
.... .... .... .0.. = PTP_UTC_REASONABLE
.... .... .... ..0. = PTP_LI_59
.... .... .... ...0 = PTP_LI_61
*/