キット化する上で、必要となるのが、組立マニュアルです。このマニュアルは、設計で使用したFusion360を使い、画面キャプチャをして制作します。
表示色・ネジの追加
組立図の作成
右図のようにすると、「部品をどのようにして組み立てているのか」が図を見てわかるようになります。
キット化する上で、必要となるのが、組立マニュアルです。このマニュアルは、設計で使用したFusion360を使い、画面キャプチャをして制作します。
以前に完成したLEDマトリックス表示装置ですが、使用している中で、WiFiが使用できない場合、日付の設定ができないことがありました。
この場合、何かの方法で日付設定が必要です。設計した基板に汎用ポートを付けていたため、このポートにタクトスイッチを付けて対応します。内部RTCの日付が違い、WiFiがなくても手動で日付の設定ができるように改良を加えました。
これにより、どのような環境でも、日付・時刻を設定できるようになりました。
「キットを組み立てるときに、手軽に、手間が少なくできること」がコンセプトにしています。
3Dプリンタ、レーザ加工機でパーツを製作した後、キット用に使用しているM3のタッピングねじで取り付け始めました。
設計の中で不具合もあり、調整しながら組み上げていきます。
2025年10月18日(土)、19日(日)の2日間、愛知県北名古屋市の西春駅近くの「ヨシヅヤ Yストア 西春店」4F 特設会場にてNT名古屋2025が開催されました。
出品は自作した「ガチャガチャ」です。
子供たちには楽しんで頂けたようです。この展示会は技術系の方が多いので、仕組みなどを解説しながら見て頂きました。
地元の活動で行っているものづくり体験教室での製作キット「ビー玉ガチャキット」も見て頂き、購入もして頂きありがとうございました。
この他、LED作品や電光掲示板なども展示しました。
多くの皆様と交流させていただき大変ためになりました。
uint16_t Tetris_Block[][5] = {
// 回転パターン, , 色
{0x0000, 0x0000, 0x0000, 0x0000, 0}, // dummy
{0x4444, 0x0F00, 0x4444, 0x0F00, 1}, // I
{0x4620, 0x06C0, 0x4620, 0x06C0, 2}, // S
{0x2640, 0x0C60, 0x2640, 0x0C60, 3}, // Z
{0x4E00, 0x4640, 0x0E40, 0x4C40, 4}, // T
{0x88C0, 0x2E00, 0x6220, 0xE800, 5}, // L
{0x44C0, 0xE200, 0xC880, 0x8E00, 6}, // J
{0xCC00, 0xCC00, 0xCC00, 0xCC00, 7} // O
};
void Tetris_setBlock( int blk, int rot) {
uint16_t dat = Tetris_Block[blk][rot % 4];
uint16_t col = Tetris_Block[blk][4];
for (int x = 0; x <= 3; x++) {
for (int y = 0; y <= 3; y++) {
BLOCK[x][y] = 0;
}
}
if ( ( dat & 0x8000 ) != 0 ) BLOCK[0][0] = col;
if ( ( dat & 0x4000 ) != 0 ) BLOCK[1][0] = col;
if ( ( dat & 0x2000 ) != 0 ) BLOCK[2][0] = col;
if ( ( dat & 0x1000 ) != 0 ) BLOCK[3][0] = col;
if ( ( dat & 0x0800 ) != 0 ) BLOCK[0][1] = col;
if ( ( dat & 0x0400 ) != 0 ) BLOCK[1][1] = col;
if ( ( dat & 0x0200 ) != 0 ) BLOCK[2][1] = col;
if ( ( dat & 0x0100 ) != 0 ) BLOCK[3][1] = col;
if ( ( dat & 0x0080 ) != 0 ) BLOCK[0][2] = col;
if ( ( dat & 0x0040 ) != 0 ) BLOCK[1][2] = col;
if ( ( dat & 0x0020 ) != 0 ) BLOCK[2][2] = col;
if ( ( dat & 0x0010 ) != 0 ) BLOCK[3][2] = col;
if ( ( dat & 0x0008 ) != 0 ) BLOCK[0][3] = col;
if ( ( dat & 0x0004 ) != 0 ) BLOCK[1][3] = col;
if ( ( dat & 0x0002 ) != 0 ) BLOCK[2][3] = col;
if ( ( dat & 0x0001 ) != 0 ) BLOCK[3][3] = col;
}
void Tetris_drawBlock( int x, int y, int blk, int rot){
Tetris_setBlock( blk, rot); // BLOCK配列にブロック状態を展開
for(int j=0;j<4; j++){
for(int i=0;i<4; i++){
if( BLOCK[i][j]>0 ){
// ブロックがあるときはブロックを描く
Vout.drawRect( (x+i)*9 , (y+j)*9 , 8, 8, Tetris_Block_Color[BLOCK[i][j]] );
Vout.fillRect( (x+i)*9+2, (y+j)*9+2, 4, 4, Tetris_Block_Color[BLOCK[i][j]] );
}else{
// ブロックが無い時は黒で塗潰す
Vout.fillRect( (x+i)*9 , (y+j)*9 , 8, 8, BLACK );
}
}
}
}
int blockX=5, blockY=8, blockNo=1, blockRot=0;
void loop(void) {
// ブロックを表示 X座標 , Y座標 , ブロックNo, 回転
Tetris_drawBlock( blockX, blockY, blockNo, blockRot);
Vout.display();
blockNo=blockNo+1;
if( blockNo > 7 ) blockNo =1;
delay(300);
}
#include "img_ika1.h"
#include "img_ika2.h"
#include "img_kani1.h"
#include "img_kani2.h"
#include "img_tako1.h"
#include "img_tako2.h"
LGFX gfx;
void setup() {
gfx.init();
}
int x=5, y=10;
void loop() {
gfx.drawPng((std::uint8_t*)img_ika1, sizeof(img_ika1), x+48, y);
gfx.drawPng((std::uint8_t*)img_ika1, sizeof(img_ika1), x+26, y);
gfx.drawPng((std::uint8_t*)img_ika1, sizeof(img_ika1), x+4, y);
gfx.drawPng((std::uint8_t*)img_kani1, sizeof(img_kani1), x+2, y+20);
gfx.drawPng((std::uint8_t*)img_tako1, sizeof(img_tako1), x, y+40);
delay(300);
x++;
gfx.drawPng((std::uint8_t*)img_ika2, sizeof(img_ika2), x+48, y);
gfx.drawPng((std::uint8_t*)img_ika2, sizeof(img_ika2), x+26, y);
gfx.drawPng((std::uint8_t*)img_ika2, sizeof(img_ika2), x+4, y);
gfx.drawPng((std::uint8_t*)img_kani2, sizeof(img_kani2), x+2, y+20);
gfx.drawPng((std::uint8_t*)img_tako2, sizeof(img_tako2), x, y+40);
delay(300);
x++;
if( x> gfx.width() ) x = 5;
}
#include "image.h"
LGFX gfx;
void setup() {
gfx.init();
}
void loop() {
int x = rand() % gfx.width();
int y = rand() % gfx.height();
gfx.drawPng((std::uint8_t*)img, sizeof(img), x, y);
delay(300);
}
#define LGFX_USE_V1 #include <LovyanGFX.hpp>class LGFX : public lgfx::LGFX_Device { public: lgfx::Panel_CVBS _panel_instance; LGFX(void) { { // 表示パネル制御の設定を行います。 auto cfg = _panel_instance.config(); // 表示パネル設定用の構造体を取得します。 // 出力解像度を設定; cfg.memory_width = 240; // 出力解像度 幅 cfg.memory_height = 160; // 出力解像度 高さ // 実際に利用する解像度を設定; cfg.panel_width = 208; // 実際に使用する幅 (memory_width と同値か小さい値を設定する) cfg.panel_height = 128; // 実際に使用する高さ (memory_heightと同値か小さい値を設定する) // 表示位置オフセット量を設定; cfg.offset_x = 16; // 表示位置を右にずらす量 (初期値 0) cfg.offset_y = 16; // 表示位置を下にずらす量 (初期値 0) _panel_instance.config(cfg); // 通常は memory_width と panel_width に同じ値を指定し、 offset_x = 0 で使用します。; // 画面端の表示が画面外に隠れるのを防止したい場合は、 panel_width の値をmemory_widthより小さくし、offset_x で左右の位置調整をします。; // 例えば memory_width より panel_width を 32 小さい値に設定した場合、offset_x に 16 を設定することで左右位置が中央寄せになります。; // 上下方向 (memory_height , panel_height , offset_y ) についても同様に、必要に応じて調整してください。; } { auto cfg = _panel_instance.config_detail(); // 出力信号の種類を設定; // cfg.signal_type = cfg.signal_type_t::NTSC; cfg.signal_type = cfg.signal_type_t::NTSC_J; // cfg.signal_type = cfg.signal_type_t::PAL; // cfg.signal_type = cfg.signal_type_t::PAL_M; // cfg.signal_type = cfg.signal_type_t::PAL_N; // 出力先のGPIO番号を設定; cfg.pin_dac = 26; // DACを使用するため、 25 または 26 のみが選択できます; // PSRAMメモリ割当の設定; cfg.use_psram = 1; // 0=PSRAM不使用 / 1=PSRAMとSRAMを半々使用 / 2=全部PSRAM使用; // 出力信号の振幅の強さを設定; cfg.output_level = 128; // 初期値128 // ※ GPIOに保護抵抗が付いている等の理由で信号が減衰する場合は数値を上げる。; // ※ M5StackCore2 はGPIOに保護抵抗が付いているため 200 を推奨。; // 彩度信号の振幅の強さを設定; cfg.chroma_level = 128; // 初期値128 // 数値を下げると彩度が下がり、0で白黒になります。数値を上げると彩度が上がります。; // バックグラウンドでPSRAMの読出しを行うタスクの優先度を設定; // cfg.task_priority = 25; // バックグラウンドでPSRAMの読出しを行うタスクを実行するCPUを選択 (APP_CPU_NUM or PRO_CPU_NUM); // cfg.task_pinned_core = PRO_CPU_NUM; _panel_instance.config_detail(cfg); } setPanel(&_panel_instance); } }; LGFX gfx; void setup(void) { gfx.init(); for (int x = 0; x < gfx.width(); ++x) { int v = x * 256 / gfx.width(); gfx.fillRect(x, 0 * gfx.height() >> 3, 7, gfx.height() >> 3, gfx.color888(v, v, v)); gfx.fillRect(x, 1 * gfx.height() >> 3, 7, gfx.height() >> 3, gfx.color888(v, 0 ,0)); gfx.fillRect(x, 2 * gfx.height() >> 3, 7, gfx.height() >> 3, gfx.color888(0, v, 0)); gfx.fillRect(x, 3 * gfx.height() >> 3, 7, gfx.height() >> 3, gfx.color888(0, 0, v)); } delay(1000); gfx.drawLine(0,0, gfx.width() - 1, gfx.height() - 1, TFT_GREEN); gfx.drawRect(0,0, gfx.width(), gfx.height(), TFT_RED); gfx.drawRect(1,1, gfx.width() - 2, gfx.height() - 2, TFT_GREEN); gfx.drawRect(2,2, gfx.width() - 4, gfx.height() - 4, TFT_BLUE); gfx.fillRect(gfx.width()/2, (gfx.height() >> 3) * 4, gfx.height() / 3, gfx.height() / 3, TFT_PURPLE); } void loop(void) { gfx.setCursor(20, (gfx.height() >> 3) * 4); gfx.setTextSize(0); gfx.println("hello at " + String(millis())); }
ESP32マイコンのBLEを使い、ゲームコントローラの製作を行いました。
右図は完成品です。PlayStationの操作ボタンをイメージして、設計しています。
単3電池用の電池ケースを内蔵しています。
#define LED_R 23
#define LED_Y 17
#define LED_B 16
#define SW_START 33
#define SW_SELECT 32
#define SW_LU 27 // Left side UP Button
#define SW_LD 13 // Left side DOWN Button
#define SW_LR 12 // Left side RIGHT Button
#define SW_LL 14 // Left side LEFT Button
#define SW_RU 22 // Right side UP Button
#define SW_RD 18 // Right side DOWN Button
#define SW_RR 19 // Right side RIGHT Button
#define SW_RL 21 // Right side LEFT Button
#define DC_VOLT 35
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
String device_name = "ESP32-Controller-Slave";
void setup() {
Serial.begin(115200);
SerialBT.begin(device_name);
Serial.printf("The device with name \"%s\" is started.\nNow you can pair it with Bluetooth!\n", device_name.c_str());
pinMode(LED_R,OUTPUT);
pinMode(LED_Y,OUTPUT);
pinMode(LED_B,OUTPUT);
pinMode(SW_START,INPUT_PULLUP);
pinMode(SW_SELECT,INPUT_PULLUP);
pinMode(SW_LU,INPUT_PULLUP);
pinMode(SW_LD,INPUT_PULLUP);
pinMode(SW_LR,INPUT_PULLUP);
pinMode(SW_LL,INPUT_PULLUP);
pinMode(SW_RU,INPUT_PULLUP);
pinMode(SW_RD,INPUT_PULLUP);
pinMode(SW_RR,INPUT_PULLUP);
pinMode(SW_RL,INPUT_PULLUP);
pinMode(DC_VOLT,INPUT);
}
void loop() {
if (SerialBT.available()) {
Serial.write(SerialBT.read());
}
int sw_lu=digitalRead(SW_LU);
int sw_ld=digitalRead(SW_LD);
int sw_ll=digitalRead(SW_LL);
int sw_lr=digitalRead(SW_LR);
int sw_ru=digitalRead(SW_RU);
int sw_rd=digitalRead(SW_RD);
int sw_rl=digitalRead(SW_RL);
int sw_rr=digitalRead(SW_RR);
int sw_str=digitalRead(SW_START);
int sw_sel=digitalRead(SW_SELECT);
int dcv=analogRead(DC_VOLT);
SerialBT.printf("%1d%1d%1d%1d%1d%1d%1d%1d%1d%1d:%4d\n",
sw_lu, sw_ld, sw_lr, sw_ll, sw_ru, sw_rd, sw_rl, sw_rr, sw_str, sw_sel, dcv);
delay(1000);
}
void loop() {
if (Serial.available()) {
SerialBT.write(Serial.read());
}
if (SerialBT.available()) {
Serial.write(SerialBT.read());
}
delay(20);
}void serialEvent() {
while (Serial.available()) {
// get the new byte:
char inChar = (char)Serial.read();
// add it to the inputString:
inputString += inChar;
// if the incoming character is a newline, set a flag so the main loop can
// do something about it:
if (inChar == '\n') {
stringComplete = true;
}
}
}
boolean stringComplete=false;
String inputString;
void loop() {
serialBTEvent();
if (stringComplete) {
Serial.println(inputString);
inputString = "";
stringComplete = false;
}
delay(20);
}
void serialBTEvent() {
while (SerialBT.available()) {
char inChar = (char)SerialBT.read();
if (inChar == '\n') {
stringComplete = true;
}else{
inputString += inChar;
}
}
}
よくある迷路パズルは片面だけですが、空いている面を活用して、両面の迷路パズルにしました。デザインは「XINXENDO SW」風です。
右図の上が表からみた図、下が裏からみた図です。この図では見ずらいのですが、丸印をつけた部分で、ボールが表と裏を行き来することができます。
この通路を使うと、すべての場所にボールを移動させることができるようにしています。
自宅に3Dプリンタを昨年の12月に購入しました。Bambu社の「A1 mini」です。
いろいろな人に、「最近の3Dプリンタが手軽で高性能になっている」と聞き、購入しました。
それからまだ1年になりませんが、3Dプリンタというと「フィラメントが詰まる」などのトラブルがあり、なかなか成形するのに手間がかかるという印象でしたが、特に問題なく使えていることに満足していました。ただ、2色で造形するときや異なる色のフィラメントにするときには、フィラメントを入れ替える必要があり、手間が必要と感じていました。
「バーサライタ方式の電光掲示板2」では、第1水準漢字まで対応するようにしました。
その後、BDFデータが第2水準漢字まで対応しているため、第2水準漢字まで変換できるようにしました。
今回はドット数が30ドットあるため、24ビットのビットマップフォントを使い、表示させるようにします。
mgo-tec様のサイトで紹介されているライブラリ「ESP32_SPIFFS_ShinonomeFNT」をもとにして変更します。
ここで紹介するプログラムは、ESP32マイコンであれば動作します。
使用するテープLEDは「WS2812」と呼ばれるマイコン内蔵のフルカラーLEDです。
マイコンから1ポートのみで出力できるため、よく見かけるようになりました。このテープLEDは、テープの途中で切断して利用できるため、必要な長さで設計できます。
今回は、60LEDs/mのタイプで、1mに60個LEDが付いているものです。
LEDマトリックスに画像を表示させるには、いろいろな画像形式を知る必要があります。現在、よく使われるJPG形式やPNG形式は圧縮処理されて保存されています。これに対して、BMP形式やTIFF形式は非圧縮形式で、画像データがそのまま保存されています。このため、ファイルは大きくな...