package main import ( "fmt" "machine" "net/netip" "time" "image/color" "tinygo.org/x/drivers/ssd1306" "tinygo.org/x/tinyfont" "tinygo.org/x/tinyfont/freesans" "tinygo.org/x/tinyfont/proggy" ) var ( defaultFont = &freesans.Regular9pt7b tinyFont = &proggy.TinySZ8pt7b ) // ------------------------------------------------------------- // Panic Handler & Entry Point // ------------------------------------------------------------- func panicErr(msg string, err error) { if err != nil { println("FATAL:", msg, "-", err.Error()) } for { time.Sleep(500 * time.Millisecond) } } // syncNTP triggers an NTP request and polls until completed or timed out. func syncNTP(stack *NetworkStack, server netip.Addr, timeout time.Duration) (time.Duration, error) { if err := stack.stack.StartNTP(server); err != nil { return 0, fmt.Errorf("start NTP: %w", err) } deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { offset, done := stack.stack.ResultNTPOffset() if done { return offset, nil } time.Sleep(50 * time.Millisecond) } return 0, fmt.Errorf("NTP request timed out") } func main() { time.Sleep(2 * time.Second) println("Starting app...") // Bump frequency to 400kHz for snappy display flushes i2c := machine.I2C0 err := i2c.Configure(machine.I2CConfig{ Frequency: 400 * machine.KHz, SDA: machine.GPIO4, SCL: machine.GPIO5, }) if err != nil { panicErr("I2C configuration", err) } // Instantiate nil oled variable var oled *ssd1306.Device // White color for display white := color.RGBA{R: 255, G: 255, B: 255, A: 255} // Probe address 0x3C with a zero-byte or 1-byte read println("Initializing display...") var dummy [1]byte if err := i2c.ReadRegister(0x3C, 0x00, dummy[:]); err == nil { println("SSD1306 detected at 0x3C") dev := ssd1306.NewI2C(i2c) dev.Configure(ssd1306.Config{ Address: 0x3C, Width: 128, Height: 64, }) oled = dev // Run some initial clearing and add booting text to the screen oled.ClearBuffer() tinyfont.WriteLine(oled, defaultFont, 0, 32, "Booting...", white) oled.Display() time.Sleep(1 * time.Second) } else { panicErr("Failed to init display.", err) } println("Starting Wi-Fi...") oled.ClearBuffer() tinyfont.WriteLine(oled, defaultFont, 0, 32, "Starting Wi-Fi...", white) oled.Display() netStack, err := InitNetwork(WifiSSID, WifiPass) if err != nil { panicErr("network init", err) } println("Wi-Fi Connected!") oled.ClearBuffer() tinyfont.WriteLine(oled, defaultFont, 0, 32, "Wi-Fi Connected!", white) oled.Display() time.Sleep(1 * time.Second) // Extract IP string from the returned NetworkStack ipStr := netStack.localIP.String() // Sync wall time using public NTP (e.g. Cloudflare: 162.159.200.1 / time.cloudflare.com) println("Synchronizing clock via NTP...") oled.ClearBuffer() tinyfont.WriteLine(oled, defaultFont, 0, 32, "Syncing NTP...", white) oled.Display() ntpServer := netip.MustParseAddr("162.159.200.1") timeOffset, err := syncNTP(netStack, ntpServer, 5*time.Second) if err != nil { panicErr("NTP sync failed", err) } println("NTP synced successfully!") oled.ClearBuffer() tinyfont.WriteLine(oled, defaultFont, 0, 32, "NTP synced!", white) oled.Display() time.Sleep(1 * time.Second) lastSecond := -1 // Remove static call before the loop, then in the loop: for { currentTime := time.Now().Add(timeOffset).UTC() currentSecond := currentTime.Second() if currentSecond != lastSecond { lastSecond = currentSecond timeSeperator := ":" if currentSecond%2 == 1 { timeSeperator = " " } // Dynamically determine DST vs Standard time for this second loc, zoneName := ResolveTimezone(ActiveTimezone, currentTime) localTime := currentTime.In(loc) timeStr := fmt.Sprintf("%02d%s%02d%s%02d %s", localTime.Hour(), timeSeperator, localTime.Minute(), timeSeperator, localTime.Second(), zoneName) oled.ClearBuffer() tinyfont.WriteLine(oled, defaultFont, 8, 32, timeStr, white) tinyfont.WriteLine(oled, tinyFont, 0, 60, fmt.Sprintf("IP: %s", ipStr), white) oled.Display() } time.Sleep(10 * time.Millisecond) } }