Handle invalid triplets

"Invalid triplets" are triplets that have failed Hamming 24/18 decoding.

Previously, invalid triplets were converted at load time to either all zero
bits or, in the case of X/26 enhancement triplets, to "dummy" triplets of
reserved mode 11110 with a row address group that hopefully have no effect.
Now the X/26 enhancement triplet list can explicitly store invalid triplets
and will show them as "error decoding triplet".

Invalid triplets in other packets such as X/28/0 will still be zeroed out at
load time.

Since the TTI format has no provision for storing invalid triplets, saving a
page will convert the invalid triplets to reserved mode 11110 as described
above.

The actual bits of invalid triplets are not stored on the assumption that
they are not recoverable. Thus exporting to t42 format will write an invalid
triplet as a Hamming coded result of all zero bits which will still cause a
Hamming decoding failure.
This commit is contained in:
Gavin MacGregor
2025-11-04 17:56:04 +00:00
parent 1b3623d61b
commit 1efa8c196d
8 changed files with 125 additions and 47 deletions

View File

@@ -36,9 +36,13 @@ QByteArray PageX26Base::packetFromEnhancementList(int p) const
const int enhanceListPointer = p*13+t;
if (enhanceListPointer < m_enhancements.size()) {
result[t*3+1] = m_enhancements.at(enhanceListPointer).address();
result[t*3+2] = m_enhancements.at(enhanceListPointer).mode() | ((m_enhancements.at(enhanceListPointer).data() & 1) << 5);
result[t*3+3] = m_enhancements.at(enhanceListPointer).data() >> 1;
if (!m_enhancements.at(enhanceListPointer).isValid())
result[t*3+1] = result[t*3+2] = result[t*3+3] = 0xff;
else {
result[t*3+1] = m_enhancements.at(enhanceListPointer).address();
result[t*3+2] = m_enhancements.at(enhanceListPointer).mode() | ((m_enhancements.at(enhanceListPointer).data() & 1) << 5);
result[t*3+3] = m_enhancements.at(enhanceListPointer).data() >> 1;
}
// If this is the last triplet, get a copy to repeat to the end of the packet
if (enhanceListPointer == m_enhancements.size()-1) {
@@ -64,19 +68,24 @@ QByteArray PageX26Base::packetFromEnhancementList(int p) const
void PageX26Base::setEnhancementListFromPacket(int p, QByteArray pkt)
{
// Preallocate entries in the m_enhancements list to hold our incoming triplets.
// We write "dummy" reserved 11110 Row Triplets in the allocated entries which then get overwritten by the packet contents.
// We write invalid triplets in the allocated entries which then get overwritten by the packet contents.
// This is in case of missing packets so we can keep Local Object pointers valid.
while (m_enhancements.size() < (p+1)*13)
m_enhancements.append( X26Triplet{ 41, 0x1e, 0 } );
m_enhancements.append( X26Triplet{ 0xff, 0xff, 0xff } );
X26Triplet newX26Triplet;
for (int t=0; t<13; t++) {
const int enhanceListPointer = p*13+t;
newX26Triplet.setAddress(pkt.at(t*3+1) & 0x3f);
newX26Triplet.setMode(pkt.at(t*3+2) & 0x1f);
newX26Triplet.setData(((pkt.at(t*3+3) & 0x3f) << 1) | ((pkt.at(t*3+2) & 0x20) >> 5));
// Need the "& 0xff" since QByteArray.at() returns (signed) chars
if ((pkt.at(t*3+2) & 0xff) == 0xff)
newX26Triplet.setInvalid();
else {
newX26Triplet.setAddress(pkt.at(t*3+1) & 0x3f);
newX26Triplet.setMode(pkt.at(t*3+2) & 0x1f);
newX26Triplet.setData(((pkt.at(t*3+3) & 0x3f) << 1) | ((pkt.at(t*3+2) & 0x20) >> 5));
}
m_enhancements.replace(enhanceListPointer, newX26Triplet);
}
if (newX26Triplet.mode() == 0x1f && newX26Triplet.address() == 0x3f && newX26Triplet.data() & 0x01)

View File

@@ -26,6 +26,11 @@ X26Triplet::X26Triplet(int address, int mode, int data)
m_data = data;
}
bool X26Triplet::isValid() const
{
return m_mode != 0xff;
}
int X26Triplet::address() const
{
return m_address;
@@ -61,6 +66,11 @@ bool X26Triplet::isRowTriplet() const
return (m_address >= 40);
}
void X26Triplet::setInvalid()
{
m_address = m_mode = m_data = 0xff;
}
void X26Triplet::setAddress(int address)
{
m_address = address;
@@ -180,7 +190,9 @@ void X26TripletList::updateInternalData()
triplet->m_reservedMode = false;
triplet->m_reservedData = false;
if (triplet->isRowTriplet()) {
if (!triplet->isValid())
triplet->m_error = X26Triplet::ErrorDecodingTriplet;
else if (triplet->isRowTriplet()) {
switch (triplet->modeExt()) {
case 0x00: // Full screen colour
if (activePosition.isDeployed())

View File

@@ -26,7 +26,7 @@ class X26Triplet
{
public:
// x26model.h has the Plain English descriptions of these errors
enum X26TripletError { NoError, ActivePositionMovedUp, ActivePositionMovedLeft, InvokePointerInvalid, InvokeTypeMismatch, OriginModifierAlone };
enum X26TripletError { NoError, ErrorDecodingTriplet, ActivePositionMovedUp, ActivePositionMovedLeft, InvokePointerInvalid, InvokeTypeMismatch, OriginModifierAlone };
enum ObjectSource { InvalidObjectSource, LocalObject, POPObject, GPOPObject };
X26Triplet() {}
@@ -36,6 +36,7 @@ public:
X26Triplet(int address, int mode, int data);
bool isValid() const;
int address() const;
int mode() const;
int modeExt() const;
@@ -44,6 +45,7 @@ public:
int addressColumn() const;
bool isRowTriplet() const;
void setInvalid();
void setAddress(int address);
void setMode(int mode);
void setData(int data);

View File

@@ -227,6 +227,9 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
int foundPageNumber = -1;
bool firstPacket0Found = false;
bool pageBodyPacketsFound = false;
bool errorEnhancements = false;
bool errorLinks = false;
bool errorPresentation = false;
m_inFile = inFile;
@@ -374,6 +377,7 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
// Error found in at least one byte of the link
// Neutralise the whole link to same magazine, page FF, subcode 3F7F
qDebug("X/27/%d link %d decoding error", readDesignationCode, i);
errorLinks = true;
m_inLine[b] = 0xf;
m_inLine[b+1] = 0xf;
m_inLine[b+2] = 0xf;
@@ -416,15 +420,17 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
// Error decoding Hamming 24/18
qDebug("X/%d/%d triplet %d decoding error", readPacketNumber, readDesignationCode, i);
if (readPacketNumber == 26) {
// Enhancements packet, set to "dummy" Address 41, Mode 0x1e, Data 0
m_inLine[b] = 41;
m_inLine[b+1] = 0x1e;
m_inLine[b+2] = 0;
// Enhancements packet, set to invalid triplet
m_inLine[b] = 0xff;
m_inLine[b+1] = 0xff;
m_inLine[b+2] = 0xff;
errorEnhancements = true;
} else {
// Zero out whole decoded triplet, bound to make things go wrong...
m_inLine[b] = 0x00;
m_inLine[b+1] = 0x00;
m_inLine[b+2] = 0x00;
errorPresentation = true;
}
} else {
m_inLine[b] = d & 0x0003f;
@@ -441,8 +447,15 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
} else if (!pageBodyPacketsFound) {
m_error = "X/0 found, but no page body packets were found.";
return false;
} else
return true;
}
if (errorEnhancements)
m_warnings.append("Error decoding triplet(s) in enhancement data.");
if (errorLinks)
m_warnings.append("Error decoding FLOF links.");
if (errorPresentation)
m_warnings.append("Error decoding triplet(s) in presentation data.");
return true;
}

View File

@@ -160,8 +160,23 @@ QByteArray SaveTTIFormat::format18BitPacket(QByteArray packet)
// TTI stores the triplets 6 bits at a time like we do, without Hamming encoding
// We don't touch the first byte; the caller replaces it with the designation code
// unless it's X/1-X/25 used in (G)POP pages
for (int i=1; i<packet.size(); i++)
for (int i=1; i<packet.size(); i++) {
// Save invalid triplets as address 41, mode 0x1e, data 0
// which hopefully won't do anything when parsed as X/26 enhancements
if ((packet.at(i) & 0xff) == 0xff)
switch (i % 3) {
case 1:
packet[i] = 41;
break;
case 2:
packet[i] = 0x1e;
break;
case 0:
packet[i] = 0;
break;
}
packet[i] = packet.at(i) | 0x40;
}
return packet;
}
@@ -322,26 +337,30 @@ QByteArray SaveT42Format::format4BitPacket(QByteArray packet)
QByteArray SaveT42Format::format18BitPacket(QByteArray packet)
{
for (int c=1; c<packet.size(); c+=3) {
unsigned int D5_D11;
unsigned int D12_D18;
unsigned int P5, P6;
unsigned int Byte_0;
for (int c=1; c<packet.size(); c+=3)
// For invalid packets, save as all zeroes which will fail Hamming 24/18 decoding
if ((packet.at(c) & 0xff) == 0xff)
packet[c] = packet[c+1] = packet[c+2] = 0;
else {
unsigned int D5_D11;
unsigned int D12_D18;
unsigned int P5, P6;
unsigned int Byte_0;
const unsigned int toEncode = packet[c] | (packet[c+1] << 6) | (packet[c+2] << 12);
const unsigned int toEncode = packet[c] | (packet[c+1] << 6) | (packet[c+2] << 12);
Byte_0 = (hamming_24_18_forward[0][(toEncode >> 0) & 0xff] ^ hamming_24_18_forward[1][(toEncode >> 8) & 0xff] ^ hamming_24_18_forward_2[(toEncode >> 16) & 0x03]);
packet[c] = Byte_0;
Byte_0 = (hamming_24_18_forward[0][(toEncode >> 0) & 0xff] ^ hamming_24_18_forward[1][(toEncode >> 8) & 0xff] ^ hamming_24_18_forward_2[(toEncode >> 16) & 0x03]);
packet[c] = Byte_0;
D5_D11 = (toEncode >> 4) & 0x7f;
D12_D18 = (toEncode >> 11) & 0x7f;
D5_D11 = (toEncode >> 4) & 0x7f;
D12_D18 = (toEncode >> 11) & 0x7f;
P5 = 0x80 & ~(hamming_24_18_parities[0][D12_D18] << 2);
packet[c+1] = D5_D11 | P5;
P5 = 0x80 & ~(hamming_24_18_parities[0][D12_D18] << 2);
packet[c+1] = D5_D11 | P5;
P6 = 0x80 & ((hamming_24_18_parities[0][Byte_0] ^ hamming_24_18_parities[0][D5_D11]) << 2);
packet[c+2] = D12_D18 | P6;
}
P6 = 0x80 & ((hamming_24_18_parities[0][Byte_0] ^ hamming_24_18_parities[0][D5_D11]) << 2);
packet[c+2] = D12_D18 | P6;
}
return packet;
}
@@ -440,14 +459,21 @@ bool SaveEP1Format::getWarnings(const PageBase &subPage)
QByteArray SaveEP1Format::format18BitPacket(QByteArray packet)
{
for (int c=1; c<packet.size(); c+=3) {
// Shuffle triplet bits to 6 bit address, 5 bit mode, 7 bit data
packet[c+2] = ((packet.at(c+2) & 0x3f) << 1) | ((packet.at(c+1) & 0x20) >> 5);
packet[c+1] = packet.at(c+1) & 0x1f;
// Address of termination marker is 7f instead of 3f
if (packet.at(c+1) == 0x1f && packet.at(c) == 0x3f)
packet[c] = 0x7f;
}
for (int c=1; c<packet.size(); c+=3)
if ((packet.at(c+1) & 0xff) == 0xff) {
// Save invalid triplets as address 41, mode 0x1e, data 0
// which hopefully won't do anything when parsed as X/26 enhancements
packet[c] = 41;
packet[c+1] = 0x1e;
packet[c+2] = 0;
} else {
// Shuffle triplet bits to 6 bit address, 5 bit mode, 7 bit data
packet[c+2] = ((packet.at(c+2) & 0x3f) << 1) | ((packet.at(c+1) & 0x20) >> 5);
packet[c+1] = packet.at(c+1) & 0x1f;
// Address of termination marker is 7f instead of 3f
if (packet.at(c+1) == 0x1f && packet.at(c) == 0x3f)
packet[c] = 0x7f;
}
return packet;
}

View File

@@ -678,6 +678,13 @@ void X26DockWidget::updateAllCookedTripletWidgets(const QModelIndex &index)
{
const int modeExt = index.model()->data(index.model()->index(index.row(), 2), Qt::EditRole).toInt();
if (modeExt == 0xff) {
disableTripletWidgets();
m_cookedModePushButton->setEnabled(true);
m_cookedModePushButton->setText("Replace...");
return;
}
m_cookedModePushButton->setEnabled(true);
m_cookedModePushButton->setText(m_modeTripletNames.modeName(modeExt));
@@ -1034,7 +1041,7 @@ void X26DockWidget::insertTriplet(int modeExt, int row)
if (modeExt >= 0x20 && modeExt != 0x24 && modeExt != 0x25 && modeExt != 0x26 && modeExt != 0x2a) {
const int existingTripletModeExt = index.model()->data(index.model()->index(index.row(), 2), Qt::EditRole).toInt();
if (existingTripletModeExt >= 0x20 && existingTripletModeExt != 0x24 && existingTripletModeExt != 0x25 && existingTripletModeExt != 0x26 && existingTripletModeExt != 0x2a)
if (existingTripletModeExt >= 0x20 && existingTripletModeExt <= 0x3f && existingTripletModeExt != 0x24 && existingTripletModeExt != 0x25 && existingTripletModeExt != 0x26 && existingTripletModeExt != 0x2a)
newTriplet.setAddress(index.model()->data(index.model()->index(index.row(), 0), Qt::UserRole).toInt());
}
// If we're inserting a Set Active Position or Full Row Colour triplet,

View File

@@ -108,7 +108,7 @@ QVariant X26Model::data(const QModelIndex &index, int role) const
else
return QVariant();
case 1:
if (!triplet.isRowTriplet())
if (triplet.isValid() && !triplet.isRowTriplet())
return triplet.addressColumn();
// For Set Active Position and Origin Modifier, data is the column
else if (triplet.modeExt() == 0x04)
@@ -122,9 +122,14 @@ QVariant X26Model::data(const QModelIndex &index, int role) const
QString result;
if (role == Qt::DisplayRole) {
if (index.column() == 2)
if (index.column() == 2) {
if (!triplet.isValid())
return "Error decoding triplet";
return (m_modeTripletNames.modeName(triplet.modeExt()));
}
// Column 3 - describe effects of data/address triplet parameters in plain English
if (!triplet.isValid())
return QVariant();
switch (triplet.modeExt()) {
case 0x01: // Full row colour
case 0x07: // Address row 0
@@ -609,12 +614,15 @@ bool X26Model::setData(const QModelIndex &index, const QVariant &value, int role
return true;
case 2: // Cooked triplet mode
if (intValue < 0x20 && !triplet.isRowTriplet()) {
if (!triplet.isValid()) {
// Changing from invalid triplet
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETaddress, 0x00, intValue < 0x20 ? 41 : 0, role));
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETdata, 0x00, 32, role));
} else if (intValue < 0x20 && !triplet.isRowTriplet()) {
// Changing mode from column triplet to row triplet
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETaddress, 0x00, 41, role));
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETdata, 0x00, 0, role));
}
if (intValue >= 0x20 && triplet.isRowTriplet()) {
} else if (intValue >= 0x20 && triplet.isRowTriplet()) {
// Changing mode from row triplet to column triplet
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETaddress, 0x00, 0, role));
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETdata, 0x00, 0, role));

View File

@@ -60,8 +60,9 @@ private:
};
// Needs to be in the same order as enum X26TripletError in x26triplets.h
const tripletErrorShow m_tripletErrors[6] {
const tripletErrorShow m_tripletErrors[7] {
{ "", 0 }, // No error
{ "Error decoding triplet", 2 },
{ "Active Position can't move up", 0 },
{ "Active Position can't move left within row", 1 },
{ "Invocation not pointing to Object Definition", 3 },