diff --git a/firmware/application/bitmap.hpp b/firmware/application/bitmap.hpp index 8a1bd151..da1d45b5 100644 --- a/firmware/application/bitmap.hpp +++ b/firmware/application/bitmap.hpp @@ -727,6 +727,28 @@ static constexpr Bitmap bitmap_icon_setup { { 16, 16 }, bitmap_icon_setup_data }; +static constexpr uint8_t bitmap_target_data[] = { + 0x80, 0x00, + 0x80, 0x00, + 0xE0, 0x03, + 0x90, 0x04, + 0x88, 0x08, + 0x04, 0x10, + 0x04, 0x10, + 0x1F, 0x7C, + 0x04, 0x10, + 0x04, 0x10, + 0x88, 0x08, + 0x90, 0x04, + 0xE0, 0x03, + 0x80, 0x00, + 0x80, 0x00, + 0x00, 0x00, +}; +static constexpr Bitmap bitmap_target { + { 16, 16 }, bitmap_target_data +}; + static constexpr uint8_t bitmap_sig_saw_down_data[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/firmware/application/pocsag_app.cpp b/firmware/application/pocsag_app.cpp index 369b6948..6aace1bc 100644 --- a/firmware/application/pocsag_app.cpp +++ b/firmware/application/pocsag_app.cpp @@ -157,7 +157,7 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage * message) { std::string console_info; - console_info = "\n\x1B\x02" + to_string_time(message->packet.timestamp()); + console_info = "\n" + to_string_datetime(message->packet.timestamp(), HM); console_info += " " + pocsag::bitrate_str(message->packet.bitrate()); console_info += " ADDR:" + to_string_dec_uint(pocsag_state.address); console_info += " F" + to_string_dec_uint(pocsag_state.function); diff --git a/firmware/application/recent_entries.cpp b/firmware/application/recent_entries.cpp index 4b9c7562..bc6ef17a 100644 --- a/firmware/application/recent_entries.cpp +++ b/firmware/application/recent_entries.cpp @@ -41,7 +41,7 @@ void RecentEntriesHeader::paint(Painter& painter) { const Style style { .font = parent_style.font, - .background = Color::dark_blue(), + .background = Color::blue(), .foreground = parent_style.foreground, }; diff --git a/firmware/application/string_format.cpp b/firmware/application/string_format.cpp index f96dd361..80e8b9e2 100644 --- a/firmware/application/string_format.cpp +++ b/firmware/application/string_format.cpp @@ -144,18 +144,22 @@ std::string to_string_hex_array(uint8_t * const array, const int32_t l) { return str_return; } -std::string to_string_datetime(const rtc::RTC& value) { - return to_string_dec_uint(value.year(), 4, '0') + "/" + - to_string_dec_uint(value.month(), 2, '0') + "/" + - to_string_dec_uint(value.day(), 2, '0') + " " + - to_string_dec_uint(value.hour(), 2, '0') + ":" + - to_string_dec_uint(value.minute(), 2, '0') + ":" + - to_string_dec_uint(value.second(), 2, '0'); -} - -std::string to_string_time(const rtc::RTC& value) { - return to_string_dec_uint(value.hour(), 2, '0') + ":" + - to_string_dec_uint(value.minute(), 2, '0'); +std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format) { + std::string string { "" }; + + if (format == YMDHMS) { + string += to_string_dec_uint(value.year(), 4, '0') + "/" + + to_string_dec_uint(value.month(), 2, '0') + "/" + + to_string_dec_uint(value.day(), 2, '0') + " "; + } + + string += to_string_dec_uint(value.hour(), 2, '0') + ":" + + string += to_string_dec_uint(value.minute(), 2, '0'); + + if ((format == YMDHMS) || (format == HMS)) + string += ":" + to_string_dec_uint(value.second(), 2, '0'); + + return string; } std::string to_string_timestamp(const rtc::RTC& value) { diff --git a/firmware/application/string_format.hpp b/firmware/application/string_format.hpp index 68822520..8af71b15 100644 --- a/firmware/application/string_format.hpp +++ b/firmware/application/string_format.hpp @@ -29,6 +29,12 @@ #include "lpc43xx_cpp.hpp" using namespace lpc43xx; +enum TimeFormat { + YMDHMS = 0, + HMS = 1, + HM = 2 +}; + // TODO: Allow l=0 to not fill/justify? Already using this way in ui_spectrum.hpp... std::string to_string_bin(const uint32_t n, const uint8_t l = 0); std::string to_string_dec_uint(const uint32_t n, const int32_t l = 0, const char fill = 0); @@ -38,8 +44,7 @@ std::string to_string_hex_array(uint8_t * const array, const int32_t l = 0); std::string to_string_short_freq(const uint64_t f); -std::string to_string_datetime(const rtc::RTC& value); -std::string to_string_time(const rtc::RTC& value); +std::string to_string_datetime(const rtc::RTC& value, const TimeFormat format = YMDHMS); std::string to_string_timestamp(const rtc::RTC& value); #endif/*__STRING_FORMAT_H__*/ diff --git a/firmware/application/ui_adsb_rx.cpp b/firmware/application/ui_adsb_rx.cpp index 4d5497ad..d134c4de 100644 --- a/firmware/application/ui_adsb_rx.cpp +++ b/firmware/application/ui_adsb_rx.cpp @@ -24,7 +24,6 @@ #include "ui_alphanum.hpp" #include "ui_geomap.hpp" -#include "adsb.hpp" #include "string_format.hpp" #include "portapack.hpp" #include "baseband_api.hpp" @@ -33,13 +32,12 @@ #include #include -using namespace adsb; using namespace portapack; namespace ui { template<> -void RecentEntriesTable::draw( +void RecentEntriesTable::draw( const Entry& entry, const Rect& target_rect, Painter& painter, @@ -48,9 +46,15 @@ void RecentEntriesTable::draw( painter.draw_string( target_rect.location(), style, - to_string_hex(entry.ICAO_address, 6) + " " + entry.callsign + " " + (entry.hits <= 9999 ? to_string_dec_uint(entry.hits, 5) : "9999+") + " " + entry.time - //to_string_hex_array((uint8_t*)entry.raw_data, 10) + " " + entry.time + to_string_hex(entry.ICAO_address, 6) + + (entry.pos.valid ? " \x1B\x02" : " ") + + entry.callsign + " \x1B\x00" + + (entry.hits <= 999 ? to_string_dec_uint(entry.hits, 4) : "999+") + " " + + entry.time_string ); + + if (entry.pos.valid) + painter.draw_bitmap(target_rect.location() + Point(15 * 8, 0), bitmap_target, style.foreground, style.background); } void ADSBRxView::focus() { @@ -73,24 +77,35 @@ void ADSBRxView::on_frame(const ADSBFrameMessage * message) { if (frame.check_CRC() && frame.get_ICAO_address()) { + frame.set_rx_timestamp(datetime.minute() * 60 + datetime.second()); + auto& entry = ::on_packet(recent, ICAO_address); rtcGetTime(&RTCD1, &datetime); - str_timestamp = to_string_dec_uint(datetime.hour(), 2, '0') + ":" + - to_string_dec_uint(datetime.minute(), 2, '0') + ":" + - to_string_dec_uint(datetime.second(), 2, '0'); + str_timestamp = to_string_datetime(datetime, HMS); + entry.set_time_string(str_timestamp); - entry.set_time(str_timestamp); - entry.set_raw(frame.get_raw_data()); entry.inc_hit(); if (frame.get_DF() == DF_ADSB) { - if (frame.get_msg_type() == TC_IDENT) { + uint8_t msg_type = frame.get_msg_type(); + uint8_t * raw_data = frame.get_raw_data(); + + if ((msg_type >= 1) && (msg_type <= 4)) { callsign = decode_frame_id(frame); entry.set_callsign(callsign); - } else if (frame.get_msg_type() == TC_AIRBORNE_POS) { - callsign = "Altitude: " + to_string_dec_uint(decode_frame_pos(frame)) + "ft"; - entry.set_pos(callsign); + } else if ((msg_type >= 9) && (msg_type <= 18)) { + entry.set_frame_pos(frame, raw_data[6] & 4); + + if (entry.pos.valid) { + callsign = "Alt:" + to_string_dec_uint(entry.pos.altitude) + + " Lat" + to_string_dec_int(entry.pos.latitude) + + "." + to_string_dec_int((int)(entry.pos.latitude * 1000) % 100) + + " Lon" + to_string_dec_int(entry.pos.longitude) + + "." + to_string_dec_int((int)(entry.pos.longitude * 1000) % 100); + + entry.set_pos_string(callsign); + } } } @@ -98,7 +113,7 @@ void ADSBRxView::on_frame(const ADSBFrameMessage * message) { } } -ADSBRxView::ADSBRxView(NavigationView& nav) { +ADSBRxView::ADSBRxView(NavigationView&) { baseband::run_image(portapack::spi_flash::image_tag_adsb_rx); add_children({ @@ -113,8 +128,10 @@ ADSBRxView::ADSBRxView(NavigationView& nav) { }); recent_entries_view.set_parent_rect({ 0, 64, 240, 224 }); - recent_entries_view.on_select = [this](const ADSBRecentEntry& entry) { - text_debug_a.set(entry.geo_pos); + recent_entries_view.on_select = [this](const AircraftRecentEntry& entry) { + text_debug_a.set(entry.pos_string); + text_debug_b.set(to_string_hex_array(entry.frame_pos_even.get_raw_data(), 14)); + text_debug_c.set(to_string_hex_array(entry.frame_pos_odd.get_raw_data(), 14)); }; baseband::set_adsb(); diff --git a/firmware/application/ui_adsb_rx.hpp b/firmware/application/ui_adsb_rx.hpp index 516a9999..6ec035ef 100644 --- a/firmware/application/ui_adsb_rx.hpp +++ b/firmware/application/ui_adsb_rx.hpp @@ -26,28 +26,30 @@ #include "ui_font_fixed_8x16.hpp" #include "recent_entries.hpp" +#include "adsb.hpp" #include "message.hpp" +using namespace adsb; + namespace ui { -struct ADSBRecentEntry { +struct AircraftRecentEntry { using Key = uint32_t; static constexpr Key invalid_key = 0xffffffff; uint32_t ICAO_address { }; uint16_t hits { 0 }; - uint8_t raw_data[14] { }; // 112 bits at most - std::string callsign { " " }; - std::string time { "" }; - std::string geo_pos { "" }; - - ADSBRecentEntry( - ) : ADSBRecentEntry { 0 } - { - } + adsb_pos pos { false, 0, 0, 0 }; - ADSBRecentEntry( + ADSBFrame frame_pos_even { }; + ADSBFrame frame_pos_odd { }; + + std::string callsign { " " }; + std::string time_string { "" }; + std::string pos_string { "" }; + + AircraftRecentEntry( const uint32_t ICAO_address ) : ICAO_address { ICAO_address } { @@ -65,24 +67,32 @@ struct ADSBRecentEntry { hits++; } - void set_pos(std::string& new_pos) { - geo_pos = new_pos; + void set_frame_pos(ADSBFrame& frame, uint32_t parity) { + if (!parity) + frame_pos_even = frame; + else + frame_pos_odd = frame; + + if (!frame_pos_even.empty() && !frame_pos_odd.empty()) { + if (abs(frame_pos_even.get_rx_timestamp() - frame_pos_odd.get_rx_timestamp()) < 20) + pos = decode_frame_pos(frame_pos_even, frame_pos_odd); + } } - void set_time(std::string& new_time) { - time = new_time; + void set_pos_string(std::string& new_pos_string) { + pos_string = new_pos_string; } - void set_raw(uint8_t * raw_ptr) { - memcpy(raw_data, raw_ptr, 14); + void set_time_string(std::string& new_time_string) { + time_string = new_time_string; } }; -using ADSBRecentEntries = RecentEntries; +using AircraftRecentEntries = RecentEntries; class ADSBRxView : public View { public: - ADSBRxView(NavigationView& nav); + ADSBRxView(NavigationView&); ~ADSBRxView(); void focus() override; @@ -94,12 +104,12 @@ private: const RecentEntriesColumns columns { { { "ICAO", 6 }, - { "Callsign", 8 }, - { "Hits", 5 }, + { "Callsign", 9 }, + { "Hits", 4 }, { "Time", 8 } } }; - ADSBRecentEntries recent { }; - RecentEntriesView> recent_entries_view { columns, recent }; + AircraftRecentEntries recent { }; + RecentEntriesView> recent_entries_view { columns, recent }; RSSI rssi { { 19 * 8, 4, 10 * 8, 8 }, diff --git a/firmware/common/adsb.cpp b/firmware/common/adsb.cpp index 84343710..f06c064d 100644 --- a/firmware/common/adsb.cpp +++ b/firmware/common/adsb.cpp @@ -138,11 +138,11 @@ void encode_frame_squawk(ADSBFrame& frame, const uint32_t squawk) { } float cpr_mod(float a, float b) { - return a - (b * floor(a / b)); + return a - (b * floor(a / b)); } int cpr_NL(float lat) { - if (lat < 0) + if (lat < 0) lat = -lat; // Symmetry for (size_t c = 0; c < 58; c++) { @@ -150,7 +150,7 @@ int cpr_NL(float lat) { return 59 - c; } - return 1; + return 1; } int cpr_N(float lat, int is_odd) { @@ -162,6 +162,10 @@ int cpr_N(float lat, int is_odd) { return nl; } +float cpr_Dlon(float lat, int is_odd) { + return 360.0 / cpr_N(lat, is_odd); +} + void encode_frame_pos(ADSBFrame& frame, const uint32_t ICAO_address, const int32_t altitude, const float latitude, const float longitude, const uint32_t time_parity) { @@ -203,35 +207,76 @@ void encode_frame_pos(ADSBFrame& frame, const uint32_t ICAO_address, const int32 frame.make_CRC(); } -// Decoding method (from dump1090): -// index int j = floor(((59 * latcprE - 60 * latcprO) / 131072) + 0.50) -// latE = DlatE * (cpr_mod(j, 60) + (latcprE / 131072)) -// latO = DlatO * (cpr_mod(j, 59) + (latcprO / 131072)) -// if latE >= 270 -> latE -= 360 -// if latO >= 270 -> latO -= 360 -// if (cpr_NL(latE) != cpr_NL(latO)) return; - -// int ni = cpr_N(latE ,0); -// int m = floor((((loncprE * (cpr_NL(latE) - 1)) - (loncprO * cpr_NL(latE))) / 131072) + 0.5) -// lon = cpr_Dlon(latE, 0) * (cpr_mod(m, ni) + loncprE / 131072); -// lat = latE; -// ... or ... -// int ni = cpr_N(latO ,0); -// int m = floor((((loncprE * (cpr_NL(latO) - 1)) - (loncprO * cpr_NL(latO))) / 131072) + 0.5) -// lon = cpr_Dlon(latO, 0) * (cpr_mod(m, ni) + loncprO / 131072); -// lat = latO; -// ... and ... -// if (lon > 180) lon -= 360; - -// Only altitude is decoded for now -uint32_t decode_frame_pos(ADSBFrame& frame) { - uint8_t * raw_data = frame.get_raw_data(); +// Decoding method from dump1090 +adsb_pos decode_frame_pos(ADSBFrame& frame_even, ADSBFrame& frame_odd) { + uint8_t * raw_data; + uint32_t latcprE, latcprO, loncprE, loncprO; + float latE, latO, m, Dlon; + int ni; + adsb_pos position { false, 0, 0, 0 }; - // Q-bit is present - if (raw_data[5] & 1) - return ((((raw_data[5] >> 1) << 4) | ((raw_data[6] & 0xF0) >> 4)) * 25) - 1000; + uint32_t time_even = frame_even.get_rx_timestamp(); + uint32_t time_odd = frame_odd.get_rx_timestamp(); + uint8_t * frame_data_even = frame_even.get_raw_data(); + uint8_t * frame_data_odd = frame_odd.get_raw_data(); - return 0; + // Return most recent altitude + if (time_even > time_odd) + raw_data = frame_data_even; + else + raw_data = frame_data_odd; + + // Q-bit must be present + if (raw_data[5] & 1) + position.altitude = ((((raw_data[5] & 0xFE) << 3) | ((raw_data[6] & 0xF0) >> 4)) * 25) - 1000; + + // Position + latcprE = ((frame_data_even[6] & 3) << 15) | (frame_data_even[7] << 7) | (frame_data_even[8] >> 1); + loncprE = ((frame_data_even[8] & 1) << 16) | (frame_data_even[9] << 8) | frame_data_even[10]; + + latcprO = ((frame_data_odd[6] & 3) << 15) | (frame_data_odd[7] << 7) | (frame_data_odd[8] >> 1); + loncprO = ((frame_data_odd[8] & 1) << 16) | (frame_data_odd[9] << 8) | frame_data_odd[10]; + + // Compute latitude index + float j = floor((((59.0 * latcprE) - (60.0 * latcprO)) / 131072.0) + 0.5); + latE = (360.0 / 60.0) * (cpr_mod(j, 60) + (latcprE / 131072.0)); + latO = (360.0 / 59.0) * (cpr_mod(j, 59) + (latcprO / 131072.0)); + + if (latE >= 270) latE -= 360; + if (latO >= 270) latO -= 360; + + // Both frames must be in the same latitude zone + if (cpr_NL(latE) != cpr_NL(latO)) + return position; + + // Compute longitude + if (time_even > time_odd) { + // Use even frame + ni = cpr_N(latE, 0); + Dlon = 360.0 / ni; + + m = floor((((loncprE * (cpr_NL(latE) - 1)) - (loncprO * cpr_NL(latE))) / 131072.0) + 0.5); + + position.longitude = Dlon * (cpr_mod(m, ni) + loncprE / 131072.0); + + position.latitude = latE; + } else { + // Use odd frame + ni = cpr_N(latO, 1); + Dlon = 360.0 / ni; + + m = floor((((loncprE * (cpr_NL(latO) - 1)) - (loncprO * cpr_NL(latO))) / 131072.0) + 0.5); + + position.longitude = Dlon * (cpr_mod(m, ni) + loncprO / 131072.0); + + position.latitude = latO; + } + + if (position.longitude > 180) position.longitude -= 360; + + position.valid = true; + + return position; } // speed is in knots diff --git a/firmware/common/adsb.hpp b/firmware/common/adsb.hpp index 8ba94289..374fa8de 100644 --- a/firmware/common/adsb.hpp +++ b/firmware/common/adsb.hpp @@ -49,6 +49,13 @@ enum data_selector { BDS_HEADING = 0x60 }; +struct adsb_pos { + bool valid; + float latitude; + float longitude; + int32_t altitude; +}; + const float adsb_lat_lut[58] = { 10.47047130, 14.82817437, 18.18626357, 21.02939493, 23.54504487, 25.82924707, 27.93898710, 29.91135686, @@ -74,7 +81,8 @@ std::string decode_frame_id(ADSBFrame& frame); void encode_frame_pos(ADSBFrame& frame, const uint32_t ICAO_address, const int32_t altitude, const float latitude, const float longitude, const uint32_t time_parity); -uint32_t decode_frame_pos(ADSBFrame& frame); + +adsb_pos decode_frame_pos(ADSBFrame& frame_even, ADSBFrame& frame_odd); void encode_frame_velo(ADSBFrame& frame, const uint32_t ICAO_address, const uint32_t speed, const float angle, const int32_t v_rate); diff --git a/firmware/common/adsb_frame.hpp b/firmware/common/adsb_frame.hpp index 3ed83daf..36685843 100644 --- a/firmware/common/adsb_frame.hpp +++ b/firmware/common/adsb_frame.hpp @@ -44,6 +44,13 @@ public: uint32_t get_ICAO_address() { return (raw_data[1] << 16) + (raw_data[2] << 8) + raw_data[3]; } + + void set_rx_timestamp(uint32_t timestamp) { + rx_timestamp = timestamp; + } + uint32_t get_rx_timestamp() { + return rx_timestamp; + } void clear() { index = 0; @@ -57,8 +64,8 @@ public: raw_data[index++] = byte; } - uint8_t * get_raw_data() { - return raw_data; + uint8_t * get_raw_data() const { + return (uint8_t* const)raw_data; } void make_CRC() { @@ -73,18 +80,23 @@ public: bool check_CRC() { uint32_t computed_CRC = compute_CRC(); - if (raw_data[11] != ((computed_CRC >> 16) & 0xFF)) return false; - if (raw_data[12] != ((computed_CRC >> 8) & 0xFF)) return false; - if (raw_data[13] != (computed_CRC & 0xFF)) return false; + if ((raw_data[11] != ((computed_CRC >> 16) & 0xFF)) || + (raw_data[12] != ((computed_CRC >> 8) & 0xFF)) || + (raw_data[13] != (computed_CRC & 0xFF))) return false; return true; } + bool empty() { + return (index == 0); + } + private: static const uint8_t adsb_preamble[16]; static const char icao_id_lut[65]; alignas(4) uint8_t index { 0 }; alignas(4) uint8_t raw_data[14] { }; // 112 bits at most + uint32_t rx_timestamp { }; uint32_t compute_CRC() { uint8_t adsb_crc[14] = { 0 }; // Temp buffer diff --git a/firmware/common/ui.cpp b/firmware/common/ui.cpp index f1011463..c5482e73 100644 --- a/firmware/common/ui.cpp +++ b/firmware/common/ui.cpp @@ -26,6 +26,17 @@ namespace ui { +Color term_colors[8] = { + Color::black(), + Color::red(), + Color::green(), + Color::yellow(), + Color::blue(), + Color::magenta(), + Color::cyan(), + Color::white() +}; + bool Rect::contains(const Point p) const { return (p.x() >= left()) && (p.y() >= top()) && (p.x() < right()) && (p.y() < bottom()); diff --git a/firmware/common/ui.hpp b/firmware/common/ui.hpp index e2113bcf..a6625417 100644 --- a/firmware/common/ui.hpp +++ b/firmware/common/ui.hpp @@ -77,7 +77,7 @@ struct Color { return { 255, 175, 0 }; } static constexpr Color dark_orange() { - return { 127, 88, 0 }; + return { 127, 88, 0 }; } static constexpr Color yellow() { @@ -102,7 +102,11 @@ struct Color { } static constexpr Color cyan() { - return { 0, 255, 255 }; + return { 0, 255, 255 }; + } + + static constexpr Color magenta() { + return { 255, 0, 255 }; } static constexpr Color white() { @@ -113,17 +117,19 @@ struct Color { return { 127, 127, 127 }; } static constexpr Color grey() { - return { 91, 91, 91 }; + return { 91, 91, 91 }; } static constexpr Color dark_grey() { - return { 63, 63, 63 }; + return { 63, 63, 63 }; } static constexpr Color purple() { - return { 204, 0, 102 }; + return { 204, 0, 102 }; } }; +extern Color term_colors[8]; + struct ColorRGB888 { uint8_t r; uint8_t g; diff --git a/firmware/common/ui_painter.cpp b/firmware/common/ui_painter.cpp index 70286594..cfa7ad0a 100644 --- a/firmware/common/ui_painter.cpp +++ b/firmware/common/ui_painter.cpp @@ -45,14 +45,26 @@ int Painter::draw_char(const Point p, const Style& style, const char c) { int Painter::draw_string(Point p, const Font& font, const Color foreground, const Color background, const std::string text) { + bool escape = false; size_t width = 0; + Color pen = foreground; for(const auto c : text) { - const auto glyph = font.glyph(c); - display.draw_glyph(p, glyph, foreground, background); - const auto advance = glyph.advance(); - p += advance; - width += advance.x(); + if (escape) { + pen = term_colors[c & 7]; + if (!c) pen = foreground; + escape = false; + } else { + if (c == '\x1B') { + escape = true; + } else { + const auto glyph = font.glyph(c); + display.draw_glyph(p, glyph, pen, background); + const auto advance = glyph.advance(); + p += advance; + width += advance.x(); + } + } } return width; } diff --git a/firmware/common/ui_widget.cpp b/firmware/common/ui_widget.cpp index 3edc0496..82ee290a 100644 --- a/firmware/common/ui_widget.cpp +++ b/firmware/common/ui_widget.cpp @@ -546,12 +546,7 @@ void Console::write(std::string message) { for (const auto c : message) { if (escape) { - if (c == '\x01') - pen_color = ui::Color::red(); - else if (c == '\x02') - pen_color = ui::Color::green(); - else if (c == '\x03') - pen_color = ui::Color::blue(); + pen_color = term_colors[c & 7]; escape = false; } else { if (c == '\n') { diff --git a/firmware/portapack-h1-havoc.bin b/firmware/portapack-h1-havoc.bin index 5adf36d1..42396cbe 100644 Binary files a/firmware/portapack-h1-havoc.bin and b/firmware/portapack-h1-havoc.bin differ