Melden Sie sich bei Ihrem Altimeter Cloud Konto an
Noch kein Konto? Registrieren
Wir senden Ihnen einen Bestätigungslink per E-Mail. Prüfen Sie auch Ihren Spam-Ordner.
Haben Sie bereits ein Konto? Anmelden
Jeder Flug, den Jupiter aufzeichnet, wird zu einer einzigen .aclz-Datei. Sie ist vollständig in sich geschlossen: Die Sensordaten bei jeder Rate, die erkannten Flugereignisse, die Sensorsynchronisationsergebnisse und eine Momentaufnahme der genauen Einstellungen, mit denen das Gerät flog, sind alle in einer Datei enthalten, kryptographisch signiert, so dass jede Änderung erkannt werden kann. Ein Flug mit voller Rate kostet nach Komprimierung etwa 100 KB pro Minute, was das Hochladen ganzer Flüge über eine Mobilfunkverbindung praktikabel macht.
Eine .aclz-Datei ist ein mit LZMA komprimierter Strom (die 7-Zip-Algorithmusfamilie, lesbar mit Standardtools), der zwei Dinge einschließt: einen festen 64-KB-Header und die Flugdaten selbst in Spalten gespeichert. Die Datei wird vor der Komprimierung signiert: Das Gerät berechnet einen SHA-256-Digest des vollständigen unkomprimierten Inhalts mit dem Signaturfach auf Null, signiert den Digest mit Ed25519, platziert die Signatur in ihr Fach im Header und komprimiert dann erst. Die Verifizierung führt die gleichen Schritte rückwärts aus, so dass ein einzelnes geändertes Byte irgendwo in der Datei, ein Wert, ein Zeitstempel, ein Ereignis, die Überprüfung fehlschlagen lässt.
Die erste Hälfte des Headers hält den Flug selbst: die echte Startzeit t=0, die Startplatz-Höhenlage, Pro-Tier-Abtastzählungen, das Landungsergebnis, jeden erkannten Ausbrennungs- und Stufen-Zündzeitstempel, den Apogäums-Rekord, die Barometer-zu-Beschleunigungsmesser-Synchronisation, die bei diesem Flug gemessen wurde, und die Ed25519-Signatur. Platz ist auch für das Flugereignisprotokoll reserviert, eine Zeitleiste von Ausgaben, Schutzmaßnahmen und Fehlern, die in einer zukünftigen Firmware ankommen. Die zweite Hälfte ist eine vollständige Momentaufnahme der Gerätekonfiguration zum Zeitpunkt des Speicherns, jede Einstellung und jede Regel wie geflogen, was bedeutet, dass ein Flug immer gegen die genaue Konfiguration, die ihn hervorbrachte, interpretiert werden kann, auch Jahre später und nach vielen Einstellungsänderungen.
Jupiter zeichnet sechs Datentiers bei Raten auf, die damit abgestimmt sind, wie schnell sich jede Größe tatsächlich ändert, von 600 Hz oben bis 1 Hz für langsame Hausverwaltung. Die genauen Felder in jedem Tier wachsen immer noch, wenn Funktionen hinzukommen, und die Feldlisten sind pro Formatversion definiert, anstatt hier versprochen zu werden, aber die aktuelle Form ist: Beschleunigung bei 600 Hz; Rotation und Orientierung bei 300 Hz; Magnetometer, Höhe und vorhergesagtes Apogäum bei 100 Hz; Zünder-Kontinuität und Stromversorgung bei 50 Hz; GNSS, Temperaturen und Licht bei 10 Hz; und tiefe GNSS-, Mobilfunk- und Servo-Details bei 1 Hz. Höhe wird beispielsweise derzeit als sowohl die rohe als auch die gefilterte Reihe gespeichert. Jede Probe wird von einer Hauptuhr mit 10-Mikrosekunden-Auflösung zeitgestempelt, und die Zeitstempel reisen als drei Ströme statt einer: Die Tiers von 600/300/100 Hz teilen sich die erste, 50 Hz trägt ihre eigene, und 10/1 Hz teilen sich die dritte. Es gibt drei, weil das Abtastzahl-System die Tier-Gruppen mit unterschiedlichen effektiven Raten laufen lassen kann, so dass jede Gruppe ihre eigene Zeitachse besitzt und nichts jemals von Lockstep-Verhalten ausgeht; und weil alle drei Ströme die gleiche Uhr ticken, kann jeder Wert immer über jeden anderen genau gelegt werden.
In der Datei werden Daten nach Spalten gespeichert, jeweils eine komplette Feldzeitserie auf einmal, so dass ähnliche Werte zusammensitzen, bevor der Kompressor sie sieht. Jede Spalte speichert Unterschiede zwischen aufeinanderfolgenden Proben statt der Werte selbst, so gepackt, dass eine kleine Änderung eine einzelne Byte kostet, was Sensordaten größtenteils sind. In regelmäßigen Abständen, alle Viertelsekunde in den schnellen Tiers, wird stattdessen ein absoluter Wert geschrieben. Diese Keyframes existieren für Robustheit: eine Differenzkette mit einem fehlerhaften Byte wäre vom diesem Punkt bis zum Ende der Spalte falsch, aber ein Keyframe resynchronisiert das Dekodieren, so dass Beschädigungen an einer Datei sie lokal beeinträchtigen, statt alles nach dem Fehler zu zerstören. Die Zeitstempel selbst sind auf die gleiche Weise geschützt. Höhenwerte werden mit Millimeter-Genauigkeit gespeichert, feiner als die Unsicherheit der Sensorkette selbst, so dass keine Quantisierungstreppe jemals sichtbar ist, egal wie weit Sie hineinzoomen.
Die Datei selbst ist zur Inspektion offen, und dieser Abschnitt ist die Karte dafür, es richtig zu machen. Verbinden Sie sich über USB und jeder Flug ist eine flight_<t0>.aclz-Datei, die ein Standard-LZMA-Strom ist: 7-Zip öffnet sie direkt, xz --decompress --format=lzma in der Befehlszeile, und Pythons eingebautes lzma-Modul mit format=lzma.FORMAT_ALONE.
Dekomprimiert ist das Layout ein fester 64-KB-Header, gefolgt von den Datenspalten. Die Header-Felder, die Sie zum Dekodieren benötigen, sitzen alle an festen Offsets: die ASCII-Magic-Zeichenkette bei Offset 0; die Formatversion als 32-Bit-Integer bei Offset 32, die Sie überprüfen müssen, bevor Sie die Spalten berühren, weil Spaltenzählungen und Werteskalen zwischen Versionen ändern und die Daten es Ihnen nicht selbst sagen können; t=0 bei Offset 36 als 32-Bit-Tickzahl auf der 10-Mikrosekunden-Uhr; die Startplatz-Höhenlage als float bei 40; die sechs Pro-Tier-Abtastzählungen als 32-Bit-Integer von Offset 44; und die sechs Pro-Tier-Keyframe-Intervalle als 16-Bit-Integer von Offset 68. Die Einstellungs-Momentaufnahme beginnt bei Offset 32768: ein SET1-Marker, eine 32-Bit-Länge, dann Schlüssel=Wert-Paare, die durch das 0x1F-Byte getrennt sind, nah genug an Klartext, dass ein Texteditor es zeigt.
Die Spalten beginnen bei Offset 65536 und sind reine Sequenzen von Variablen-Längen-Integern, eine Spalte komplett vor der nächsten, in einer festen Reihenfolge: der erste Zeitstempel-Strom, dann jedes 600-Hz-Feld, jedes 300-Hz-Feld, jedes 100-Hz-Feld, der zweite Zeitstempel-Strom, die 50-Hz-Felder, der dritte Zeitstempel-Strom, die 10-Hz-Felder und die 1-Hz-Felder. Jeder Wert ist ein Varint (sieben Bits pro Byte, niedrige Bits zuerst, oberstes Bit bedeutet ein weiteres Byte folgt), der eine Zigzag-kodierte Zahl mit Vorzeichen trägt, so dass das Dekodieren eines Wertes (u >> 1) ^ -(u & 1) ist. Die Wiedermontage ist dann die Keyframe-Regel: innerhalb einer Spalte ist Probe i ein absoluter Wert, wenn i % kf == 0 für das Keyframe-Intervall dieses Tiers, andernfalls ein Delta, das zum vorherigen dekoden Wert addiert wird. Das ist der ganze Mechanismus; die Keyframes existieren, so dass ein fehlerhaftes Byte höchstens ein Intervall statt des Rests der Spalte beschädigt. Das NaN-Sentinel ist INT32_MIN. Langsamere Tiers innerhalb einer Gruppe indizieren in ihren Gruppen-Zeitstempel-Strom (eine 300-Hz-Probe j nutzt Stream-Eintrag 2j, eine 100-Hz-Probe j Eintrag 6j, eine 1-Hz-Probe j Eintrag 10j des dritten Stroms), und für Formatversion 9 sind die Feldzählungen pro Tier 3, 7, 7, 8, 18 und 49.
Der unten angegebene Referenz-Konverter ist das kanonische bearbeitete Beispiel, und es ist der ganze Arbeitsablauf in einem Befehl: Führen Sie python3 aclz_to_csv.py flight_1234.aclz aus und Dekomprimierung, Header, Einstellungen und alle sechs Tiers kommen ohne andere Tools mit heraus. Es dekodiert den Header und die Einstellungen für jede Version, dekodiert den vollständigen Spaltensatz für Version 9 und schreibt eine CSV pro Tier mit Zeit in Sekunden von t=0. Es gibt absichtlich die rohen quantisierten Integer aus, die das Gerät gespeichert hat, da Feldmeanings und Skalen Versions-Territorium sind. Es ist auch absichtlich die einzige vollständige Implementierung, die wir veröffentlichen: jedes vollständige Skript in einer anderen Sprache ist ein weiterer Parser, um ihn mit einem noch wachsenden Format synchron zu halten, so portieren Sie von diesem, wenn Sie einen anderen Stack benötigen, und überprüfen Sie das Versionsbyye zuerst in allem, das Sie erstellen.
Dies ist Python-Code: aclz_to_csv.py, der vollständige Konverter
#!/usr/bin/env python3
# aclz_to_csv.py - Referenz-Konverter für Jupiter .aclz-Flugprotokolle.
# Schreibt eine CSV pro Daten-Tier (tier600.csv .. tier1.csv) plus settings.txt.
# Vollständige Spalten-Dekodierung ist für Formatversion 9 geschrieben. Werte sind die
# rohen quantisierten Integer, die das Gerät gespeichert hat (Feldmeanings und Skalen
# sind pro Formatversion definiert); die Zeit-Spalte ist Sekunden von t=0. Das
# NaN-Sentinel (INT32_MIN) wird als leere Zelle ausgegeben.
import lzma, struct, sys
INT32_MIN = -2147483648
def read_varint(buf, pos):
u = 0; s = 0
while True:
b = buf[pos]; pos += 1
u |= (b & 0x7F) << s
if not (b & 0x80):
return u, pos
s += 7
def unzig(u):
return (u >> 1) ^ -(u & 1)
def decode_column(buf, pos, n, kf):
col = []; prev = 0
for i in range(n):
u, pos = read_varint(buf, pos)
v = unzig(u)
x = v if (kf == 0 or i % kf == 0) else prev + v
col.append(x); prev = x
return col, pos
def write_csv(name, stamps, t0, cols):
with open(name, 'w') as f:
f.write('t_s,' + ','.join('f%d' % i for i in range(len(cols))) + '\n')
for r in range(len(stamps)):
t = (stamps[r] - t0) / 100000.0
row = ['' if c[r] == INT32_MIN else str(c[r]) for c in cols]
f.write('%.5f,%s\n' % (t, ','.join(row)))
def main(path):
with open(path, 'rb') as f:
body = lzma.decompress(f.read(), format=lzma.FORMAT_ALONE)
magic = body[0:24].split(b'\x00')[0].decode()
if not magic.startswith('ALTIMETERCLOUD-ACL'):
sys.exit('not an ACL file (magic: %r)' % magic)
ver, t0 = struct.unpack_from('<II', body, 32)
(floor_m,) = struct.unpack_from('<f', body, 40)
counts = struct.unpack_from('<6I', body, 44) # n600 n300 n100 n50 n10 n1
kfs = struct.unpack_from('<6H', body, 68) # keyframe interval per tier
print('format v%d t0 tick=%d pad=%.1f m samples=%s' % (ver, t0, floor_m, list(counts)))
if body[32768:32772] == b'SET1':
(slen,) = struct.unpack_from('<I', body, 32772)
pairs = body[32776:32776 + slen].decode('utf-8', 'replace').split('\x1f')
with open('settings.txt', 'w') as f:
f.write('\n'.join(p for p in pairs if p) + '\n')
print('settings.txt: %d entries' % sum(1 for p in pairs if p))
if ver != 9:
sys.exit('column decode here is written for format v9; this file is v%d - '
'update the column table before use' % ver)
n600, n300, n100, n50, n10, n1 = counts
pos = 65536
sA, pos = decode_column(body, pos, n600, kfs[0]) # stamps: 600/300/100 group
c600 = []
for _ in range(3):
c, pos = decode_column(body, pos, n600, kfs[0]); c600.append(c)
c300 = []
for _ in range(7):
c, pos = decode_column(body, pos, n300, kfs[1]); c300.append(c)
c100 = []
for _ in range(7):
c, pos = decode_column(body, pos, n100, kfs[2]); c100.append(c)
sB, pos = decode_column(body, pos, n50, kfs[3]) # stamps: 50 group
c50 = []
for _ in range(8):
c, pos = decode_column(body, pos, n50, kfs[3]); c50.append(c)
sC, pos = decode_column(body, pos, n10, kfs[4]) # stamps: 10/1 group
c10 = []
for _ in range(18):
c, pos = decode_column(body, pos, n10, kfs[4]); c10.append(c)
c1 = []
for _ in range(49):
c, pos = decode_column(body, pos, n1, kfs[5]); c1.append(c)
write_csv('tier600.csv', sA, t0, c600)
write_csv('tier300.csv', [sA[2 * j] for j in range(n300)], t0, c300)
write_csv('tier100.csv', [sA[6 * j] for j in range(n100)], t0, c100)
write_csv('tier50.csv', sB, t0, c50)
write_csv('tier10.csv', sC, t0, c10)
write_csv('tier1.csv', [sC[10 * j] for j in range(n1)], t0, c1)
print('six tier CSVs written; %d bytes of column data consumed' % (pos - 65536))
if __name__ == '__main__':
main(sys.argv[1] if len(sys.argv) > 1 else 'flight.aclz')
Für einen Port sind der Decompress-Einstiegspunkt plus drei Primitive, Varint-Lesevorgänge, Zigzag-Dekodierung und Delta-mit-Keyframe-Wiedermontage, alles, was das Format benötigt; der Rest ist Buchführung. In Perl (beachten Sie das Zigzag: verwenden Sie die Zweig-Form, da Bitwise XOR in manchen Sprachen unsigniert ist, Perl eingeschlossen):
Dies ist Perl-Code: der Decompress-Einstiegspunkt und die Port-Primitive
# Einstiegspunkt: Slurp die .aclz und dekomprimieren. Compress::Raw::Lzma liest
# den LZMA-alone-Strom direkt (oder Shell zu: xz --decompress --format=lzma).
use strict; use warnings;
use Compress::Raw::Lzma;
sub read_aclz {
my ($path) = @_;
open(my $fh, '<:raw', $path) or die "open $path: $!";
my $comp = do { local $/; <$fh> };
close $fh;
my ($lz, $st) = Compress::Raw::Lzma::AloneDecoder->new(AppendOutput => 1);
my $body = '';
$lz->code($comp, $body);
return \$body; # Ref zum dekomprimierten Header + Spalten
}
# Die drei Primitive, die jeder Port benötigt.
sub read_varint { # ($bufref, $pos) -> ($value, $newpos)
my ($buf, $pos) = @_; my ($u, $s) = (0, 0);
while (1) {
my $b = ord(substr($$buf, $pos++, 1));
$u |= ($b & 0x7F) << $s;
return ($u, $pos) unless $b & 0x80;
$s += 7;
}
}
sub unzig { # Zigzag-Dekodierung; Zweig-Form, nicht XOR: Perls ^ ist unsigniert
my $u = shift; return ($u & 1) ? -(($u + 1) >> 1) : ($u >> 1);
}
sub decode_column { # ($bufref, $pos, $n, $kf) -> (\@col, $newpos)
my ($buf, $pos, $n, $kf) = @_; my @col; my $prev = 0;
for my $i (0 .. $n - 1) {
(my $u, $pos) = read_varint($buf, $pos);
my $v = unzig($u);
my $x = ($kf == 0 || $i % $kf == 0) ? $v : $prev + $v;
push @col, $x; $prev = $x;
}
return (\@col, $pos);
}
Die zusammengesetzte Datei wird mit LZMA bei der Anstrengung komprimiert, die von der log_zlevel-Einstellung gewählt wird, die auf der Seite Komprimierung & Verschlüsselung zusammen mit der Stärke des Signaturschemas behandelt wird. Das spaltige Delta-Layout und der Kompressor funktionieren zusammen: Die Deltas ebnen die Daten, die Spalten gruppieren sie, und LZMA entfernt, was bleibt. Die Signatur darunter macht das Ergebnis zu permanenten Beweisen: Ihr Flug, wie er flog, nachweislich unverändert.