5 Commits

Author SHA1 Message Date
gballan d09369c394 [Issue #1] - Added configurable NTPServer option in config.go; also added DNS name resolution so you don't have to use IPs
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-11 00:39:54 -04:00
gballan 5e00c5c8ed [Issue #2] - Updating README with timezone info
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-11 00:31:11 -04:00
gballan 8798092cc8 [Issue #2] - Added timezone support with a list of support timezones
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-11 00:23:24 -04:00
gballan cc96fbccc0 [Issue #10] - Added config option to allow the user to set the hostname
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-10 23:57:46 -04:00
gballan e478758c49 [Issue #9] - Set network stack to enable ICMP, with a queue limit of 4
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-10 23:51:41 -04:00
5 changed files with 188 additions and 21 deletions
+20
View File
@@ -40,6 +40,26 @@ To help with debugging/testing deployments, I also used these components:
* [TinyGo](https://tinygo.org/getting-started/install/) - TinyGo installation guide
### Configuration
While **Pico Clock** supports timezones, the TinyGo environment doesn't include the IANA database for timezones. This means that each timezone has to be manually added in the code. *If you would like a timezone added, open an issue and I will look into adding it*.
Currently the list of supported timezones is:
* `UTC` - No timezone offset
* `US/Eastern` - Dynamic DST timezone for US Eastern; alias `ET`
* `US/Central` - Dynamic DST timezone for US Eastern; alias `CT`
* `US/Mountain` - Dynamic DST timezone for US Eastern; alias `MT`
* `US/Pacific` - Dynamic DST timezone for US Eastern; alias `PT`
* `EST` - Static set -5 offset (no DST change).
* `EDT` - Static set -4 offset (no DST change).
* `CST` - Static set -6 offset (no DST change).
* `CDT` - Static set -4 offset (no DST change).
* `MST` - Static set -7 offset (no DST change).
* `MDT` - Static set -5 offset (no DST change).
* `PST` - Static set -8 offset (no DST change).
* `PDT` - Static set -7 offset (no DST change).
### Steps
1. Clone this git repository `git clone https://git.metaunix.net/BitGoblin/pico-weather`.
+9
View File
@@ -1,6 +1,15 @@
package main
const (
// Wi-Fi settings
WifiSSID = "MyWifiNetwork"
WifiPass = "MyWifiPassword"
// Network settings
Hostname = "pico2w-clock"
EnableICMP = true
// Time settings
NTPServer = "pool.ntp.org"
ActiveTimezone = "UTC"
)
+23 -11
View File
@@ -109,12 +109,23 @@ func main() {
// 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("Resolving NTP server:", NTPServer)
oled.ClearBuffer()
tinyfont.WriteLine(oled, defaultFont, 0, 32, "Resolving NTP...", white)
oled.Display()
ntpServer, err := resolveHostToIP(netStack, NTPServer, 5*time.Second)
if err != nil {
panicErr("NTP DNS lookup failed", err)
}
println("NTP server IP: ", ntpServer.String())
time.Sleep(1 * time.Second)
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)
@@ -128,30 +139,31 @@ func main() {
lastSecond := -1
// Remove static call before the loop, then in the loop:
for {
currentTime := time.Now().Add(timeOffset).UTC()
currentSecond := currentTime.Second()
// Skip display refreshes if the second hasn't changed to save the i2c bus
if currentSecond != lastSecond {
lastSecond = currentSecond
// Alternate colon (:) with space ( ) every second
timeSeperator := ":"
if currentSecond%2 == 1 {
timeSeperator = " "
}
// Create the awful time string before displaying it
timeStr := fmt.Sprintf("%02d%s%02d%s%02d UTC",
currentTime.Hour(), timeSeperator,
currentTime.Minute(), timeSeperator,
currentTime.Second())
// 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()
// Time
tinyfont.WriteLine(oled, defaultFont, 8, 32, timeStr, white)
// IP Address
tinyfont.WriteLine(oled, tinyFont, 0, 60, fmt.Sprintf("IP: %s", ipStr), white)
oled.Display()
}
+36 -1
View File
@@ -112,16 +112,20 @@ func InitNetwork(ssid, pass string) (*NetworkStack, error) {
stack := &xnet.StackAsync{}
err = stack.Reset(xnet.StackConfig{
Hostname: "pico2w-bme280",
Hostname: Hostname,
RandSeed: time.Now().UnixNano(),
MaxActiveTCPPorts: 2,
MTU: uint16(framelen - ethernet.MaxOverheadSize),
HardwareAddress: hwaddr,
ICMPQueueLimit: 4,
})
if err != nil {
return nil, fmt.Errorf("stack config reset: %w", err)
}
// Enable network pings
_ = stack.EnableICMP(EnableICMP)
// Start background frame pump
ctx := context.Background()
go stackLoop(ctx, stack, adapter)
@@ -221,3 +225,34 @@ func tcpBackoff(consecutiveBackoffs uint) time.Duration {
wait := min(shifted, maxWait)
return time.Duration(wait)
}
// resolveHostToIP checks if host is already an IP literal, or queries the DNS server.
func resolveHostToIP(stack *NetworkStack, host string, timeout time.Duration) (netip.Addr, error) {
// 1. If it's already an IP string (e.g. "162.159.200.1"), parse and return directly
if ip, err := netip.ParseAddr(host); err == nil {
return ip, nil
}
// 2. Start asynchronous DNS A-record query
if err := stack.stack.StartLookupIP(host); err != nil {
return netip.Addr{}, fmt.Errorf("start DNS lookup: %w", err)
}
// 3. Poll ResultLookupIP until done or timed out
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
addrs, done, err := stack.stack.ResultLookupIP(host)
if done {
if err != nil {
return netip.Addr{}, fmt.Errorf("DNS lookup error: %w", err)
}
if len(addrs) == 0 {
return netip.Addr{}, fmt.Errorf("no addresses returned for %s", host)
}
return addrs[0], nil
}
time.Sleep(50 * time.Millisecond)
}
return netip.Addr{}, fmt.Errorf("DNS lookup timed out for %s", host)
}
+91
View File
@@ -0,0 +1,91 @@
package main
import (
"time"
)
type TimezoneConfig struct {
StandardName string
StandardOffset int
DaylightName string
DaylightOffset int
ObservesUSDST bool
}
// Map timezone aliases to standard/daylight offsets
var timezones = map[string]TimezoneConfig{
// Dynamic US zones (switch automatically between Standard and Daylight)
"US/Eastern": {"EST", -5 * 3600, "EDT", -4 * 3600, true},
"ET": {"EST", -5 * 3600, "EDT", -4 * 3600, true},
"US/Central": {"CST", -6 * 3600, "CDT", -5 * 3600, true},
"CT": {"CST", -6 * 3600, "CDT", -5 * 3600, true},
"US/Mountain": {"MST", -7 * 3600, "MDT", -6 * 3600, true},
"MT": {"MST", -7 * 3600, "MDT", -6 * 3600, true},
"US/Pacific": {"PST", -8 * 3600, "PDT", -7 * 3600, true},
"PT": {"PST", -8 * 3600, "PDT", -7 * 3600, true},
// Fixed non-DST zones
"UTC": {"UTC", 0, "UTC", 0, false},
"EST": {"EST", -5 * 3600, "EST", -5 * 3600, false},
"EDT": {"EDT", -4 * 3600, "EDT", -4 * 3600, false},
"CST": {"CST", -6 * 3600, "CST", -6 * 3600, false},
"CDT": {"CDT", -5 * 3600, "CDT", -5 * 3600, false},
"MST": {"MST", -7 * 3600, "MST", -7 * 3600, false},
"MDT": {"MDT", -6 * 3600, "MDT", -6 * 3600, false},
"PST": {"PST", -8 * 3600, "PST", -8 * 3600, false},
"PDT": {"PDT", -7 * 3600, "PDT", -7 * 3600, false},
}
// IsUSDST checks if the given UTC time falls in US Daylight Saving Time
// (Second Sunday in March at 02:00 local to first Sunday in November at 02:00 local).
func IsUSDST(t time.Time, stdOffsetSec int) bool {
month := t.Month()
if month < time.March || month > time.November {
return false // Dec, Jan, Feb
}
if month > time.March && month < time.November {
return true // Apr through Oct
}
day := t.Day()
weekday := int(t.Weekday())
if month == time.March {
// Second Sunday in March is between 8th and 14th
prevSunday := day - weekday
if prevSunday < 8 {
return false
}
if prevSunday >= 15 {
return true
}
// Transition occurs at 2:00 AM standard local time -> (2 - stdOffsetHours) in UTC
stdOffsetHours := stdOffsetSec / 3600
switchHourUTC := 2 - stdOffsetHours
return weekday != int(time.Sunday) || t.Hour() >= switchHourUTC
}
// November: first Sunday is between 1st and 7th
prevSunday := day - weekday
if prevSunday < 1 {
return true // Before first Sunday
}
// Fall-back occurs at 2:00 AM daylight local time -> (2 - (stdOffsetHours + 1)) in UTC
stdOffsetHours := stdOffsetSec / 3600
switchHourUTC := 2 - (stdOffsetHours + 1)
return weekday == int(time.Sunday) && t.Hour() < switchHourUTC
}
// ResolveTimezone dynamically determines the *time.Location and label
// based on whether the zone observes DST at the given UTC time.
func ResolveTimezone(name string, utcNow time.Time) (*time.Location, string) {
cfg, ok := timezones[name]
if !ok {
return time.FixedZone("UTC", 0), "UTC"
}
if cfg.ObservesUSDST && IsUSDST(utcNow, cfg.StandardOffset) {
return time.FixedZone(cfg.DaylightName, cfg.DaylightOffset), cfg.DaylightName
}
return time.FixedZone(cfg.StandardName, cfg.StandardOffset), cfg.StandardName
}