From be0427889ffc79776f497e4d83640a72af3339ad Mon Sep 17 00:00:00 2001 From: Arjan Onwezen Date: Sat, 26 Mar 2022 07:05:13 -0400 Subject: [PATCH] Added country to AIS app, so you know from which country a vessel is. It's using the MID database, which is also part of the PR. mids.db should be placed in /AIS folder on SD card. --- firmware/application/apps/ais_app.cpp | 21 ++ firmware/application/apps/ais_app.hpp | 5 +- firmware/application/database.cpp | 10 + firmware/application/database.hpp | 6 + .../MaritimeIdentificationDigits.csv | 295 ++++++++++++++++++ firmware/tools/make_mids_db/README.md | 13 + firmware/tools/make_mids_db/make_mid_db.py | 55 ++++ firmware/tools/make_mids_db/mids.db | Bin 0 -> 10584 bytes sdcard/AIS/mids.db | Bin 0 -> 10584 bytes 9 files changed, 403 insertions(+), 2 deletions(-) create mode 100755 firmware/tools/make_mids_db/MaritimeIdentificationDigits.csv create mode 100644 firmware/tools/make_mids_db/README.md create mode 100755 firmware/tools/make_mids_db/make_mid_db.py create mode 100644 firmware/tools/make_mids_db/mids.db create mode 100644 sdcard/AIS/mids.db diff --git a/firmware/application/apps/ais_app.cpp b/firmware/application/apps/ais_app.cpp index ee7527c3..06a36a6f 100644 --- a/firmware/application/apps/ais_app.cpp +++ b/firmware/application/apps/ais_app.cpp @@ -22,6 +22,7 @@ #include "ais_app.hpp" #include "string_format.hpp" +#include "database.hpp" #include "baseband_api.hpp" @@ -62,6 +63,25 @@ static std::string mmsi( return to_string_dec_uint(mmsi, 9, '0'); // MMSI is always is always 9 characters pre-padded with zeros } + +static std::string mid( + const ais::MMSI& mmsi +) { + std::database db; + std::string mid_code = ""; + std::database::MidDBRecord mid_record = {}; + int return_code = 0; + + // Try getting the country name from mids.db using MID code for given MMSI + mid_code = to_string_dec_uint(mmsi, 9, ' ').substr(0, 3); + return_code = db.retrieve_mid_record(&mid_record, mid_code); + switch(return_code) { + case DATABASE_RECORD_FOUND: return mid_record.country; + case DATABASE_NOT_FOUND: return "No mids.db file"; + default: return ""; + } +} + static std::string navigational_status(const unsigned int value) { switch(value) { case 0: return "under way w/engine"; @@ -271,6 +291,7 @@ void AISRecentEntryDetailView::paint(Painter& painter) { auto field_rect = Rect { rect.left(), rect.top() + 16, rect.width(), 16 }; field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi)); + field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi)); field_rect = draw_field(painter, field_rect, s, "Name", entry_.name); field_rect = draw_field(painter, field_rect, s, "Call", entry_.call_sign); field_rect = draw_field(painter, field_rect, s, "Dest", entry_.destination); diff --git a/firmware/application/apps/ais_app.hpp b/firmware/application/apps/ais_app.hpp index e0093d8b..c7f03aca 100644 --- a/firmware/application/apps/ais_app.hpp +++ b/firmware/application/apps/ais_app.hpp @@ -131,11 +131,11 @@ private: AISRecentEntry entry_ { }; Button button_done { - { 125, 216, 96, 24 }, + { 125, 224, 96, 24 }, "Done" }; Button button_see_map { - { 19, 216, 96, 24 }, + { 19, 224, 96, 24 }, "See on map" }; GeoMapView* geomap_view { nullptr }; @@ -169,6 +169,7 @@ private: static constexpr uint32_t initial_target_frequency = 162025000; static constexpr uint32_t sampling_rate = 2457600; static constexpr uint32_t baseband_bandwidth = 1750000; + NavigationView& nav_; AISRecentEntries recent { }; diff --git a/firmware/application/database.cpp b/firmware/application/database.cpp index edac3ef7..54bc1de4 100644 --- a/firmware/application/database.cpp +++ b/firmware/application/database.cpp @@ -27,6 +27,16 @@ namespace std { +int database::retrieve_mid_record(MidDBRecord* record, std::string search_term){ + + file_path = "AIS/mids.db"; + index_item_length = 4; + record_length = 32; + + result = std::database::retrieve_record(file_path, index_item_length, record_length, record, search_term); + + return(result); +} int database::retrieve_airline_record(AirlinesDBRecord* record, std::string search_term){ diff --git a/firmware/application/database.hpp b/firmware/application/database.hpp index 7159f6b1..9dee1a61 100644 --- a/firmware/application/database.hpp +++ b/firmware/application/database.hpp @@ -44,6 +44,12 @@ public: #define DATABASE_NOT_FOUND -1 // database not found / could not be opened #define DATABASE_RECORD_NOT_FOUND -2 // record could not be found in database + struct MidDBRecord { + char country[32]; // country name + }; + + int retrieve_mid_record(MidDBRecord* record, std::string search_term); + struct AirlinesDBRecord { char airline[32]; // airline name char country[32]; // country name diff --git a/firmware/tools/make_mids_db/MaritimeIdentificationDigits.csv b/firmware/tools/make_mids_db/MaritimeIdentificationDigits.csv new file mode 100755 index 00000000..744218e7 --- /dev/null +++ b/firmware/tools/make_mids_db/MaritimeIdentificationDigits.csv @@ -0,0 +1,295 @@ +Digit,Allocated to +201,Albania (Republic of) +202,Andorra (Principality of) +203,Austria +204,Portugal - Azores +205,Belgium +206,Belarus (Republic of) +207,Bulgaria (Republic of) +208,Vatican City State +209,Cyprus (Republic of) +210,Cyprus (Republic of) +211,Germany (Federal Republic of) +212,Cyprus (Republic of) +213,Georgia +214,Moldova (Republic of) +215,Malta +216,Armenia (Republic of) +218,Germany (Federal Republic of) +219,Denmark +220,Denmark +224,Spain +225,Spain +226,France +227,France +228,France +229,Malta +230,Finland +231,Denmark - Faroe Islands +232,United Kingdom of Great Britain and Northern Ireland +233,United Kingdom of Great Britain and Northern Ireland +234,United Kingdom of Great Britain and Northern Ireland +235,United Kingdom of Great Britain and Northern Ireland +236,United Kingdom of Great Britain and Northern Ireland - Gibraltar +237,Greece +238,Croatia (Republic of) +239,Greece +240,Greece +241,Greece +242,Morocco (Kingdom of) +243,Hungary +244,Netherlands (Kingdom of the) +245,Netherlands (Kingdom of the) +246,Netherlands (Kingdom of the) +247,Italy +248,Malta +249,Malta +250,Ireland +251,Iceland +252,Liechtenstein (Principality of) +253,Luxembourg +254,Monaco (Principality of) +255,Portugal - Madeira +256,Malta +257,Norway +258,Norway +259,Norway +261,Poland (Republic of) +262,Montenegro +263,Portugal +264,Romania +265,Sweden +266,Sweden +267,Slovak Republic +268,San Marino (Republic of) +269,Switzerland (Confederation of) +270,Czech Republic +271,Turkey +272,Ukraine +273,Russian Federation +274,North Macedonia (Republic of) +275,Latvia (Republic of) +276,Estonia (Republic of) +277,Lithuania (Republic of) +278,Slovenia (Republic of) +279,Serbia (Republic of) +301,United Kingdom of Great Britain and Northern Ireland - Anguilla +303,United States of America - Alaska (State of) +304,Antigua and Barbuda +305,Antigua and Barbuda +306,"Netherlands (Kingdom of the) - Bonaire, Sint Eustatius and Saba" +306,Netherlands (Kingdom of the) - Curaçao +306,Netherlands (Kingdom of the) - Sint Maarten (Dutch part) +307,Netherlands (Kingdom of the) - Aruba +308,Bahamas (Commonwealth of the) +309,Bahamas (Commonwealth of the) +310,United Kingdom of Great Britain and Northern Ireland - Bermuda +311,Bahamas (Commonwealth of the) +312,Belize +314,Barbados +316,Canada +319,United Kingdom of Great Britain and Northern Ireland - Cayman Islands +321,Costa Rica +323,Cuba +325,Dominica (Commonwealth of) +327,Dominican Republic +329,France - Guadeloupe (French Department of) +330,Grenada +331,Denmark - Greenland +332,Guatemala (Republic of) +334,Honduras (Republic of) +336,Haiti (Republic of) +338,United States of America +339,Jamaica +341,Saint Kitts and Nevis (Federation of) +343,Saint Lucia +345,Mexico +347,France - Martinique (French Department of) +348,United Kingdom of Great Britain and Northern Ireland - Montserrat +350,Nicaragua +351,Panama (Republic of) +352,Panama (Republic of) +353,Panama (Republic of) +354,Panama (Republic of) +355,Panama (Republic of) +356,Panama (Republic of) +357,Panama (Republic of) +358,United States of America - Puerto Rico +359,El Salvador (Republic of) +361,France - Saint Pierre and Miquelon (Territorial Collectivity of) +362,Trinidad and Tobago +364,United Kingdom of Great Britain and Northern Ireland - Turks and Caicos Islands +366,United States of America +367,United States of America +368,United States of America +369,United States of America +370,Panama (Republic of) +371,Panama (Republic of) +372,Panama (Republic of) +373,Panama (Republic of) +374,Panama (Republic of) +375,Saint Vincent and the Grenadines +376,Saint Vincent and the Grenadines +377,Saint Vincent and the Grenadines +378,United Kingdom of Great Britain and Northern Ireland - British Virgin Islands +379,United States of America - United States Virgin Islands +401,Afghanistan +403,Saudi Arabia (Kingdom of) +405,Bangladesh (People's Republic of) +408,Bahrain (Kingdom of) +410,Bhutan (Kingdom of) +412,China (People's Republic of) +413,China (People's Republic of) +414,China (People's Republic of) +416,China (People's Republic of) - Taiwan (Province of China) +417,Sri Lanka (Democratic Socialist Republic of) +419,India (Republic of) +422,Iran (Islamic Republic of) +423,Azerbaijan (Republic of) +425,Iraq (Republic of) +428,Israel (State of) +431,Japan +432,Japan +434,Turkmenistan +436,Kazakhstan (Republic of) +437,Uzbekistan (Republic of) +438,Jordan (Hashemite Kingdom of) +440,Korea (Republic of) +441,Korea (Republic of) +443,"State of Palestine (In accordance with Resolution 99 Rev. Dubai, 2018)" +445,Democratic People's Republic of Korea +447,Kuwait (State of) +450,Lebanon +451,Kyrgyz Republic +453,China (People's Republic of) - Macao (Special Administrative Region of China) +455,Maldives (Republic of) +457,Mongolia +459,Nepal (Federal Democratic Republic of) +461,Oman (Sultanate of) +463,Pakistan (Islamic Republic of) +466,Qatar (State of) +468,Syrian Arab Republic +470,United Arab Emirates +471,United Arab Emirates +472,Tajikistan (Republic of) +473,Yemen (Republic of) +475,Yemen (Republic of) +477,China (People's Republic of) - Hong Kong (Special Administrative Region of China) +478,Bosnia and Herzegovina +501,France - Adelie Land +503,Australia +506,Myanmar (Union of) +508,Brunei Darussalam +510,Micronesia (Federated States of) +511,Palau (Republic of) +512,New Zealand +514,Cambodia (Kingdom of) +515,Cambodia (Kingdom of) +516,Australia - Christmas Island (Indian Ocean) +518,New Zealand - Cook Islands +520,Fiji (Republic of) +523,Australia - Cocos (Keeling) Islands +525,Indonesia (Republic of) +529,Kiribati (Republic of) +531,Lao People's Democratic Republic +533,Malaysia +536,United States of America - Northern Mariana Islands (Commonwealth of the) +538,Marshall Islands (Republic of the) +540,France - New Caledonia +542,New Zealand - Niue +544,Nauru (Republic of) +546,France - French Polynesia +548,Philippines (Republic of the) +550,Timor-Leste (Democratic Republic of) +553,Papua New Guinea +555,United Kingdom of Great Britain and Northern Ireland - Pitcairn Island +557,Solomon Islands +559,United States of America - American Samoa +561,Samoa (Independent State of) +563,Singapore (Republic of) +564,Singapore (Republic of) +565,Singapore (Republic of) +566,Singapore (Republic of) +567,Thailand +570,Tonga (Kingdom of) +572,Tuvalu +574,Viet Nam (Socialist Republic of) +576,Vanuatu (Republic of) +577,Vanuatu (Republic of) +578,France - Wallis and Futuna Islands +601,South Africa (Republic of) +603,Angola (Republic of) +605,Algeria (People's Democratic Republic of) +607,France - Saint Paul and Amsterdam Islands +608,United Kingdom of Great Britain and Northern Ireland - Ascension Island +609,Burundi (Republic of) +610,Benin (Republic of) +611,Botswana (Republic of) +612,Central African Republic +613,Cameroon (Republic of) +615,Congo (Republic of the) +616,Comoros (Union of the) +617,Cabo Verde (Republic of) +618,France - Crozet Archipelago +619,Côte d'Ivoire (Republic of) +620,Comoros (Union of the) +621,Djibouti (Republic of) +622,Egypt (Arab Republic of) +624,Ethiopia (Federal Democratic Republic of) +625,Eritrea +626,Gabonese Republic +627,Ghana +629,Gambia (Republic of the) +630,Guinea-Bissau (Republic of) +631,Equatorial Guinea (Republic of) +632,Guinea (Republic of) +633,Burkina Faso +634,Kenya (Republic of) +635,France - Kerguelen Islands +636,Liberia (Republic of) +637,Liberia (Republic of) +638,South Sudan (Republic of) +642,Libya (State of) +644,Lesotho (Kingdom of) +645,Mauritius (Republic of) +647,Madagascar (Republic of) +649,Mali (Republic of) +650,Mozambique (Republic of) +654,Mauritania (Islamic Republic of) +655,Malawi +656,Niger (Republic of the) +657,Nigeria (Federal Republic of) +659,Namibia (Republic of) +660,France - Reunion (French Department of) +661,Rwanda (Republic of) +662,Sudan (Republic of the) +663,Senegal (Republic of) +664,Seychelles (Republic of) +665,United Kingdom of Great Britain and Northern Ireland - Saint Helena +666,Somalia (Federal Republic of) +667,Sierra Leone +668,Sao Tome and Principe (Democratic Republic of) +669,Eswatini (Kingdom of) +670,Chad (Republic of) +671,Togolese Republic +672,Tunisia +674,Tanzania (United Republic of) +675,Uganda (Republic of) +676,Democratic Republic of the Congo +677,Tanzania (United Republic of) +678,Zambia (Republic of) +679,Zimbabwe (Republic of) +701,Argentine Republic +710,Brazil (Federative Republic of) +720,Bolivia (Plurinational State of) +725,Chile +730,Colombia (Republic of) +735,Ecuador +740,United Kingdom of Great Britain and Northern Ireland - Falkland Islands (Malvinas) +745,France - Guiana (French Department of) +750,Guyana +755,Paraguay (Republic of) +760,Peru +765,Suriname (Republic of) +770,Uruguay (Eastern Republic of) +775,Venezuela (Bolivarian Republic of) diff --git a/firmware/tools/make_mids_db/README.md b/firmware/tools/make_mids_db/README.md new file mode 100644 index 00000000..f99ced74 --- /dev/null +++ b/firmware/tools/make_mids_db/README.md @@ -0,0 +1,13 @@ +# Make mids.db + +Licensed under [GNU GPL v3](../../../LICENSE) + +Python3 script creates a MID (Marine Identification Digit) database. +MID is part of MMSI and determines (among other things) the conutry. + + +USAGE: + - Copy Excel file from https://www.itu.int/en/ITU-R/terrestrial/fmd/Pages/mid.aspx + - Convert it to a csv document + - Run Python 3 script: `./make_mids_db.py` + - Copy file to /AIS folder on SDCARD diff --git a/firmware/tools/make_mids_db/make_mid_db.py b/firmware/tools/make_mids_db/make_mid_db.py new file mode 100755 index 00000000..43615887 --- /dev/null +++ b/firmware/tools/make_mids_db/make_mid_db.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2022 ArjanOnwezen +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +# ------------------------------------------------------------------------------------- +# Create mids.db, used for AIS receiver application, using converted Excel file on page: +# https://www.itu.int/en/ITU-R/terrestrial/fmd/Pages/mid.aspx +# as a source. +# MID stands for Marine Identification Digits and can be used to determine conutry of +# vessel or coastal station. +# ------------------------------------------------------------------------------------- +import csv +import re +import unicodedata +mid_codes=bytearray() +countries=bytearray() +row_count=0 + +database=open("mids.db", "wb") + +with open('MaritimeIdentificationDigits.csv', 'rt') as csv_file: + sorted_lines=sorted(csv_file.readlines()) + + for row in csv.reader(sorted_lines, quotechar='"', delimiter=',', quoting=csv.QUOTE_ALL, skipinitialspace=True): + mid_code=row[0] + # Normalize some unicode characters +#unicodedata.normalize('NFKD', row[3][:32]).encode('ascii', 'ignore') + country=unicodedata.normalize('NFKD', "".join(re.split("\(|\)|\[|\]", row[1].split('-')[-1].replace(" of Great Britain and Northern Ireland" , ""))[::2])).replace("Democratic People's Republic of Korea", "North-Korea").strip().encode('ascii', 'ignore')[:32] + if len(mid_code) == 3 : + country_padding=bytearray() + print(mid_code,' - ', country) + mid_codes=mid_codes+bytearray(mid_code+'\0', encoding='ascii') + country_padding=bytearray('\0' * (32 - len(country)), encoding='ascii') + countries=countries+country+country_padding + row_count+=1 + +database.write(mid_codes+countries) +print("Total of", row_count, "MID codes stored in database") + diff --git a/firmware/tools/make_mids_db/mids.db b/firmware/tools/make_mids_db/mids.db new file mode 100644 index 0000000000000000000000000000000000000000..82f6373dab0abb252502ebbab5b08e4c6e3d0188 GIT binary patch literal 10584 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 bNO+^e9W=pL-_RTnPM