編集履歴一覧に戻る
higedarumaのアイコン画像

higedaruma が 2024年01月31日10時28分13秒 に編集

初版

タイトルの変更

+

SoundGun ~ 音を撃つ銃 ~ 🍊

タグの変更

+

マイク

+

カメラ

+

木工

メイン画像の変更

メイン画像が設定されました

記事種類の変更

+

製作品

ライセンスの変更

+

(MIT) The MIT License

本文の変更

+

# はじめに ダイの大冒険に出てくる 魔弾銃のように[音の弾を撃つ銃](https://protopedia.net/prototype/169)を過去に作りました。 しかし、音を撃つと言っても、スマホから音を再生するというものでした。 それから7年の時が過ぎ、3Dプリンタなどを手に入れ以前よりもものづくりがしやすい環境になり、 もう一度チャレンジしてみたいと思い、今回作ってみました。 # 遊び方 トリガーを引くと、パラメトリック・スピーカーから音声が再生されます。 弾は最大5発分とし、グリップの底にあるボタンを押すとリロードします。 音声は、自分が吹き込んだ声、または既存の音声の2つをトグルスイッチにより切り替えることが可能です。 自分の音声を吹き込む場合は、設定用のトグルスイッチをONにし、トリガーを引くことで最大5秒間録音します。 リロードボタンを押した状態で、トリガーを5秒間引くと、録音情報をすべて削除します。 銃のスコープにはカメラつけており、銃後ろにあるモニターで状況を見ることができます。 また、モニター下にあるダイヤルを回すことで擬似的に映像をズームします。 銃口には、ToFの距離センサーもつけており、対象物までの距離などもモニターに表示しています。 # 部品リスト | 部品 | 個数 | |---|:---:| | Spresense Main Board | 1 | | Spresense 拡張ボー ド| 1| | Spresense HDRカメラボード | 1 | |B-stem 4CM01(マイク基板)|1| | 2.8インチTFT液晶(GMT020-02)| 1 | |ボタン付きロータリーエンコーダー| 1| | トグルスイッチ | 2| | タクトスイッチ|2| |パラメトリック・スピーカー実験キット|1| |モノラルマイク|1| |5V-12V昇圧USBケーブル|1| |ToF式距離センサーL1タイプ(VL53L1X)|1| | 木材(丸棒、板2枚)| 3| |黒ペンキ|1| # 配線図 ![配線図](https://camo.elchika.com/25ac352b2a3f5076ddfebb55842fae7969914384/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f38396565656263332d636334632d343638332d616661372d306164333230333932386635/) # 開発過程 | | | |:---:|:---| | ![木材](https://camo.elchika.com/2a5e4115e4559934443dc1592741a97d604aadac/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f65616230363134352d663339372d346133642d386261312d393632346436393338383361/) | ![黒に](https://camo.elchika.com/513b316f0019b290330e7b7d0fc24a6328eb1ae2/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f33373363396432382d613464382d343231332d383634652d333734313761643834623237/) | | ![タクトスイッチを埋め込む](https://camo.elchika.com/c94db458a9b14aba5d980e6dcf28bf173d2ceb7e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f66386234316465382d303236362d343733662d383764662d303437656338643134323634/) | ![カバーを被せる](https://camo.elchika.com/fa725a1478b8def6d93ff4f2046f82f425a49d15/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f36353865636333392d346438392d343339662d613434612d373563663162323732363764/) | | ![基板関係は木にダイレクトでつける](https://camo.elchika.com/83d5591d8105ca757fc327a9ea5832f8a0d05a93/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f66313639386431662d363338302d343666342d386636362d336436356361316361383031/) | ![トリガー部分は木なので触りごごちよし](https://camo.elchika.com/7242bab5acd35c8d37c3772ea2320703e105be85/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f32326137663933662d633834312d343266392d623461612d366339643338346664393531/) | |![組み上げた状態](https://camo.elchika.com/f76826c9f363bfb2a3e7e19b5a0fb242a81f0be1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f33376633373939312d633134362d346337302d623535652d363464356562393336336135/)|![組み上げた状態](https://camo.elchika.com/8706eddc8e5f008a9dc000b6eed67f307a65dc62/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f33356538306437652d613631332d343165642d613638392d386564353932343831613635/)| |![中の配線](https://camo.elchika.com/2c6e0bdcb7ded4b26612ca22e8dbb9a562a68860/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f36336461616662372d356230652d343434392d396564302d346430303730383639633766/) | ![自作ケーブルの品質がイマイチでした・・](https://camo.elchika.com/d1d7b79cd221811eb492ba68ab7ca73116c4f9fa/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f34666565643562392d373939322d343265632d383631642d303638323864373933323737/) | | ![スコープにカメラを入れてます。](https://camo.elchika.com/325e746ede27dbcf9bbb4e36238ff0af6422a63e/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f37326163653931612d303139352d346463372d613565332d363233396237366432353932/) | ![背面モニターに表示](https://camo.elchika.com/b6abe43b4a4c0bd35e803c2551645c0d48d2d58a/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f38356636623665352d663663322d343532632d626438632d333764393838666137356261/) | ![スコープの映像](https://camo.elchika.com/3e5d63d865aadd1ab5a540d5a36acdcbf92a3a51/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f65623732353863622d613363302d343465612d623133332d393238653665343864663037/) | ![画面下のダイヤルを回すと擬似的に拡大します。](https://camo.elchika.com/9fb2db9a05fc026eb059e3be3a4bc60c7f3a3646/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f34653136343632612d343564372d346630622d396133312d3034613837646534633632342f64376236613430342d633035362d346530632d613833372d643361623764386135363135/) # プログラム ## メイン ```cpp:メイン #ifdef SUBCORE #error "Core selection is wrong!!" #endif #include "Common.h" #include "GunInfo.h" #include "ShareMemory.h" #include "GunView.h" #include "GunSound.h" GunInfo info; GunView gunView; GunSound gunSound; ShareMemory shareMemory; PushButton encoderSW(SW_PIN); PushButton modeSW(MODE_PIN); PushButton typeSW(TYPE_PIN); PushButton shootButton(SHOOT_PIN); PushButton reloadButton(RELOAD_PIN); void initButton(){ encoderSW.begin(); modeSW.begin(); typeSW.begin(); shootButton.begin(); reloadButton.begin(); } void setup() { Serial.begin(115200); initButton(); getState(); //shareMemory.init(); gunView.init(); gunSound.init(); } void getState(){ // //モードを取得 // info.setMode(modeSW.read() ? Gun : Setting); // //音声種類を取得 // info.setSoundType(typeSW.read() ? SoundGun : SoundOriginal); // // 音声チェック // gunSound.setSound(info.getSoundType()); // info.setBullet(gunSound.getSoundNum()); } void loop() { // Serial.print("m:"); // Serial.print(modeSW.read()); // Serial.print(" t:"); // Serial.print(typeSW.read()); // Serial.print(" s:"); // Serial.print(shootButton.read()); // Serial.print(" r:"); // Serial.print(reloadButton.read()); // Serial.print(" z:"); // Serial.print(info.getZoom()); // Serial.print(" d:"); // Serial.print(info.getDistance()); // Serial.println(); //shareMemory.tick(info); switch(info.getMode()){ case Gun: gun(); break; case Setting: setting(); break; } gunView.draw(info); } void gun(){ //銃撃 if(shootButton.pressed()){ if(info.getBullet() > 0){ //音声再生 gunSound.play(info.getBullet()-1); //弾数の削減 info.setBullet(info.getBullet()-1); } } // リロード if(reloadButton.pressed()){ info.setBullet(gunSound.getSoundNum()); } //音声の切り替え if(typeSW.has_changed()){ gunSound.stop(); info.setSoundType(typeSW.read() ? SoundGun : SoundOriginal); gunSound.setSound(info.getSoundType()); } //モードの切り替え if(modeSW.released()){ Serial.println("mode -> setting"); gunSound.stop(); //設定時はオリジナルに変更 info.setMode(Setting); gunSound.setSound(SoundOriginal); } } void setting(){ // リロードが押されている場合は、削除モードに if(reloadButton.read()){ //モードが切り替わった場合 if(info.getSettingMode() == Record){ //録音中は停止する if(gunSound.isRecording() && info.isRecording()){ info.setRecording(false); gunSound.stop(); gunSound.setSound(SoundOriginal); } //モードを削除に info.setSettingMode(Clear); } //トリガーを押されたらタイマーを開始する if(shootButton.pressed()){ info.setRecordTime(millis()); info.setRecording(true); } // 時間を超えた場合は、録音データを削除 if(info.isRecording()){ if(info.getRecordTime() >= RECORD_TIME){ gunSound.deleteSound(); gunSound.setSound(SoundOriginal); info.setRecording(false); } } // トリガーが話されたら削除キャンセル if(shootButton.released()){ if(info.isRecording()){ info.setRecording(false); } } } else{ //モードが切り替わった場合は状態を初期化する if(info.getSettingMode() == Clear){ info.setRecording(false); info.setSettingMode(Record); } //トリガーを押されたら録音を開始する if(shootButton.pressed()){ //弾の余裕がある場合のに実施 if(gunSound.getSoundNum() < BULLET_MAX){ //録音開始時間 gunSound.record(); info.setRecordTime(millis()); info.setRecording(true); } } // タイムアウトの場合は録音終了 if(gunSound.isRecording() && info.isRecording()){ if(info.getRecordTime() >= RECORD_TIME){ info.setRecording(false); gunSound.stop(); gunSound.setSound(SoundOriginal); } } // トリガーが話されたら録音終了 if(shootButton.released()){ if(gunSound.isRecording() && info.isRecording()){ info.setRecording(false); gunSound.stop(); gunSound.setSound(SoundOriginal); } } } //モードの切り替え if(modeSW.pressed()){ if(gunSound.isRecording() && info.isRecording()){ info.setRecording(false); gunSound.stop(); } Serial.println("mode -> gun"); getState(); } } ``` ```h:GunSound.h #ifndef GUNSOUND_H #define GUNSOUND_H #include <SDHCI.h> #include <Audio.h> #include <SP_AudioPlayer.h> #include <SP_AudioRecorder.h> #include "GunInfo.h" #include <vector> //https://github.com/baggio63446333/SP_Audio #define GUNSOUND_PATH "/normal/gun" #define GUNORIGINAL_PATH "/original" using namespace std; class GunSound{ public: void init(); void record(); void play(int num); bool isPlaying(); bool isRecording(); void setSound(SoundType soundType); int getSoundNum(); void deleteSound(); void setVolume(int _volume); void getVolume(); void setRecordVolume(int _volume); void getRecordVolume(); void stop(); private: SDClass SD; vector<String> files; int volume = 10; int recordVolume = 0; SP_AudioPlayer player; SP_AudioRecorder recorder; String getSound(int index); }; #endif ``` ```cpp:GunSound.cpp #include "GunSound.h" void GunSound::init(){ while(!SD.begin()) { Serial.println("Insert SD card."); } player.begin(); player.volume(volume); player.setFs(44100); player.setBitlen(16); player.setChannel(1); recorder.begin(); recorder.volume(recordVolume); recorder.setFs(48000); recorder.setBitlen(16); recorder.setChannel(1); } void GunSound::record(){ const char fname[32]; sprintf("/mnt/sd0/original/%d.wav", getSoundNum()+1); recorder.wavrecorder(fname); } void GunSound::play(int index){ String path = getSound(index); if(path.equals("")){ return; } //再生中の場合は止める if(isPlaying()){ stop(); } player.wavplay(path.c_str(), false); } bool GunSound::isPlaying(){ return player.isPlaying(); } bool GunSound::isRecording(){ return recorder.isRecording(); } void GunSound::stop(){ if(player.isPlaying()){ player.stop(); } if(recorder.isRecording()){ recorder.stop(); } } String GunSound::getSound(int index){ if(index < 0 || index > getSoundNum()){ return ""; } return files.at(index); } int GunSound::getSoundNum(){ return files.size(); } void GunSound::setSound(SoundType soundType){ files.clear(); File root = SD.open(soundType == SoundGun ? GUNSOUND_PATH : GUNORIGINAL_PATH); //音声データを検索する while (true) { File entry = root.openNextFile(); if (!entry) { break; } if(entry.isDirectory()){ continue; } String file(entry.name()); //macの場合ゴミファイルが入るので・・ if(file.indexOf("._") == -1){ Serial.println(entry.name()); files.push_back(file); } entry.close(); } root.close(); //名前順にしておく sort(files.begin(), files.end()); } void GunSound::deleteSound(){ File root = SD.open(GUNORIGINAL_PATH); while (true) { File entry = root.openNextFile(); if (!entry) { break; } if(entry.isDirectory()){ continue; } SD.remove(entry.name()); } } void GunSound::setVolume(int _volume){ volume = _volume; if(volume < -1020){ volume = -1020; } if(volume > 120){ volume = 120; } } void GunSound::getVolume(){ return volume; } void GunSound::setRecordVolume(int _volume){ recordVolume = _volume; if(recordVolume < 0){ recordVolume = 0; } if(recordVolume > 120){ recordVolume = 120; } } void GunSound::getRecordVolume(){ return recordVolume; } ``` ```cpp:LGFX.hpp #ifndef LGFX_H #define LGFX_H #define LGFX_USE_V1 #define LGFX_AUTODETECT #include <LovyanGFX.hpp> /* * GND * VCC * SCL - 13 (SPI4) * SDA - 11 (SPI4) * RST - 9 * DC - 8 * CS - 10 (SPI4) */ class LGFX : public lgfx::LGFX_Device { lgfx::Panel_ST7789 _panel_instance; lgfx::Bus_SPI _bus_instance; // SPIバスのインスタンス //lgfx::Light_PWM _light_instance; public: LGFX(void) { { // バス制御の設定を行います。 auto cfg = _bus_instance.config(); // バス設定用の構造体を取得します。 cfg.spi_mode = 3; cfg.freq_write = 30000000; cfg.freq_read = 16000000; cfg.pin_dc = 8; cfg.spi_port = 4; _bus_instance.config(cfg); _panel_instance.setBus(&_bus_instance); } { // 表示パネル制御の設定を行います。 auto cfg = _panel_instance.config(); // 表示パネル設定用の構造体を取得します。 cfg.pin_cs = 10; // CSが接続されているピン番号 (-1 = disable) cfg.pin_rst = 9; // RSTが接続されているピン番号 (-1 = disable) cfg.pin_busy = - 1; 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 = true; // パネルの明暗が反転してしまう場合 trueに設定 cfg.rgb_order = false; // パネルの赤と青が入れ替わってしまう場合 trueに設定 cfg.dlen_16bit = false; // データ長を16bit単位で送信するパネルの場合 trueに設定 cfg.bus_shared = true; // SDカードとバスを共有している場合 trueに設定(drawJpgFile等でバス制御を行います) _panel_instance.config(cfg); } setPanel(&_panel_instance); // 使用するパネルをセットします。 } }; #endif ``` ```h:GunView.h #ifndef VIEW_H #define VIEW_H // カメラ #include <Camera.h> #include "Common.h" #include "LGFX.hpp" #include "GunInfo.h" #define DISPLAT_WIDTH 320 #define DISPLAT_HEIGHT 240 #define CENTER_X DISPLAT_WIDTH/2 #define CENTER_Y DISPLAT_HEIGHT/2 //zoom bar #define ZOOM_BAR_TOP 40 #define ZOOM_BAR_BOTTOM 190 class GunView{ public: void init(); void drawSettingChargeView(GunInfo &info); void draw(GunInfo &info); void drawCameraView(GunInfo &info); private: float sideBar[3] = {0, 0, 0}; float sideBarPos[3] = {0, 0, 0}; uint8_t distancePos = 0; uint8_t zoomPos = 0; void printCameraError(enum CamErr err); //void initCamera(void (*camCB)(CamImage)){ void initCamera(); static void cameraCallback(CamImage img); }; #endif ``` ```cpp:GunView.cpp #include "GunView.h" static LGFX lcd; static LGFX_Sprite sprite(&lcd); static LGFX_Sprite preview(&sprite); void GunView::init(){ lcd.init(); lcd.setSwapBytes(true); // バイト順変換を有効にする。 lcd.setRotation(1); // スプライト用 sprite.createSprite(DISPLAT_WIDTH, DISPLAT_HEIGHT); // カメラプレビュー用 preview.setSwapBytes(true); preview.createSprite(DISPLAT_WIDTH, DISPLAT_HEIGHT); // カメラの初期化 initCamera(); } void GunView::draw(GunInfo &info){ switch(info.getMode()){ case Gun: //Serial.println("Gun"); drawCameraView(info); break; case Setting: //Serial.println("Setting"); drawSettingChargeView(info); break; } lcd.display(); } void GunView::drawSettingChargeView(GunInfo &info){ //画面初期化 sprite.fillScreen(TFT_BLACK); sprite.setFont(&fonts::Font2); sprite.setTextSize(1); sprite.setTextDatum(12); sprite.setTextColor(sprite.color565( 255, 255, 255)); sprite.drawString("SETTING", 30, 25); sprite.fillRect(20, 20, 5, 15, TFT_WHITE); sprite.drawLine(20, 35, 100, 35, TFT_WHITE); sprite.setFont(&fonts::Font0); sprite.setTextColor(TFT_WHITE); sprite.setTextDatum(12); sprite.fillRect(190, 95, 100, 10, TFT_BLACK); sprite.drawString("PULL TRIGGER", 240 + (40 - sprite.textWidth("POSSIBLE CHARGE"))/2, 100); sprite.setFont(&fonts::Font4); char *title = info.getSettingMode() == Record ? "CHAGE THE SOUND" : "CHAGE THE SOUND"; sprite.drawString(title, 30 + (260 - sprite.textWidth(title))/2, 75); //バー float bar_pos = 0.0f; if(info.isRecording()){ bar_pos = 1- info.getRecordTime()/RECORD_TIME; if(bar_pos > 1){ bar_pos = 1.0f; } } sprite.drawRect(30, 110, 260, 40, TFT_WHITE); sprite.fillRect(33, 113, 254 * bar_pos, 34, TFT_WHITE); sprite.fillArc(30, 150, 0, 5, 0, 271, TFT_WHITE); sprite.drawArc(30, 150, 0, 8, 0, 271, TFT_WHITE); // sprite.fillArc(290, 140, 0, 5, 269, 180, TFT_WHITE); // sprite.drawArc(290, 140, 0, 8, 269, 180, TFT_WHITE); sprite.drawLine(30, 150, 60, 180, TFT_WHITE); sprite.fillRect(60, 170, 5, 40, TFT_WHITE); // 弾丸表示 for(int i=0; i < BULLET_MAX; i++){ sprite.fillRect(75 + i * 40 , 170, 30, 40, i < info.getBullet() ? TFT_WHITE : sprite.color565( 128, 128, 128)); } sprite.pushSprite(0, 0); } /** * カメラ表示用 */ void GunView::drawCameraView(GunInfo &info){ //------------ // カメラ画像 //------------- float zoom = (info.getZoom()+10)/10.0f; preview.setPivot(preview.width()/2.0, preview.height()/2.0); preview.pushRotateZoom(DISPLAT_WIDTH/2, DISPLAT_HEIGHT/2, 180, zoom, zoom, 0); //-------------- // スコープの描画 //-------------- distancePos += info.getDistance() > distancePos ? -2 : 2; if(distancePos < 0){ distancePos = 0; } if(distancePos > info.getDistance()){ distancePos = info.getDistance(); } sprite.fillArc(CENTER_X, CENTER_Y, 78, 80, 0, 360, TFT_WHITE); int deg = 180 * ((float)distancePos/ DISTANCE_MAX); sprite.fillArc(CENTER_X, CENTER_Y, 70, 80, deg, deg + 35, TFT_WHITE); sprite.fillArc(CENTER_X, CENTER_Y, 70, 80, deg + 180, deg + 215, TFT_WHITE); sprite.drawCircle(CENTER_X, CENTER_Y, 75, TFT_WHITE); //-------------- // メニューっぽい模様 //-------------- sprite.drawCircle(137, 220, 8, TFT_WHITE); sprite.fillCircle(137, 220, 4, TFT_WHITE); sprite.fillCircle(158, 220, 8, TFT_WHITE); sprite.fillCircle(179, 220, 8, TFT_WHITE); sprite.drawCircle(200, 220, 8, TFT_WHITE); sprite.fillCircle(200, 220, 4, TFT_WHITE); sprite.fillRect(201, 219, 30, 2, TFT_WHITE); //-------------- // 弾数の表示 //-------------- // 弾の表示の模様 sprite.drawLine(226, 219, 250, 200, TFT_WHITE); sprite.drawLine(231, 219, 255, 200, TFT_WHITE); sprite.fillRect(250, 200, 70, 2, TFT_WHITE); // 弾丸表示 for(int i=0; i < BULLET_MAX; i++){ sprite.fillRect(250+i*15 , 175, 10, 20, i < info.getBullet() ? TFT_WHITE : sprite.color565( 128, 128, 128)); } // リロード時 if(info.getBullet() < 1){ sprite.setFont(&fonts::Font0); sprite.setTextDatum(12); sprite.fillRect(250, 161, 70, 10, TFT_RED); sprite.setTextColor(TFT_WHITE); sprite.drawString("RELOAD", 250 + (70 - sprite.textWidth("RELOAD"))/2, 166); } // zoom bar sprite.fillTriangle(35, 66.5, 45, 66.5, 40, 75, TFT_WHITE); sprite.fillRect(ZOOM_BAR_TOP, 80, 2, 110, TFT_WHITE); sprite.fillTriangle(35, 203.5, 45, 203.5, 40, 195, TFT_WHITE); // barの位置 float bar_pos = ZOOM_BAR_TOP + (ZOOM_BAR_BOTTOM - ZOOM_BAR_TOP) * (1 - (float)(info.getZoom()) /ZOOM_MAX); sprite.fillTriangle(47, bar_pos , 57.5, bar_pos - 5 , 57.5, bar_pos + 5, TFT_WHITE); //-------------- // 距離の表示 //-------------- sprite.fillCircle(160, 120, 3, TFT_WHITE); sprite.drawCircle(160, 120, 10, TFT_WHITE); sprite.drawLine(160, 120, 250, 60, TFT_WHITE); sprite.fillRect(250, 60, 70, 2, TFT_WHITE); // 距離(数値) sprite.setTextColor(TFT_WHITE); sprite.setFont(&fonts::Font4); sprite.setTextDatum(12); String distanceStr(info.getDistance()); sprite.drawString(distanceStr.c_str(), 250 + (70 - sprite.textWidth(distanceStr.c_str()))/2, 45); sprite.setFont(&fonts::Font0); sprite.fillRect(250, 64, 70, 10, TFT_WHITE); sprite.setTextColor(TFT_BLACK); sprite.drawString("DISTANCE", 250 + (70 - sprite.textWidth("DISTANCE"))/2, 69); // 距離(バー) sprite.drawLine(260, 74, 260, 87, TFT_WHITE); sprite.fillRect(260, 78, (60 * (float)DISTANCE_MAX / distancePos), 7, TFT_WHITE); //------------ // mode //------------ sprite.setFont(&fonts::Font2); sprite.setTextSize(1); sprite.setTextDatum(12); sprite.setTextColor(sprite.color565( 255, 255, 255)); sprite.drawString(info.getMode() == Gun ? "NORMAL" : "ORIGIN", 30, 25); sprite.fillRect(20, 20, 5, 15, TFT_WHITE); sprite.drawLine(20, 35, 100, 35, TFT_WHITE); //----------- // 謎バー //----------- for(int i=0; i<3; i++){ sideBarPos[i] += sideBar[i] > sideBarPos[i] ? -0.1f : 0.1f; if(sideBarPos[i] <= sideBar[i]){ sideBarPos[i] = sideBar[i]; sideBar[i] = random(37); } else if(sideBarPos[i] >= sideBar[i]){ sideBarPos[i] = sideBar[i]; sideBar[i] = random(37); } } sprite.fillRect(75 + (36 - sideBarPos[0]), 200, sideBarPos[0], 7, TFT_WHITE); sprite.fillRect(75 + (36 - sideBarPos[1]), 210, sideBarPos[1], 7, TFT_WHITE); sprite.fillRect(75 + (36 - sideBarPos[2]), 220, sideBarPos[2], 7, TFT_WHITE); sprite.fillRect(112, 200, 2, 27, TFT_WHITE); sprite.drawLine(112, 200, 137, 220, TFT_WHITE); sprite.pushSprite(0, 0); } /** * カメラエラー時の動作 */ void GunView::printCameraError(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; } } /** * カメラの初期化 */ //void initCamera(void (*camCB)(CamImage)){ void GunView::initCamera(){ CamErr err; //カメラライブラリの初期化 Serial.println("Prepare camera"); err = theCamera.begin(1, CAM_VIDEO_FPS_30, CAM_IMGSIZE_QVGA_H, CAM_IMGSIZE_QVGA_V, CAM_IMAGE_PIX_FMT_RGB565, 7); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } //カメラプレビューの開始 Serial.println("Start streaming"); err = theCamera.startStreaming(true, cameraCallback); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } // 静止画フォーマットの設定 Serial.println("Set still picture format"); err = theCamera.setStillPictureImageFormat( CAM_IMGSIZE_QVGA_H, CAM_IMGSIZE_QVGA_V, CAM_IMAGE_PIX_FMT_JPG); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } // ホワイトバランスの調整 Serial.println("Set Auto white balance parameter"); err = theCamera.setAutoWhiteBalanceMode(CAM_WHITE_BALANCE_DAYLIGHT); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } // ISOの設定 Serial.println("Set ISO Sensitivity"); err = theCamera.setAutoISOSensitivity(true); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } // 露出の設定 Serial.println("Set Auto exposure"); err = theCamera.setAutoExposure(true); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } // HDRの設定 Serial.println("Set HDR"); err = theCamera.setHDR(CAM_HDR_MODE_ON); if (err != CAM_ERR_SUCCESS) { printCameraError(err); } } /** * カメラプレビュー用Callback */ void GunView::cameraCallback(CamImage img) { //画像が有効かどうか if (!img.isAvailable()){ Serial.println("Failed to get video stream image"); } CamErr err; preview.pushImage(0, 0, DISPLAT_WIDTH, DISPLAT_HEIGHT, (uint16_t *)img.getImgBuff()); } ``` ```h:ShareMemory.h #ifndef SHARE_MEMORY_H #define SHARE_MEMORY_H #include "GunInfo.h" // 共有メモリ #include <MP.h> #define CORE_ENCODER 1 // 各種外部機器処理CORE #define CORE_TOF 2 // 各種外部機器処理CORE #define MSG_SET_SHARE_ADDR 1 // 共有メモリの設定命令 #define RES_SETED_SHARE_ADDR 2 // 共有メモリの設定済応答 #define VAL_ENCODER 0 // エンコーダー値 #define VAL_TOF 1 // ToFセンサ値 #define SHARE_MEMORY_SIZE 90 // 共有メモリサイズ class ShareMemory{ public: void init(); // 共有メモリの初期化 void tick(GunInfo &info); // private: int16_t *share_memory_addr; // 共有メモリアドレス }; #endif ``` ```cpp:ShareMemory.cpp #include "ShareMemory.h" /** * 共有メモリの初期化 */ void ShareMemory::init(){ MP.begin(CORE_ENCODER); // 各センサー処理用 MP.begin(CORE_TOF); // 各センサー処理用 // 共有メモリを確保 uint16_t *addr = (uint16_t *)MP.AllocSharedMemory(SHARE_MEMORY_SIZE); if (!addr) { printf("Error: out of memory\n"); return; } // 先頭アドレスの表示 printf("SharedMemory Address=@%08lx\n", (uint32_t)addr); // 共有メモリの先頭アドレスをグローバルで保持 share_memory_addr = &addr[0]; // 各種COREに共有メモリのアドレスを通知 MP.Send(MSG_SET_SHARE_ADDR, share_memory_addr, CORE_ENCODER); MP.Send(MSG_SET_SHARE_ADDR, share_memory_addr, CORE_TOF); // 現在は設定済み応答を確認しない } void ShareMemory::tick(GunInfo &info){ info.setZoom(share_memory_addr[ VAL_ENCODER ]); info.setDistance(share_memory_addr[ VAL_TOF ]); }``` ## サブコア 各種センサーのセンシングを実施しています。 ```cpp:ToFセンサ #ifndef GUNIO_H #define GUNIO_H #include <Wire.h> #include <VL53L1X.h> #include <MP.h> class GunIO{ public: GunInfo(){ } void init(){ MPLog("start subcore tof => init\n"); Wire.begin(); Wire.setClock(400000); // use 400 kHz I2C sensor.setTimeout(500); if (!sensor.init()){ MPLog("start subcore tof => init error\n"); while (1); } sensor.setDistanceMode(VL53L1X::Long); sensor.setMeasurementTimingBudget(50000); sensor.startContinuous(50); } void tick(){ } int getDistance() { return sensor.read(); } private: VL53L1X sensor; // ToFセンサー }; #endif ``` ```cpp:GunIO.hpp #ifndef GUNIO_H #define GUNIO_H #include <Wire.h> #include <VL53L1X.h> #include <MP.h> class GunIO{ public: GunInfo(){ } void init(){ MPLog("start subcore tof => init\n"); Wire.begin(); Wire.setClock(400000); // use 400 kHz I2C sensor.setTimeout(500); if (!sensor.init()){ MPLog("start subcore tof => init error\n"); while (1); } sensor.setDistanceMode(VL53L1X::Long); sensor.setMeasurementTimingBudget(50000); sensor.startContinuous(50); } void tick(){ } int getDistance() { return sensor.read(); } private: VL53L1X sensor; // ToFセンサー }; #endif ``` ```cpp:ロータリーエンコーダー #if (SUBCORE != 1) #error "Core selection is wrong!!" #endif #include "GunIO.hpp" #include <MP.h> // 共有メモリ #define CORE_ENCODER 1 // エンコーダー処理CORE #define MSG_SET_SHARE_ADDR 1 // 共有メモリの設定命令 #define RES_SETED_SHARE_ADDR 2 // 共有メモリの設定済応答 #define VAL_ENCODER 0 // エンコーダー値 #define VAL_TOF 1 // 距離 #define SHARE_MEMORY_SIZE 90 // 共有メモリサイズ int16_t *share_memory_addr; // 共有メモリアドレス GunIO gunIO; void setup() { MPLog("start subcore encoder\n"); int8_t msgid; uint16_t *addr; MP.begin(); // メインコアから共有メモリの先頭アドレスの受け取り MP.Recv(&msgid, &addr); if(msgid == MSG_SET_SHARE_ADDR){ // グローバルへ先頭アドレスを保持 share_memory_addr = &addr[0]; share_memory_addr[VAL_ENCODER] = 0; } MPLog("start subcore tof => init sharememory\n"); gunIO.init(); } void loop() { gunIO.tick(); share_memory_addr[VAL_ENCODER] = gunIO.getEncoder(); // Serial.print("e:"); // Serial.println(gunIO.getEncoder()); } ``` ```cpp:GunIO.hpp #ifndef GUNIO_H #define GUNIO_H #include <RotaryEncoder.h> #include <Wire.h> #include <VL53L1X.h> #include <MP.h> // ロータリーエンコーダー #define PINA 12 // ロータリーエンコーダーA入力(CLK) #define PINB 6 // ロータリーエンコーダーB入力(SW) //#define SW_PIN 12 // スイッチ #define ROTARYMIN 0 // ロータリーエンコーダー最小値 #define ROTARYMAX 40 // ロータリーエンコーダー最大値 class GunIO{ public: GunInfo(){ // encoder = new RotaryEncoder(PINA, PINB, RotaryEncoder::LatchMode::TWO03); } void init(){ MPLog("start subcore encoder => init\n"); encoder = new RotaryEncoder(PINA, PINB, RotaryEncoder::LatchMode::TWO03); } void tick(){ if(encoder != NULL){ encoder->tick(); int newPos = encoder->getPosition(); if (newPos < ROTARYMIN) { encoder->setPosition(ROTARYMIN); newPos = ROTARYMIN; } else if (newPos > ROTARYMAX) { encoder->setPosition(ROTARYMAX); newPos = ROTARYMAX; } if (lastPos != newPos) { lastPos = newPos; } } } int getEncoder() { return lastPos; } private: int lastPos = -1; // ロータリーエンコーダー前回値 int16_t encoderCount = 0; // ロータリーエンコーダー現在値 RotaryEncoder *encoder = NULL; // ロータリーエンコーダー }; #endif ``` # 遊べなかった 本当に申し訳訳ないです。 カメラケーブルをテスト中に破損させてしまい、、カメラが利用できなくなりました。 結果として動画を撮るところまでできませんでした・・。 部材届いたら完成させます。。 現状動作には影響なかったですが、メモリを確保するためサブコア利用をやめ、``pThead``に移行したいです。