fumiのアイコン画像
fumi 2024年01月30日作成 (2024年03月04日更新) © MIT
製作品 製作品 閲覧数 458
fumi 2024年01月30日作成 (2024年03月04日更新) © MIT 製作品 製作品 閲覧数 458

SPRESENSEのカメラと液晶とGPSで私も運転手気分

SPRESENSEのカメラと液晶とGPSで私も運転手気分

説明

カメラで撮影した画像に速度を表示して運転手の気分を味わえるデバイスを作りました。

  • 画面:カメラで撮影した画像を表示
  • 左上:受信しているGPS衛星の数
  • 右上:時刻(JST)
  • 左下:緯度経度(画像はGPSデータのマスク関数を通している場合)
  • 右下:速度

夜道でも撮影できるSpresense HDRカメラボードが素晴らしく、5fps〜10fpsくらいで表示できます。(目で見た感覚)
Serial出力で確認したら室内でも6fps出てました。

目次

部品

  • Spresense メインボード
  • Spresense 拡張ボード
  • Spresense HDRカメラボード
  • ILI9341搭載2.8インチSPI制御タッチパネル付TFT液晶 MSP2807
  • 配線(液晶と拡張ボードを接続)
  • ダンボール

組み立て手順

  1. Spresense メインボード に Spresense HDRカメラボード(フレキシブル)を接続。
    ケーブルの表と裏に注意。
    参考:HDR カメラボードの接続方法
  2. Spresense メインボード を Spresense 拡張ボードに接続。
    押し込むだけ。
  3. ILI9341 を Spresense 拡張ボードに接続。
    参考:Spresense Arduino チュートリアル
    キャプションを入力できます

接続イメージ:
キャプションを入力できます

ソースコード

Arduinoで開発しました。
ソースはスケッチと液晶表示の2つです。

スケッチ(Spresense_LCD_Camera_GPS.ino)

