From 8c859f571d061e6df215b40cabeba43be8b0f3fb Mon Sep 17 00:00:00 2001 From: rdon Date: Sat, 7 Jun 2025 23:50:30 +0900 Subject: [PATCH] Add software. (23_akatonbo) --- 23_akatonbo/display.go | 176 ++++++++++++++++++++++++ 23_akatonbo/go.mod | 10 ++ 23_akatonbo/go.sum | 6 + 23_akatonbo/main.go | 304 +++++++++++++++++++++++++++++++++++++++++ main.go | 0 5 files changed, 496 insertions(+) create mode 100644 23_akatonbo/display.go create mode 100644 23_akatonbo/go.mod create mode 100644 23_akatonbo/go.sum create mode 100644 23_akatonbo/main.go create mode 100644 main.go diff --git a/23_akatonbo/display.go b/23_akatonbo/display.go new file mode 100644 index 0000000..84f6e5b --- /dev/null +++ b/23_akatonbo/display.go @@ -0,0 +1,176 @@ +package main + +import ( + "fmt" + "image/color" + "machine" + "time" + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ssd1306" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/shnm" +) + +// 画面レイアウト定数 +const ( + STATUS_Y = 12 // ステータス行のテキスト表示位置 + STATUS_AREA_END = 19 // ステータス行エリアの終端 + SEPARATOR_Y = 16 // 区切り線の位置 + SCROLL_START_Y = 30 // スクロール領域の開始位置 + SCROLL_CLEAR_Y = 20 // スクロール領域のクリア開始位置 + LINE_HEIGHT = 12 // 行の高さ + SCROLL_MAX_Y = 60 // スクロール領域の最大Y位置 + MAX_SCROLL_LINES = 3 // スクロール表示可能行数 +) + +var white = color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} +var black = color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF} + +// Display 構造体 - 画面制御とスクロール管理 +type Display struct { + device *ssd1306.Device + currentLine int // 現在の表示行(自動スクロール用) + lines [MAX_SCROLL_LINES]string // 表示中の行を保存 +} + +// InitDisplay ディスプレイを初期化する関数 +func InitDisplay() *Display { + machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 2.8 * machine.MHz, + SDA: machine.GPIO12, + SCL: machine.GPIO13, + }) + + device := ssd1306.NewI2C(machine.I2C0) + device.Configure(ssd1306.Config{ + Address: 0x3C, + Width: 128, + Height: 64, + }) + + device.SetRotation(drivers.Rotation180) + device.ClearDisplay() + time.Sleep(50 * time.Millisecond) + + return &Display{ + device: &device, + currentLine: 0, + lines: [MAX_SCROLL_LINES]string{}, // 空文字列で初期化 + } +} + +// UpdateStatus ステータス行を更新する関数 +func (d *Display) UpdateStatus(status string) { + // ステータス行のエリアをクリア + for y := 0; y < STATUS_AREA_END; y++ { + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), int16(y), black) + } + } + + // ステータス行に表示 + tinyfont.WriteLine(d.device, &shnm.Shnmk12, 0, STATUS_Y, status, white) + + // 区切り線を描画 + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), SEPARATOR_Y, white) + } + + d.device.Display() +} + +// ClearScrollArea ステータス行以外をクリアする関数 +func (d *Display) ClearScrollArea() { + for y := SCROLL_CLEAR_Y; y < 64; y++ { + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), int16(y), black) + } + } + d.currentLine = 0 // 表示行をリセット + // 行バッファもクリア + for i := range d.lines { + d.lines[i] = "" + } + d.device.Display() +} + +// PrintLine 一行表示する関数(必要に応じて自動スクロール) +func (d *Display) PrintLine(message string) { + // デバッグ情報をコンソールに出力 + fmt.Printf("PrintLine: currentLine=%d, message='%s'\n", d.currentLine, message) + + // 表示可能行数を超えた場合は画面をスクロール + if d.currentLine >= MAX_SCROLL_LINES { + fmt.Println("スクロール実行中...") + d.scrollUp() + d.currentLine = MAX_SCROLL_LINES - 1 + fmt.Printf("スクロール後: currentLine=%d\n", d.currentLine) + } + + // 新しいメッセージを配列に保存 + d.lines[d.currentLine] = message + fmt.Printf("行[%d]に保存: '%s'\n", d.currentLine, message) + + // 画面を再描画(スクロール領域のみ) + d.redrawScrollArea() + d.currentLine++ + fmt.Printf("currentLine更新: %d\n", d.currentLine) +} + +// scrollUp 画面を1行上にスクロールする(内部関数) +func (d *Display) scrollUp() { + fmt.Println("scrollUp開始") + // デバッグ: スクロール前の状態を表示 + fmt.Print("スクロール前の行: ") + for i := 0; i < MAX_SCROLL_LINES; i++ { + fmt.Printf("[%d]='%s' ", i, d.lines[i]) + } + fmt.Println() + + // 行を1つずつ上に移動 + for i := 0; i < MAX_SCROLL_LINES-1; i++ { + d.lines[i] = d.lines[i+1] + } + // 最後の行をクリア + d.lines[MAX_SCROLL_LINES-1] = "" + + // デバッグ: スクロール後の状態を表示 + fmt.Print("スクロール後の行: ") + for i := 0; i < MAX_SCROLL_LINES; i++ { + fmt.Printf("[%d]='%s' ", i, d.lines[i]) + } + fmt.Println() +} + +// redrawScrollArea スクロール領域を再描画する(内部関数) +func (d *Display) redrawScrollArea() { + fmt.Println("redrawScrollArea開始") + + // スクロール領域をクリア + for y := SCROLL_CLEAR_Y; y < 64; y++ { + for x := 0; x < 128; x++ { + d.device.SetPixel(int16(x), int16(y), black) + } + } + + // 保存されている全ての行を再描画 + for i := 0; i < MAX_SCROLL_LINES; i++ { + if d.lines[i] != "" { + y := int16(i*LINE_HEIGHT + SCROLL_START_Y) + fmt.Printf("描画: 行[%d] Y=%d '%s'\n", i, y, d.lines[i]) + if y >= SCROLL_START_Y && y <= SCROLL_MAX_Y { + tinyfont.WriteLine(d.device, &shnm.Shnmk12, 0, y, d.lines[i], white) + } else { + fmt.Printf("描画範囲外: Y=%d (範囲: %d-%d)\n", y, SCROLL_START_Y, SCROLL_MAX_Y) + } + } + } + + d.device.Display() + fmt.Println("redrawScrollArea完了") +} + +// GetDevice デバイスへの直接アクセス(必要な場合) +func (d *Display) GetDevice() *ssd1306.Device { + return d.device +} diff --git a/23_akatonbo/go.mod b/23_akatonbo/go.mod new file mode 100644 index 0000000..568058d --- /dev/null +++ b/23_akatonbo/go.mod @@ -0,0 +1,10 @@ +module 23_akatonbo + +go 1.24.1 + +require ( + tinygo.org/x/drivers v0.31.0 + tinygo.org/x/tinyfont v0.6.0 +) + +require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect diff --git a/23_akatonbo/go.sum b/23_akatonbo/go.sum new file mode 100644 index 0000000..8394e92 --- /dev/null +++ b/23_akatonbo/go.sum @@ -0,0 +1,6 @@ +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +tinygo.org/x/drivers v0.31.0 h1:Q2RpvTRMtdmjHD2Xyn4e8WXsJZKpIny3Lg4hzG1dLu4= +tinygo.org/x/drivers v0.31.0/go.mod h1:ZdErNrApSABdVXjA1RejD67R8SNRI6RKVfYgQDZtKtk= +tinygo.org/x/tinyfont v0.6.0 h1:GibXDSFz6xrWnEDkDRo6vsbOyRw0MVj/eza3zNHMSHs= +tinygo.org/x/tinyfont v0.6.0/go.mod h1:onflMSkpWl7r7j4MIqhPEVV39pn7yL4N3MOePl3G+G8= diff --git a/23_akatonbo/main.go b/23_akatonbo/main.go new file mode 100644 index 0000000..ad64614 --- /dev/null +++ b/23_akatonbo/main.go @@ -0,0 +1,304 @@ +package main + +// Please connect a piezo buzzer to the 3V3 and EX01 pins on the back terminal. +// +// | EX01 | EX03 | 3V3 | SDA0 | 3V3 | 3V3 | | GROVE | +// | EX02 | EX04 | GND | SCL0 | GND | GND | - - | GND | 3V3 | SDA0 | SCL0 | + +import ( + "fmt" + "machine" + "time" + "tinygo.org/x/drivers/tone" +) + +// タクトスイッチのピン定義 +const BUTTON_PIN = machine.GPIO2 + +// ボタンの初期化 +func initButton() { + BUTTON_PIN.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) +} + +// ボタンが押されたかチェック(チャタリング対策付き) +func isButtonPressed() bool { + if !BUTTON_PIN.Get() { // プルアップなので押されると false + time.Sleep(50 * time.Millisecond) // チャタリング対策 + if !BUTTON_PIN.Get() { // 再確認 + // ボタンが離されるまで待つ + for !BUTTON_PIN.Get() { + time.Sleep(10 * time.Millisecond) + } + time.Sleep(50 * time.Millisecond) // 離した後のチャタリング対策 + return true + } + } + return false +} + +type NoteWithDuration struct { + Note tone.Note + Duration time.Duration +} + +var pinToPWM = map[machine.Pin]tone.PWM{ + machine.GPIO14: machine.PWM7, // for EX01 +} + +// ブザーを初期化する関数 +func initBuzzer() (tone.Speaker, error) { + bzrPin := machine.GPIO14 + pwm := pinToPWM[bzrPin] + speaker, err := tone.New(pwm, bzrPin) + if err != nil { + return tone.Speaker{}, err + } + return speaker, nil +} + +func getSong() []interface{} { + const bpm = 100 + beat := time.Minute / bpm + eighth := beat / 2 + quarter := beat + dottedQuarter := beat * 3 / 2 + dottedHalf := beat * 3 + + return []interface{}{ + "夕焼け", + NoteWithDuration{tone.G4, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.C5, dottedQuarter}, + NoteWithDuration{tone.D5, eighth}, + + "小焼けの", + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.C6, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, quarter}, + + "赤とんぼ", + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.C5, quarter}, + NoteWithDuration{tone.D5, quarter}, + NoteWithDuration{tone.E5, dottedHalf}, + + "負われて", + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, dottedQuarter}, + NoteWithDuration{tone.A5, eighth}, + + "見たのは", + NoteWithDuration{tone.C6, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.A5, eighth}, + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.E5, eighth}, + + "何時の日か", + NoteWithDuration{tone.G5, eighth}, + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.E5, eighth}, + NoteWithDuration{tone.D5, eighth}, + NoteWithDuration{tone.C5, eighth}, + NoteWithDuration{tone.C5, dottedHalf}, + } +} + +// 音符名と周波数を取得する関数(全オクターブ対応) +func getNoteName(note tone.Note) string { + switch note { + // 3オクターブ + case tone.C3: + return "ド(C3) 131Hz" + case tone.CS3: + return "ド#(C#3) 139Hz" + case tone.D3: + return "レ(D3) 147Hz" + case tone.DS3: + return "レ#(D#3) 156Hz" + case tone.E3: + return "ミ(E3) 165Hz" + case tone.F3: + return "ファ(F3) 175Hz" + case tone.FS3: + return "ファ#(F#3) 185Hz" + case tone.G3: + return "ソ(G3) 196Hz" + case tone.GS3: + return "ソ#(G#3) 208Hz" + case tone.A3: + return "ラ(A3) 220Hz" + case tone.AS3: + return "ラ#(A#3) 233Hz" + case tone.B3: + return "シ(B3) 247Hz" + + // 4オクターブ + case tone.C4: + return "ド(C4) 262Hz" + case tone.CS4: + return "ド#(C#4) 277Hz" + case tone.D4: + return "レ(D4) 294Hz" + case tone.DS4: + return "レ#(D#4) 311Hz" + case tone.E4: + return "ミ(E4) 330Hz" + case tone.F4: + return "ファ(F4) 349Hz" + case tone.FS4: + return "ファ#(F#4) 370Hz" + case tone.G4: + return "ソ(G4) 392Hz" + case tone.GS4: + return "ソ#(G#4) 415Hz" + case tone.A4: + return "ラ(A4) 440Hz" + case tone.AS4: + return "ラ#(A#4) 466Hz" + case tone.B4: + return "シ(B4) 494Hz" + + // 5オクターブ + case tone.C5: + return "ド(C5) 523Hz" + case tone.CS5: + return "ド#(C#5) 554Hz" + case tone.D5: + return "レ(D5) 587Hz" + case tone.DS5: + return "レ#(D#5) 622Hz" + case tone.E5: + return "ミ(E5) 659Hz" + case tone.F5: + return "ファ(F5) 698Hz" + case tone.FS5: + return "ファ#(F#5) 740Hz" + case tone.G5: + return "ソ(G5) 784Hz" + case tone.GS5: + return "ソ#(G#5) 831Hz" + case tone.A5: + return "ラ(A5) 880Hz" + case tone.AS5: + return "ラ#(A#5) 932Hz" + case tone.B5: + return "シ(B5) 988Hz" + + // 6オクターブ + case tone.C6: + return "ド(C6) 1047Hz" + case tone.CS6: + return "ド#(C#6) 1109Hz" + case tone.D6: + return "レ(D6) 1175Hz" + case tone.DS6: + return "レ#(D#6) 1245Hz" + case tone.E6: + return "ミ(E6) 1319Hz" + case tone.F6: + return "ファ(F6) 1397Hz" + case tone.FS6: + return "ファ#(F#6) 1480Hz" + case tone.G6: + return "ソ(G6) 1568Hz" + case tone.GS6: + return "ソ#(G#6) 1661Hz" + case tone.A6: + return "ラ(A6) 1760Hz" + case tone.AS6: + return "ラ#(A#6) 1865Hz" + case tone.B6: + return "シ(B6) 1976Hz" + + default: + return "不明な音" + } +} + +// 楽曲を演奏する関数(画面表示付き) +func playSong(speaker tone.Speaker, song []interface{}, display *Display) { + noteIndex := 0 + for _, element := range song { + switch v := element.(type) { + case string: + display.UpdateStatus(v) + case NoteWithDuration: + noteIndex++ + noteName := getNoteName(v.Note) + display.PrintLine(fmt.Sprintf("%d: %s", noteIndex, noteName)) + + speaker.SetNote(v.Note) + time.Sleep(v.Duration) + speaker.Stop() + time.Sleep(10 * time.Millisecond) + } + } +} + +// デモ用のスクロール表示(削除) + +func main() { + fmt.Println("プログラム開始") + + // ディスプレイの初期化 + display := InitDisplay() + fmt.Println("ディスプレイ初期化完了") + display.PrintLine("ディスプレイ初期化完了") + + // ボタンの初期化 + initButton() + fmt.Println("ボタン初期化完了") + display.PrintLine("ボタン初期化完了") + + // ステータス行に「テスト音楽」を表示 + display.UpdateStatus("テスト音楽") + fmt.Println("ステータス行表示完了") + display.PrintLine("ステータス行表示完了") + + // ブザーの初期化 + speaker, err := initBuzzer() + if err != nil { + fmt.Println("failed to configure PWM") + display.PrintLine("PWM設定エラー") + return + } + fmt.Println("ブザー初期化完了") + display.PrintLine("ブザー初期化完了") + + // 楽曲データの取得 + song := getSong() + fmt.Println("楽曲データ取得完了") + display.PrintLine("楽曲データ取得完了") + + // 演奏回数カウンター + playCount := 0 + + // メインループ + display.PrintLine("ボタンを押す") + fmt.Println("ボタン待機中...") + display.UpdateStatus("待機中") + + for { + if isButtonPressed() { + playCount++ + fmt.Printf("ボタンが押されました - 演奏回数: %d\n", playCount) + display.PrintLine(fmt.Sprintf("演奏開始 (%d回目)", playCount)) + display.UpdateStatus("演奏中...") + + playSong(speaker, song, display) + + fmt.Printf("演奏完了 - %d回目\n", playCount) + display.PrintLine(fmt.Sprintf("演奏完了 (%d回目)", playCount)) + display.PrintLine("ボタンを押す") + display.UpdateStatus("待機中") + } + time.Sleep(100 * time.Millisecond) // CPU負荷軽減 + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e69de29