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; const int enhanceListPointer = p*13+t;
if (enhanceListPointer < m_enhancements.size()) { if (enhanceListPointer < m_enhancements.size()) {
result[t*3+1] = m_enhancements.at(enhanceListPointer).address(); if (!m_enhancements.at(enhanceListPointer).isValid())
result[t*3+2] = m_enhancements.at(enhanceListPointer).mode() | ((m_enhancements.at(enhanceListPointer).data() & 1) << 5); result[t*3+1] = result[t*3+2] = result[t*3+3] = 0xff;
result[t*3+3] = m_enhancements.at(enhanceListPointer).data() >> 1; 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 this is the last triplet, get a copy to repeat to the end of the packet
if (enhanceListPointer == m_enhancements.size()-1) { if (enhanceListPointer == m_enhancements.size()-1) {
@@ -64,19 +68,24 @@ QByteArray PageX26Base::packetFromEnhancementList(int p) const
void PageX26Base::setEnhancementListFromPacket(int p, QByteArray pkt) void PageX26Base::setEnhancementListFromPacket(int p, QByteArray pkt)
{ {
// Preallocate entries in the m_enhancements list to hold our incoming triplets. // 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. // This is in case of missing packets so we can keep Local Object pointers valid.
while (m_enhancements.size() < (p+1)*13) while (m_enhancements.size() < (p+1)*13)
m_enhancements.append( X26Triplet{ 41, 0x1e, 0 } ); m_enhancements.append( X26Triplet{ 0xff, 0xff, 0xff } );
X26Triplet newX26Triplet; X26Triplet newX26Triplet;
for (int t=0; t<13; t++) { for (int t=0; t<13; t++) {
const int enhanceListPointer = p*13+t; const int enhanceListPointer = p*13+t;
newX26Triplet.setAddress(pkt.at(t*3+1) & 0x3f); // Need the "& 0xff" since QByteArray.at() returns (signed) chars
newX26Triplet.setMode(pkt.at(t*3+2) & 0x1f); if ((pkt.at(t*3+2) & 0xff) == 0xff)
newX26Triplet.setData(((pkt.at(t*3+3) & 0x3f) << 1) | ((pkt.at(t*3+2) & 0x20) >> 5)); 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); m_enhancements.replace(enhanceListPointer, newX26Triplet);
} }
if (newX26Triplet.mode() == 0x1f && newX26Triplet.address() == 0x3f && newX26Triplet.data() & 0x01) 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; m_data = data;
} }
bool X26Triplet::isValid() const
{
return m_mode != 0xff;
}
int X26Triplet::address() const int X26Triplet::address() const
{ {
return m_address; return m_address;
@@ -61,6 +66,11 @@ bool X26Triplet::isRowTriplet() const
return (m_address >= 40); return (m_address >= 40);
} }
void X26Triplet::setInvalid()
{
m_address = m_mode = m_data = 0xff;
}
void X26Triplet::setAddress(int address) void X26Triplet::setAddress(int address)
{ {
m_address = address; m_address = address;
@@ -180,7 +190,9 @@ void X26TripletList::updateInternalData()
triplet->m_reservedMode = false; triplet->m_reservedMode = false;
triplet->m_reservedData = false; triplet->m_reservedData = false;
if (triplet->isRowTriplet()) { if (!triplet->isValid())
triplet->m_error = X26Triplet::ErrorDecodingTriplet;
else if (triplet->isRowTriplet()) {
switch (triplet->modeExt()) { switch (triplet->modeExt()) {
case 0x00: // Full screen colour case 0x00: // Full screen colour
if (activePosition.isDeployed()) if (activePosition.isDeployed())

View File

@@ -26,7 +26,7 @@ class X26Triplet
{ {
public: public:
// x26model.h has the Plain English descriptions of these errors // 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 }; enum ObjectSource { InvalidObjectSource, LocalObject, POPObject, GPOPObject };
X26Triplet() {} X26Triplet() {}
@@ -36,6 +36,7 @@ public:
X26Triplet(int address, int mode, int data); X26Triplet(int address, int mode, int data);
bool isValid() const;
int address() const; int address() const;
int mode() const; int mode() const;
int modeExt() const; int modeExt() const;
@@ -44,6 +45,7 @@ public:
int addressColumn() const; int addressColumn() const;
bool isRowTriplet() const; bool isRowTriplet() const;
void setInvalid();
void setAddress(int address); void setAddress(int address);
void setMode(int mode); void setMode(int mode);
void setData(int data); void setData(int data);

View File

@@ -227,6 +227,9 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
int foundPageNumber = -1; int foundPageNumber = -1;
bool firstPacket0Found = false; bool firstPacket0Found = false;
bool pageBodyPacketsFound = false; bool pageBodyPacketsFound = false;
bool errorEnhancements = false;
bool errorLinks = false;
bool errorPresentation = false;
m_inFile = inFile; 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 // Error found in at least one byte of the link
// Neutralise the whole link to same magazine, page FF, subcode 3F7F // Neutralise the whole link to same magazine, page FF, subcode 3F7F
qDebug("X/27/%d link %d decoding error", readDesignationCode, i); qDebug("X/27/%d link %d decoding error", readDesignationCode, i);
errorLinks = true;
m_inLine[b] = 0xf; m_inLine[b] = 0xf;
m_inLine[b+1] = 0xf; m_inLine[b+1] = 0xf;
m_inLine[b+2] = 0xf; m_inLine[b+2] = 0xf;
@@ -416,15 +420,17 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
// Error decoding Hamming 24/18 // Error decoding Hamming 24/18
qDebug("X/%d/%d triplet %d decoding error", readPacketNumber, readDesignationCode, i); qDebug("X/%d/%d triplet %d decoding error", readPacketNumber, readDesignationCode, i);
if (readPacketNumber == 26) { if (readPacketNumber == 26) {
// Enhancements packet, set to "dummy" Address 41, Mode 0x1e, Data 0 // Enhancements packet, set to invalid triplet
m_inLine[b] = 41; m_inLine[b] = 0xff;
m_inLine[b+1] = 0x1e; m_inLine[b+1] = 0xff;
m_inLine[b+2] = 0; m_inLine[b+2] = 0xff;
errorEnhancements = true;
} else { } else {
// Zero out whole decoded triplet, bound to make things go wrong... // Zero out whole decoded triplet, bound to make things go wrong...
m_inLine[b] = 0x00; m_inLine[b] = 0x00;
m_inLine[b+1] = 0x00; m_inLine[b+1] = 0x00;
m_inLine[b+2] = 0x00; m_inLine[b+2] = 0x00;
errorPresentation = true;
} }
} else { } else {
m_inLine[b] = d & 0x0003f; m_inLine[b] = d & 0x0003f;
@@ -441,8 +447,15 @@ bool LoadT42Format::load(QFile *inFile, QList<PageBase>& subPages, QVariantHash
} else if (!pageBodyPacketsFound) { } else if (!pageBodyPacketsFound) {
m_error = "X/0 found, but no page body packets were found."; m_error = "X/0 found, but no page body packets were found.";
return false; 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 // 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 // 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 // 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; packet[i] = packet.at(i) | 0x40;
}
return packet; return packet;
} }
@@ -322,26 +337,30 @@ QByteArray SaveT42Format::format4BitPacket(QByteArray packet)
QByteArray SaveT42Format::format18BitPacket(QByteArray packet) QByteArray SaveT42Format::format18BitPacket(QByteArray packet)
{ {
for (int c=1; c<packet.size(); c+=3) { for (int c=1; c<packet.size(); c+=3)
unsigned int D5_D11; // For invalid packets, save as all zeroes which will fail Hamming 24/18 decoding
unsigned int D12_D18; if ((packet.at(c) & 0xff) == 0xff)
unsigned int P5, P6; packet[c] = packet[c+1] = packet[c+2] = 0;
unsigned int Byte_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]); 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; packet[c] = Byte_0;
D5_D11 = (toEncode >> 4) & 0x7f; D5_D11 = (toEncode >> 4) & 0x7f;
D12_D18 = (toEncode >> 11) & 0x7f; D12_D18 = (toEncode >> 11) & 0x7f;
P5 = 0x80 & ~(hamming_24_18_parities[0][D12_D18] << 2); P5 = 0x80 & ~(hamming_24_18_parities[0][D12_D18] << 2);
packet[c+1] = D5_D11 | P5; packet[c+1] = D5_D11 | P5;
P6 = 0x80 & ((hamming_24_18_parities[0][Byte_0] ^ hamming_24_18_parities[0][D5_D11]) << 2); P6 = 0x80 & ((hamming_24_18_parities[0][Byte_0] ^ hamming_24_18_parities[0][D5_D11]) << 2);
packet[c+2] = D12_D18 | P6; packet[c+2] = D12_D18 | P6;
} }
return packet; return packet;
} }
@@ -440,14 +459,21 @@ bool SaveEP1Format::getWarnings(const PageBase &subPage)
QByteArray SaveEP1Format::format18BitPacket(QByteArray packet) QByteArray SaveEP1Format::format18BitPacket(QByteArray packet)
{ {
for (int c=1; c<packet.size(); c+=3) { for (int c=1; c<packet.size(); c+=3)
// Shuffle triplet bits to 6 bit address, 5 bit mode, 7 bit data if ((packet.at(c+1) & 0xff) == 0xff) {
packet[c+2] = ((packet.at(c+2) & 0x3f) << 1) | ((packet.at(c+1) & 0x20) >> 5); // Save invalid triplets as address 41, mode 0x1e, data 0
packet[c+1] = packet.at(c+1) & 0x1f; // which hopefully won't do anything when parsed as X/26 enhancements
// Address of termination marker is 7f instead of 3f packet[c] = 41;
if (packet.at(c+1) == 0x1f && packet.at(c) == 0x3f) packet[c+1] = 0x1e;
packet[c] = 0x7f; 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; 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(); 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->setEnabled(true);
m_cookedModePushButton->setText(m_modeTripletNames.modeName(modeExt)); 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) { 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(); 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()); 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, // 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 else
return QVariant(); return QVariant();
case 1: case 1:
if (!triplet.isRowTriplet()) if (triplet.isValid() && !triplet.isRowTriplet())
return triplet.addressColumn(); return triplet.addressColumn();
// For Set Active Position and Origin Modifier, data is the column // For Set Active Position and Origin Modifier, data is the column
else if (triplet.modeExt() == 0x04) else if (triplet.modeExt() == 0x04)
@@ -122,9 +122,14 @@ QVariant X26Model::data(const QModelIndex &index, int role) const
QString result; QString result;
if (role == Qt::DisplayRole) { 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())); return (m_modeTripletNames.modeName(triplet.modeExt()));
}
// Column 3 - describe effects of data/address triplet parameters in plain English // Column 3 - describe effects of data/address triplet parameters in plain English
if (!triplet.isValid())
return QVariant();
switch (triplet.modeExt()) { switch (triplet.modeExt()) {
case 0x01: // Full row colour case 0x01: // Full row colour
case 0x07: // Address row 0 case 0x07: // Address row 0
@@ -609,12 +614,15 @@ bool X26Model::setData(const QModelIndex &index, const QVariant &value, int role
return true; return true;
case 2: // Cooked triplet mode 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 // 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::ETaddress, 0x00, 41, role));
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETdata, 0x00, 0, role)); m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETdata, 0x00, 0, role));
} } else if (intValue >= 0x20 && triplet.isRowTriplet()) {
if (intValue >= 0x20 && triplet.isRowTriplet()) {
// Changing mode from row triplet to column triplet // 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::ETaddress, 0x00, 0, role));
m_parentMainWidget->document()->undoStack()->push(new EditTripletCommand(m_parentMainWidget->document(), this, index.row(), EditTripletCommand::ETdata, 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 // 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 { "", 0 }, // No error
{ "Error decoding triplet", 2 },
{ "Active Position can't move up", 0 }, { "Active Position can't move up", 0 },
{ "Active Position can't move left within row", 1 }, { "Active Position can't move left within row", 1 },
{ "Invocation not pointing to Object Definition", 3 }, { "Invocation not pointing to Object Definition", 3 },