#include <SPI.h> #include "LCD.hpp" static bool islatlonmaskgps = true; #define TFT_CS -1 #define TFT_RST 8 #define TFT_DC 9 static LGFX lcd; static LGFX_Sprite canvas(&lcd); static LGFX_Sprite gridlayer(&canvas); static LGFX_Sprite speedmeter(&canvas); static LGFX_Sprite gpslayer(&canvas); static LGFX_Sprite clocklayer(&canvas); static LGFX_Sprite satellitelayer(&canvas); // static LGFX_Sprite informationlayer(&canvas); #include <RTC.h> #include <GNSS.h> #define MY_TIMEZONE_IN_SECONDS (9 * 60 * 60) // JST enum ParamSat { eSatGps, /**< GPS World wide coverage */ eSatGlonass, /**< GLONASS World wide coverage */ eSatGpsSbas, /**< GPS+SBAS North America */ eSatGpsGlonass, /**< GPS+Glonass World wide coverage */ eSatGpsBeidou, /**< GPS+BeiDou World wide coverage */ eSatGpsGalileo, /**< GPS+Galileo World wide coverage */ eSatGpsQz1c, /**< GPS+QZSS_L1CA East Asia & Oceania */ eSatGpsGlonassQz1c, /**< GPS+Glonass+QZSS_L1CA East Asia & Oceania */ eSatGpsBeidouQz1c, /**< GPS+BeiDou+QZSS_L1CA East Asia & Oceania */ eSatGpsGalileoQz1c, /**< GPS+Galileo+QZSS_L1CA East Asia & Oceania */ eSatGpsQz1cQz1S, /**< GPS+QZSS_L1CA+QZSS_L1S Japan */ }; SpGnss Gnss; unsigned char posFixMode; unsigned char numSatellites; float latitude; float longitude; float velocity; char gpsdate[11]; char gpstime[9]; #include <SDHCI.h> #include <stdio.h> #include <Camera.h> #define BAUDRATE (115200) #define TOTAL_PICTURE_COUNT (10) // Accero SDClass theSD; int take_picture_count = 0; static enum ParamSat satType = eSatGps; void setup_sd() { /* Initialize SD */ while ( !theSD.begin() ) { /* wait until SD card is mounted. */ Serial.println("Insert SD card."); } } void setup_camera() { CamErr err; /* begin() without parameters means that * number of buffers = 1, 30FPS, QVGA, YUV 4:2:2 format */ Serial.println("Prepare camera"); err = theCamera.begin(); if (err != CAM_ERR_SUCCESS) { printError(err); } /* Start video stream. * If received video stream data from camera device, * camera library call CamCB. */ Serial.println("Start streaming"); err = theCamera.startStreaming(true, CamCB); if (err != CAM_ERR_SUCCESS) { printError(err); } /* Auto white balance configuration */ Serial.println("Set Auto white balance parameter"); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); if (err != CAM_ERR_SUCCESS) { printError(err); } /* Set parameters about still picture. * In the following case, QUADVGA and JPEG. */ Serial.println("Set still picture format"); err = theCamera.setStillPictureImageFormat( CAM_IMGSIZE_QUADVGA_H, CAM_IMGSIZE_QUADVGA_V, CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { printError(err); } } void setup_lcd() { lcd.init(); lcd.setSwapBytes(true); lcd.setRotation(1); canvas.setColorDepth(16); canvas.createSprite(320, 240); canvas.setSwapBytes(true); gridlayer.setColorDepth(1); gridlayer.createSprite(320, 240); gridlayer.createPalette(); gridlayer.setPaletteColor(1, 0x7F7F7Fu); for (int x = 1; x < 32; x++) { gridlayer.drawFastVLine(x*10-1, 0, 240, 1); } for (int y = 0; y < 24; y++) { gridlayer.drawFastHLine(0, y*10-1, 320, 1); } speedmeter.setColorDepth(1); speedmeter.createSprite(168, 52); speedmeter.createPalette(); speedmeter.setPaletteColor(1, 0xFFFFFFu); // informationlayer.setColorDepth(1); // informationlayer.createSprite(100, 96); // informationlayer.createPalette(); // informationlayer.setPaletteColor(1, 0xFFFFFFu); // informationlayer.setTextColor(1); // informationlayer.setTextFont(2); gpslayer.setColorDepth(1); gpslayer.createSprite(69, 18); gpslayer.createPalette(); gpslayer.setPaletteColor(1, 0xFFFFFFu); gpslayer.setTextColor(1); gpslayer.setTextFont(1); clocklayer.setColorDepth(1); clocklayer.createSprite(146, 52); clocklayer.createPalette(); clocklayer.setPaletteColor(1, 0xFFFFFFu); clocklayer.setTextColor(1); clocklayer.setTextFont(7); satellitelayer.setColorDepth(1); satellitelayer.createSprite(118, 52); satellitelayer.createPalette(); satellitelayer.setPaletteColor(1, 0xFFFFFFu); satellitelayer.setTextColor(1); satellitelayer.setTextFont(7); } void setup_gps() { int result; /* Activate GNSS device */ result = Gnss.begin(); if (result != 0) { Serial.println("Gnss begin error!!"); } else { /* Setup GNSS * It is possible to setup up to two GNSS satellites systems. * Depending on your location you can improve your accuracy by selecting different GNSS system than the GPS system. * See: https://developer.sony.com/develop/spresense/developer-tools/get-started-using-nuttx/nuttx-developer-guide#_gnss * for detailed information. */ switch (satType) { case eSatGps: Gnss.select(GPS); break; case eSatGpsSbas: Gnss.select(GPS); Gnss.select(SBAS); break; case eSatGlonass: Gnss.select(GLONASS); Gnss.deselect(GPS); break; case eSatGpsGlonass: Gnss.select(GPS); Gnss.select(GLONASS); break; case eSatGpsBeidou: Gnss.select(GPS); Gnss.select(BEIDOU); break; case eSatGpsGalileo: Gnss.select(GPS); Gnss.select(GALILEO); break; case eSatGpsQz1c: Gnss.select(GPS); Gnss.select(QZ_L1CA); break; case eSatGpsQz1cQz1S: Gnss.select(GPS); Gnss.select(QZ_L1CA); Gnss.select(QZ_L1S); break; case eSatGpsBeidouQz1c: Gnss.select(GPS); Gnss.select(BEIDOU); Gnss.select(QZ_L1CA); break; case eSatGpsGalileoQz1c: Gnss.select(GPS); Gnss.select(GALILEO); Gnss.select(QZ_L1CA); break; case eSatGpsGlonassQz1c: default: Gnss.select(GPS); Gnss.select(GLONASS); Gnss.select(QZ_L1CA); break; } /* Start positioning */ result = Gnss.start(COLD_START); if (result != 0) { Serial.println("Gnss start error!!"); } else { Serial.println("Gnss setup OK"); } } } void maskgps(char *buf) { if (islatlonmaskgps) { buf[1] = '*'; buf[2] = '*'; buf[3] = '*'; buf[5] = '*'; buf[6] = '*'; buf[7] = '*'; } } String latlon2str(float latlon, const char cp, const char cm) { String latlonstr = "-123.456789"; char buf1[12]; char buf2[12]; if (latlon > 0) { sprintf(buf1, "%9.6f", latlon); sprintf(buf2, "%c%10s", cp, buf1); maskgps(buf2); buf2[11]=0; latlonstr = String(buf2); } else if (latlon < 0) { sprintf(buf1, "%9.6f", abs(latlon)); sprintf(buf2, "%c%10s", cm, buf1); maskgps(buf2); buf2[11]=0; latlonstr = String(buf2); } else { latlonstr = " 0.000000"; if (maskgps) { latlonstr = cp + "***.***000"; } } return latlonstr; } void setup() { /* Open serial communications and wait for port to open */ Serial.begin(BAUDRATE); while (!Serial){;} Serial.println("Hello"); RTC.begin(); setup_gps(); setup_lcd(); lcd.endTransaction(); bool gpsok = false; while(!gpsok) { if (Gnss.waitUpdate()) { SpNavData NavData; Gnss.getNavData(&NavData); Serial.printf("%04d/%02d/%02d ", NavData.time.year, NavData.time.month, NavData.time.day); Serial.printf("%02d:%02d:%02d ", NavData.time.hour, NavData.time.minute, NavData.time.sec); Serial.printf("numSat:%02d Lat:%s Lon:%s vel:%05.1f\n" , NavData.numSatellites , latlon2str(NavData.latitude, 'N', 'S').c_str() , latlon2str(NavData.longitude, 'E', 'W').c_str() , NavData.velocity * 3.6); gpsok = (NavData.posFixMode != FixInvalid); } } lcd.beginTransaction(); setup_sd(); setup_camera(); } void printError(enum CamErr err) { Serial.print("Error: "); switch (err) { case CAM_ERR_NO_DEVICE: Serial.println("No Device"); break; case CAM_ERR_ILLEGAL_DEVERR: Serial.println("Illegal device error"); break; case CAM_ERR_ALREADY_INITIALIZED: Serial.println("Already initialized"); break; case CAM_ERR_NOT_INITIALIZED: Serial.println("Not initialized"); break; case CAM_ERR_NOT_STILL_INITIALIZED: Serial.println("Still picture not initialized"); break; case CAM_ERR_CANT_CREATE_THREAD: Serial.println("Failed to create thread"); break; case CAM_ERR_INVALID_PARAM: Serial.println("Invalid parameter"); break; case CAM_ERR_NO_MEMORY: Serial.println("No memory"); break; case CAM_ERR_USR_INUSED: Serial.println("Buffer already in use"); break; case CAM_ERR_NOT_PERMITTED: Serial.println("Operation not permitted"); break; default: break; } } /** * Callback from Camera library when video frame is captured. */ void CamCB(CamImage img) { /* Check the img instance is available or not. */ if (img.isAvailable()) { /* If you want RGB565 data, convert image data format to RGB565 */ // Serial.println("Convert Image Start"); img.convertPixFormat(CAM_IMAGE_PIX_FMT_RGB565); // Serial.println("Convert Image End"); /* You can use image data directly by using getImgSize() and getImgBuff(). * for displaying image to a display, etc. */ // Serial.println("Display Image Start"); canvas.pushImage(0, 0, 320, 240, (uint16_t *)img.getImgBuff()); // gridlayer.pushSprite(0, 0, 0); clocklayer.pushRotateZoom(310 - 146/4, 10 + 52/4, 0, 0.5, 0.5); speedmeter.pushRotateZoom(310 - 168/4, 230 - 52/4, 0, 0.5, 0.5); satellitelayer.pushRotateZoom(10 + 118/4, 10 + 52/4, 0, 0.5, 0.5); gpslayer.pushSprite(10, 212); // informationlayer.pushSprite(0, 0); canvas.pushSprite(0, 0); // Serial.printf("numSat:%02d Lat:%09.6f Lon:%09.6f vel:%03.0f\n", numSatellites, latitude, longitude, velocity); // Serial.println("Display Image End"); Serial.println("."); } else { Serial.println("Failed to get video stream image"); } } void takePicture() { /* theCamera.setStillPictureImageFormat( * CAM_IMGSIZE_HD_H, * CAM_IMGSIZE_HD_V, * CAM_IMAGE_PIX_FMT_JPG); */ /* Take still picture. * Unlike video stream(startStreaming) , this API wait to receive image data * from camera device. */ Serial.println("call takePicture()"); CamImage img = theCamera.takePicture(); /* Check availability of the img instance. */ /* If any errors occur, the img is not available. */ if (img.isAvailable()) { /* Create file name */ char filename[19] = {0}; sprintf(filename, "PICT%06d.JPG", take_picture_count); Serial.print("Save taken picture as "); Serial.print(filename); Serial.println(""); /* Remove the old file with the same file name as new created file, * and create new file. */ theSD.remove(filename); File myFile = theSD.open(filename, FILE_WRITE); myFile.write(img.getImgBuff(), img.getImgSize()); myFile.close(); } else { /* The size of a picture may exceed the allocated memory size. * Then, allocate the larger memory size and/or decrease the size of a picture. * [How to allocate the larger memory] * - Decrease jpgbufsize_divisor specified by setStillPictureImageFormat() * - Increase the Memory size from Arduino IDE tools Menu * [How to decrease the size of a picture] * - Decrease the JPEG quality by setJPEGQuality() */ Serial.println("Failed to take picture"); } } void setRtc(SpNavData* pNavData) { SpGnssTime time = pNavData->time; RtcTime now = RTC.getTime(); RtcTime gps(time.year, time.month, time.day, time.hour, time.minute, time.sec, time.usec * 1000); gps += MY_TIMEZONE_IN_SECONDS; int diff = now - gps; if (abs(diff) >= 1) { RTC.setTime(gps); Serial.println("Setup RTC."); } } /** * @brief Take picture with format JPEG per second */ void loop() { // sleep(1); /* wait for one second to take still picture. */ // delay(1000); if (Gnss.waitUpdate()) { SpNavData NavData; Gnss.getNavData(&NavData); if (NavData.time.year >= 2000) { setRtc(&NavData); } posFixMode = NavData.posFixMode; numSatellites = NavData.numSatellites; latitude = NavData.latitude; longitude = NavData.longitude; velocity = NavData.velocity * 3.6; // informationlayer.fillScreen(0); // informationlayer.setCursor(0, 0); // informationlayer.setTextColor(1); RtcTime now = RTC.getTime(); // informationlayer.printf(" %04d/%02d/%02d\n", now.year(), now.month(), now.day()); // informationlayer.printf(" %02d:%02d:%02d\n", now.hour(), now.minute(), now.second()); // informationlayer.printf(" numSat:%2d ", numSatellites); // if (NavData.posFixMode != FixInvalid) // { // informationlayer.print("fix:x\n"); // } else { // informationlayer.print("fix:o\n"); // } gpslayer.fillScreen(0); gpslayer.setCursor(2, 1); gpslayer.printf("%s", latlon2str(latitude, 'N', 'S').c_str()); gpslayer.setCursor(2, 10); gpslayer.printf("%s", latlon2str(longitude, 'E', 'W').c_str()); // informationlayer.printf(" velocity:%05.1f", velocity); // Serial.println("GNSS Update."); speedmeter.fillScreen(0); speedmeter.setTextFont(7); speedmeter.setCursor(1, 1); speedmeter.printf("%03.0f", velocity); speedmeter.setTextFont(4); speedmeter.setCursor(100, 22); speedmeter.print("km/h"); clocklayer.fillScreen(0); clocklayer.setCursor(1, 1); clocklayer.printf("%02d:%02d", now.hour(), now.minute()); satellitelayer.fillScreen(0); satellitelayer.fillRect( 2, 24-4, 44, 9, 1); satellitelayer.fillCircle(24, 24, 8, 0); satellitelayer.fillCircle(24, 24, 6, 1); satellitelayer.setCursor(52, 1); satellitelayer.printf("%02d", NavData.numSatellites); sprintf(gpsdate, "%04d/%02d/%02d", now.year(), now.month(), now.day()); sprintf(gpstime, "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); Serial.printf("%s %s numSat:%02d Lat:%s Lon:%s vel:%05.1f\n" , gpsdate , gpstime , numSatellites , latlon2str(latitude, 'N', 'S').c_str() , latlon2str(longitude, 'E', 'W').c_str() , velocity); } }

液晶表示(LCD.hpp)

#pragma once #define LGFX_USE_V1 #include <LovyanGFX.hpp> // SPRESENSEでLovyanGFXを独自設定で利用する場合の設定例 /* このファイルを複製し、新しい名前を付けて、環境に合わせて設定内容を変更してください。 作成したファイルをユーザープログラムからincludeすることで利用可能になります。 複製したファイルはライブラリのlgfx_userフォルダに置いて利用しても構いませんが、 その場合はライブラリのアップデート時に削除される可能性があるのでご注意ください。 安全に運用したい場合はバックアップを作成しておくか、ユーザープロジェクトのフォルダに置いてください。 //*/ /// 独自の設定を行うクラスを、LGFX_Deviceから派生して作成します。 class LGFX : public lgfx::LGFX_Device { /* クラス名は"LGFX"から別の名前に変更しても構いません。 AUTODETECTと併用する場合は"LGFX"は使用されているため、LGFX以外の名前に変更してください。 また、複数枚のパネルを同時使用する場合もそれぞれに異なる名前を付けてください。 ※ クラス名を変更する場合はコンストラクタの名前も併せて同じ名前に変更が必要です。 名前の付け方は自由に決めて構いませんが、設定が増えた場合を想定し、 例えば SPRESENSE でSPI接続のILI9341の設定を行った場合、 LGFX_SPRESENSE_SPI_ILI9341 のような名前にし、ファイル名とクラス名を一致させておくことで、利用時に迷いにくくなります。 //*/ // 接続するパネルの型にあったインスタンスを用意します。 //lgfx::Panel_GC9A01 _panel_instance; //lgfx::Panel_GDEW0154M09 _panel_instance; //lgfx::Panel_HX8357B _panel_instance; //lgfx::Panel_HX8357D _panel_instance; //lgfx::Panel_ILI9163 _panel_instance; lgfx::Panel_ILI9341 _panel_instance; //lgfx::Panel_ILI9342 _panel_instance; //lgfx::Panel_ILI9481 _panel_instance; //lgfx::Panel_ILI9486 _panel_instance; //lgfx::Panel_ILI9488 _panel_instance; //lgfx::Panel_IT8951 _panel_instance; //lgfx::Panel_SH110x _panel_instance; // SH1106, SH1107 //lgfx::Panel_SSD1306 _panel_instance; //lgfx::Panel_SSD1327 _panel_instance; //lgfx::Panel_SSD1331 _panel_instance; //lgfx::Panel_SSD1351 _panel_instance; // SSD1351, SSD1357 //lgfx::Panel_SSD1963 _panel_instance; //lgfx::Panel_ST7735 _panel_instance; //lgfx::Panel_ST7735S _panel_instance; //lgfx::Panel_ST7789 _panel_instance; //lgfx::Panel_ST7796 _panel_instance; // SPIバスのインスタンスを用意します。 lgfx::Bus_SPI _bus_instance; // SPIバスのインスタンス // パネルを接続するバスの種類にあったインスタンスを用意します。 // lgfx::Bus_SPI _bus_instance; // SPIバスのインスタンス public: // コンストラクタを作成し、ここで各種設定を行います。 // クラス名を変更した場合はコンストラクタも同じ名前を指定してください。 LGFX(void) { { // バス制御の設定を行います。 auto cfg = _bus_instance.config(); // バス設定用の構造体を取得します。 cfg.spi_mode = 0; // SPI通信モードを設定 (0 ~ 3) cfg.freq_write = 40000000; // 送信時のSPIクロック cfg.freq_read = 16000000; // 受信時のSPIクロック cfg.pin_dc = 9; // SPIのD/Cピン番号を設定 (-1 = disable) cfg.spi_port = 4; // Arduino拡張ボードの場合は 4 _bus_instance.config(cfg); // 設定値をバスに反映します。 _panel_instance.setBus(&_bus_instance); // バスをパネルにセットします。 } { // 表示パネル制御の設定を行います。 auto cfg = _panel_instance.config(); // 表示パネル設定用の構造体を取得します。 // cfg.pin_cs = -1; // CSが接続されているピン番号 (-1 = disable) HW CSピンの場合は-1を指定 cfg.pin_rst = 8; // RSTが接続されているピン番号 (-1 = disable) cfg.pin_busy = -1; // BUSYが接続されているピン番号 (-1 = disable) // ※ 以下の設定値はパネル毎に一般的な初期値が設定されていますので、不明な項目はコメントアウトして試してみてください。 cfg.panel_width = 240; // 実際に表示可能な幅 cfg.panel_height = 320; // 実際に表示可能な高さ cfg.offset_x = 0; // パネルのX方向オフセット量 cfg.offset_y = 0; // パネルのY方向オフセット量 cfg.offset_rotation = 0; // 回転方向の値のオフセット 0~7 (4~7は上下反転) cfg.dummy_read_pixel = 8; // ピクセル読出し前のダミーリードのビット数 cfg.dummy_read_bits = 1; // ピクセル以外のデータ読出し前のダミーリードのビット数 cfg.readable = true; // データ読出しが可能な場合 trueに設定 cfg.invert = false; // パネルの明暗が反転してしまう場合 trueに設定 cfg.rgb_order = false; // パネルの赤と青が入れ替わってしまう場合 trueに設定 cfg.dlen_16bit = false; // データ長を16bit単位で送信するパネルの場合 trueに設定 cfg.bus_shared = true; // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います) // 以下はST7735やILI9163のようにピクセル数が変更できるドライバでのみ設定してください。 // cfg.memory_width = 240; // ドライバICがサポートしている最大の幅 // cfg.memory_height = 320; // ドライバICがサポートしている最大の高さ _panel_instance.config(cfg); } setPanel(&_panel_instance); // 使用するパネルをセットします。 } };

工夫

GPSと液晶

GPSを補足する前に液晶を使うとなかなか補足できませんでした。
GPSを補足して位置情報を取得してから液晶を表示するように工夫してみました。
起動(液晶表示)まで30秒〜2分ほどかかります。(電波を取得しやすい場所だと早い)

液晶表示

液晶表示ライブラリにlovyan03さんのlovyanGFXを使わせていただきました。
Adafruitのライブラリよりもかなり高速に表示できます。

lovyanGFXを使うことでカメラで撮影した画像に文字を透過で重ね合わせたり、チラつきなく表示できました。
(透過させると文字が読みにくかったので透過させずに表示しました。)
M5Stackで実験したときはメモリが足りず、色深度16ビットのスプライトを用意できませんでした。

もう少しやりたかったこと

  • 外部GPSアンテナ
    起動時と移動中のGPS受信が安定することが期待できる。
    外部GNSSアンテナを使用:Spresenseハードウェアドキュメント
  • ケース作成
    今回は時間がなく、ダンボールとなってしまったけど3Dプリンターで作成できればもっと格好良いモノに出来上がったはず。
  • 加速度計表示
    加速、減速、カーブでの加速度を表現したかった
  • 撮影
    液晶に表示されている画像の撮影。
    カメラの映像にGPS情報と速度、日時を書き込んだ映像を静止画としてmicroSDに保存してみたかった。
  • 液晶の明るさをコントロール(Nch MOSFETの追加とコントロール信号を接続)
fumiのアイコン画像
電子工作が好きなのではなく、電子工作を夢想するのが好きなのだと最近気づきました。
ログインしてコメントを投稿する