Added networking stack (mostly copied from pico-weather) and added NTP sync
This commit is contained in:
@@ -3,4 +3,6 @@ package main
|
|||||||
const (
|
const (
|
||||||
WifiSSID = "MyWifiName"
|
WifiSSID = "MyWifiName"
|
||||||
WifiPass = "MyWifiPassword"
|
WifiPass = "MyWifiPassword"
|
||||||
|
|
||||||
|
NtpServer = "216.239.35.0" // time.google.com IPv4
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ module git.metaunix.net/bitgoblin/pico-time
|
|||||||
go 1.25.2
|
go 1.25.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/soypat/cyw43439 v0.1.1
|
||||||
|
github.com/soypat/lneto v0.3.2
|
||||||
tinygo.org/x/drivers v0.36.0
|
tinygo.org/x/drivers v0.36.0
|
||||||
tinygo.org/x/tinyfont v0.7.0
|
tinygo.org/x/tinyfont v0.7.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
require (
|
||||||
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
|
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
|
||||||
|
github.com/tinygo-org/pio v0.3.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
github.com/soypat/cyw43439 v0.1.1 h1:vcaTiVzfuz3keK7lJpVxStZ6tV8HCw7Ugzsh1k4mneE=
|
||||||
|
github.com/soypat/cyw43439 v0.1.1/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc=
|
||||||
|
github.com/soypat/lneto v0.3.2 h1:iUFeRSq2czT7Db6MMOsAnMCBlKCqvIr941zsNf9dcu0=
|
||||||
|
github.com/soypat/lneto v0.3.2/go.mod h1:Be5PjwoYukvHFiUXxpYi8+ppH2F/gw/vjGBvFdv+Ti8=
|
||||||
|
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
|
||||||
|
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
|
||||||
|
github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
|
||||||
|
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
|
||||||
|
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
|
||||||
|
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||||
tinygo.org/x/drivers v0.36.0 h1:F0x342A6GWqh6abtCa57uAxCyz/b9MbGzvIVvIf+gpE=
|
tinygo.org/x/drivers v0.36.0 h1:F0x342A6GWqh6abtCa57uAxCyz/b9MbGzvIVvIf+gpE=
|
||||||
tinygo.org/x/drivers v0.36.0/go.mod h1:DQgKyHkB4G6IEOKVTAjApbKnWGwESN91EVJO+nMOE9Y=
|
tinygo.org/x/drivers v0.36.0/go.mod h1:DQgKyHkB4G6IEOKVTAjApbKnWGwESN91EVJO+nMOE9Y=
|
||||||
tinygo.org/x/tinyfont v0.7.0 h1:Ju901eGRThlHLoti9K7myMYyv4phd3I0DrJwc14NTTU=
|
tinygo.org/x/tinyfont v0.7.0 h1:Ju901eGRThlHLoti9K7myMYyv4phd3I0DrJwc14NTTU=
|
||||||
|
|||||||
@@ -4,28 +4,31 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"machine"
|
"machine"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tinygo.org/x/drivers/ssd1306"
|
"tinygo.org/x/drivers/ssd1306"
|
||||||
"tinygo.org/x/tinyfont"
|
"tinygo.org/x/tinyfont"
|
||||||
|
"tinygo.org/x/tinyfont/freemono"
|
||||||
"tinygo.org/x/tinyfont/proggy"
|
"tinygo.org/x/tinyfont/proggy"
|
||||||
"tinygo.org/x/tinyfont/freemono"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
white = color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
white = color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||||
black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
black = color.RGBA{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
currentTime time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Configure I2C0 using GP4 (SDA) and GP5 (SCL) at 400kHz
|
// Configure I2C0 with your pins: GP16 (SDA) and GP17 (SCL)
|
||||||
machine.I2C0.Configure(machine.I2CConfig{
|
machine.I2C0.Configure(machine.I2CConfig{
|
||||||
Frequency: 400 * machine.KHz,
|
Frequency: 400 * machine.KHz,
|
||||||
SDA: machine.GP16,
|
SDA: machine.GP16,
|
||||||
SCL: machine.GP17,
|
SCL: machine.GP17,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize the display (default I2C address for SSD1306 is usually 0x3C)
|
|
||||||
display := ssd1306.NewI2C(machine.I2C0)
|
display := ssd1306.NewI2C(machine.I2C0)
|
||||||
display.Configure(ssd1306.Config{
|
display.Configure(ssd1306.Config{
|
||||||
Address: 0x3C,
|
Address: 0x3C,
|
||||||
@@ -33,28 +36,65 @@ func main() {
|
|||||||
Height: 64,
|
Height: 64,
|
||||||
})
|
})
|
||||||
|
|
||||||
display.ClearDisplay()
|
// 1. Splash / Connection screen
|
||||||
|
display.ClearBuffer()
|
||||||
|
tinyfont.WriteLine(display, &proggy.TinySZ8pt7b, 8, 32, "Connecting Wi-Fi...", white)
|
||||||
|
display.Display()
|
||||||
|
|
||||||
// Base time for clock tracking (update to current time or set via NTP)
|
// 2. Initialize Wi-Fi hardware and lneto stack
|
||||||
currentTime := time.Date(2026, time.September, 7, 12, 0, 0, 0, time.UTC)
|
netStack, err := InitNetwork(WifiSSID, WifiPass)
|
||||||
|
if err != nil {
|
||||||
|
display.ClearBuffer()
|
||||||
|
tinyfont.WriteLine(display, &proggy.TinySZ8pt7b, 8, 32, "Wi-Fi Error!", white)
|
||||||
|
display.Display()
|
||||||
|
println("Wi-Fi init error:", err.Error())
|
||||||
|
for {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. NTP Fetch Status
|
||||||
|
display.ClearBuffer()
|
||||||
|
tinyfont.WriteLine(display, &proggy.TinySZ8pt7b, 8, 32, "Syncing NTP...", white)
|
||||||
|
display.Display()
|
||||||
|
|
||||||
|
syncedTime, err := netStack.FetchNTP(NtpServer)
|
||||||
|
if err != nil {
|
||||||
|
println("Initial NTP sync error:", err.Error())
|
||||||
|
currentTime = time.Date(2026, time.September, 7, 12, 0, 0, 0, time.UTC)
|
||||||
|
} else {
|
||||||
|
currentTime = syncedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Background NTP re-sync routine (every 1 hour)
|
||||||
|
go func() {
|
||||||
|
resyncTicker := time.NewTicker(1 * time.Hour)
|
||||||
|
for range resyncTicker.C {
|
||||||
|
if t, err := netStack.FetchNTP(NtpServer); err == nil {
|
||||||
|
mu.Lock()
|
||||||
|
currentTime = t
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 5. 1Hz UI rendering loop
|
||||||
ticker := time.NewTicker(1 * time.Second)
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
|
mu.Lock()
|
||||||
currentTime = currentTime.Add(1 * time.Second)
|
currentTime = currentTime.Add(1 * time.Second)
|
||||||
|
now := currentTime
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
// Format time and date strings
|
// Optional: Apply timezone offset if needed (e.g. now = now.Add(-4 * time.Hour))
|
||||||
timeStr := fmt.Sprintf("%02d:%02d:%02d", currentTime.Hour(), currentTime.Minute(), currentTime.Second())
|
timeStr := fmt.Sprintf("%02d:%02d:%02d", now.Hour(), now.Minute(), now.Second())
|
||||||
dateStr := currentTime.Format("Mon, Jan 02 2006")
|
dateStr := now.Format("Mon, Jan 02 2006")
|
||||||
|
|
||||||
// Clear screen buffer
|
|
||||||
display.ClearBuffer()
|
display.ClearBuffer()
|
||||||
|
|
||||||
// Render text to display buffer:
|
// Top: Date with Proggy
|
||||||
// WriteLine(target, font, x, y, text, color)
|
|
||||||
tinyfont.WriteLine(display, &proggy.TinySZ8pt7b, 16, 14, dateStr, white)
|
tinyfont.WriteLine(display, &proggy.TinySZ8pt7b, 16, 14, dateStr, white)
|
||||||
|
// Center: Time with Freemono Bold 12pt
|
||||||
tinyfont.WriteLine(display, &freemono.Bold12pt7b, 6, 44, timeStr, white)
|
tinyfont.WriteLine(display, &freemono.Bold12pt7b, 6, 44, timeStr, white)
|
||||||
|
|
||||||
// Push frame buffer to the OLED panel
|
|
||||||
display.Display()
|
display.Display()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/soypat/cyw43439"
|
||||||
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/ethernet"
|
||||||
|
"github.com/soypat/lneto/x/xnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pollTime = 5 * time.Millisecond
|
||||||
|
protoTimeout = 5 * time.Second
|
||||||
|
protoRetries = 3
|
||||||
|
|
||||||
|
tcpBufsize = 2048
|
||||||
|
tcpPacketQueueSize = 4
|
||||||
|
tcpConnPoolSize = 5
|
||||||
|
tcpEstablishedTimeout = 4 * time.Second
|
||||||
|
tcpCloseTimeout = protoTimeout
|
||||||
|
)
|
||||||
|
|
||||||
|
var nanotime = func() int64 {
|
||||||
|
return time.Now().UnixNano()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CywAdapter bridges the cyw43439 driver to the lneto interface requirements.
|
||||||
|
type CywAdapter struct {
|
||||||
|
dev *cyw43439.Device
|
||||||
|
rxBuf [1514]byte
|
||||||
|
rxLen int
|
||||||
|
hasPkt bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCywAdapter(dev *cyw43439.Device) *CywAdapter {
|
||||||
|
adapter := &CywAdapter{dev: dev}
|
||||||
|
|
||||||
|
dev.RecvEthHandle(func(pkt []byte) error {
|
||||||
|
if !adapter.hasPkt && len(pkt) <= len(adapter.rxBuf) {
|
||||||
|
copy(adapter.rxBuf[:], pkt)
|
||||||
|
adapter.rxLen = len(pkt)
|
||||||
|
adapter.hasPkt = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// In CywAdapter:
|
||||||
|
func (a *CywAdapter) PollHardware() error {
|
||||||
|
_, err := a.dev.TryPoll()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CywAdapter) SendEth(frame []byte) error {
|
||||||
|
return a.dev.SendEth(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CywAdapter) RecvEth(dst []byte) (int, error) {
|
||||||
|
if !a.hasPkt {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
n := copy(dst, a.rxBuf[:a.rxLen])
|
||||||
|
a.hasPkt = false
|
||||||
|
a.rxLen = 0
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CywAdapter) HardwareAddress6() ([6]byte, error) {
|
||||||
|
return a.dev.HardwareAddr6()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CywAdapter) MaxFrameLength() (int, error) {
|
||||||
|
return 1514, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkStack struct {
|
||||||
|
adapter *CywAdapter
|
||||||
|
stack *xnet.StackAsync
|
||||||
|
gostack xnet.StackGo
|
||||||
|
localIP netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitNetwork(ssid, pass string) (*NetworkStack, error) {
|
||||||
|
println("Initializing CYW43439 Wi-Fi hardware...")
|
||||||
|
dev := cyw43439.NewPicoWDevice()
|
||||||
|
cfg := cyw43439.DefaultWifiConfig()
|
||||||
|
if err := dev.Init(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("device init failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Associating with SSID:", ssid)
|
||||||
|
if err := dev.JoinWPA2(ssid, pass); err != nil {
|
||||||
|
return nil, fmt.Errorf("wifi association failed: %w", err)
|
||||||
|
}
|
||||||
|
println("Wi-Fi associated.")
|
||||||
|
|
||||||
|
adapter := NewCywAdapter(dev)
|
||||||
|
hwaddr, err := adapter.HardwareAddress6()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read MAC: %w", err)
|
||||||
|
}
|
||||||
|
framelen, err := adapter.MaxFrameLength()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("max frame len: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stack := &xnet.StackAsync{}
|
||||||
|
err = stack.Reset(xnet.StackConfig{
|
||||||
|
Hostname: "pico2w-bme280",
|
||||||
|
RandSeed: time.Now().UnixNano(),
|
||||||
|
MaxActiveTCPPorts: 2,
|
||||||
|
MTU: uint16(framelen - ethernet.MaxOverheadSize),
|
||||||
|
HardwareAddress: hwaddr,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stack config reset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start background frame pump
|
||||||
|
ctx := context.Background()
|
||||||
|
go stackLoop(ctx, stack, adapter)
|
||||||
|
|
||||||
|
println("Acquiring IP via DHCP...")
|
||||||
|
rstack := stack.StackRetrying(stackBackoff)
|
||||||
|
results, err := rstack.DoDHCPv4([4]byte{}, protoTimeout, protoRetries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("DHCP failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = stack.AssimilateDHCPResults(results)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("assimilate DHCP failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
println("Resolving router MAC...")
|
||||||
|
gateway, err := rstack.DoResolveHardwareAddress6(results.Router, protoTimeout, protoRetries)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("resolving router MAC failed: %w", err)
|
||||||
|
}
|
||||||
|
stack.SetGatewayHardwareAddr(gateway)
|
||||||
|
|
||||||
|
localIP := netip.AddrFrom4(results.AssignedAddr4)
|
||||||
|
println("DHCP lease assigned! IP:", localIP.String())
|
||||||
|
|
||||||
|
gostack := stack.StackBlocking(stackBackoff).StackGo(xnet.StackGoConfig{
|
||||||
|
ListenerPoolConfig: xnet.TCPPoolConfig{
|
||||||
|
PoolSize: tcpConnPoolSize,
|
||||||
|
QueueSize: tcpPacketQueueSize,
|
||||||
|
TxBufSize: tcpBufsize,
|
||||||
|
RxBufSize: tcpBufsize,
|
||||||
|
NanoTime: nanotime,
|
||||||
|
EstablishedTimeout: tcpEstablishedTimeout,
|
||||||
|
ClosingTimeout: tcpCloseTimeout,
|
||||||
|
NewBackoff: func() lneto.BackoffStrategy { return tcpBackoff },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return &NetworkStack{
|
||||||
|
adapter: adapter,
|
||||||
|
stack: stack,
|
||||||
|
gostack: gostack,
|
||||||
|
localIP: localIP,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *NetworkStack) DialTCP(ctx context.Context, hostIP string, port uint16) (net.Conn, error) {
|
||||||
|
rIP, err := netip.ParseAddr(hostIP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid host IPv4: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
laddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(ns.localIP, 0))
|
||||||
|
raddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(rIP, port))
|
||||||
|
|
||||||
|
const sockstream = 0x1
|
||||||
|
c, err := ns.gostack.Socket(ctx, "tcp", syscall.AF_INET, sockstream, laddr, raddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("socket dial: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, ok := c.(net.Conn)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("socket did not return a stream connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stackLoop(ctx context.Context, stack *xnet.StackAsync, adapter *CywAdapter) {
|
||||||
|
frameLength, _ := adapter.MaxFrameLength()
|
||||||
|
buf := make([]byte, frameLength)
|
||||||
|
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
// 1. Pump the CYW43439 hardware over SPI to trigger RecvEthHandle
|
||||||
|
_ = adapter.PollHardware()
|
||||||
|
|
||||||
|
// 2. Ingress: read from adapter into lneto
|
||||||
|
nread, err := adapter.RecvEth(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
println("recv err:", err.Error())
|
||||||
|
} else if nread > 0 {
|
||||||
|
err = stack.IngressEthernet(buf[:nread])
|
||||||
|
if err != nil && err != lneto.ErrPacketDrop {
|
||||||
|
println("ingress err:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Egress: send out any frames generated by lneto
|
||||||
|
nwrite, err := stack.EgressEthernet(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
println("egress err:", err.Error())
|
||||||
|
} else if nwrite > 0 {
|
||||||
|
if err := adapter.SendEth(buf[:nwrite]); err != nil {
|
||||||
|
println("send eth err:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nwrite == 0 && nread == 0 {
|
||||||
|
time.Sleep(pollTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stackBackoff(consecutiveBackoffs uint) time.Duration {
|
||||||
|
if consecutiveBackoffs < 10 {
|
||||||
|
return time.Millisecond
|
||||||
|
}
|
||||||
|
return 10 * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func tcpBackoff(consecutiveBackoffs uint) time.Duration {
|
||||||
|
const (
|
||||||
|
minWait = uint32(time.Microsecond)
|
||||||
|
maxWait = 5 * uint32(time.Millisecond)
|
||||||
|
maxShift = 22
|
||||||
|
)
|
||||||
|
shifted := minWait << min(consecutiveBackoffs, maxShift)
|
||||||
|
wait := min(shifted, maxWait)
|
||||||
|
return time.Duration(wait)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchNTP requests the current time from an NTP server IPv4 address over UDP (port 123).
|
||||||
|
func (ns *NetworkStack) FetchNTP(serverIP string) (time.Time, error) {
|
||||||
|
ip, err := netip.ParseAddr(serverIP)
|
||||||
|
if err != nil || !ip.Is4() {
|
||||||
|
return time.Time{}, fmt.Errorf("invalid IPv4 address: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), protoTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
laddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ns.localIP, 0))
|
||||||
|
raddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, 123))
|
||||||
|
|
||||||
|
const sockDgram = 0x2
|
||||||
|
c, err := ns.gostack.Socket(ctx, "udp", syscall.AF_INET, sockDgram, laddr, raddr)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("udp socket dial: %w", err)
|
||||||
|
}
|
||||||
|
conn, ok := c.(net.Conn)
|
||||||
|
if !ok {
|
||||||
|
return time.Time{}, errors.New("socket did not return a net.Conn")
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
_ = conn.SetDeadline(time.Now().Add(protoTimeout))
|
||||||
|
|
||||||
|
// Standard NTP request packet (48 bytes, leap indicator 0, version 4, mode 3 client = 0x23)
|
||||||
|
req := make([]byte, 48)
|
||||||
|
req[0] = 0x23
|
||||||
|
|
||||||
|
if _, err := conn.Write(req); err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("ntp write error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make([]byte, 48)
|
||||||
|
n, err := conn.Read(resp)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("ntp read error: %w", err)
|
||||||
|
}
|
||||||
|
if n < 48 {
|
||||||
|
return time.Time{}, fmt.Errorf("ntp response too short: %d bytes", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transmit timestamp seconds are at bytes 40-43 (big-endian)
|
||||||
|
secs := uint32(resp[40])<<24 | uint32(resp[41])<<16 | uint32(resp[42])<<8 | uint32(resp[43])
|
||||||
|
fraction := uint32(resp[44])<<24 | uint32(resp[45])<<16 | uint32(resp[46])<<8 | uint32(resp[47])
|
||||||
|
|
||||||
|
// NTP epoch begins Jan 1, 1900. Unix epoch begins Jan 1, 1970.
|
||||||
|
// Difference is 2,208,988,800 seconds (70 years).
|
||||||
|
const ntpEpochOffset = 2208988800
|
||||||
|
if secs < ntpEpochOffset {
|
||||||
|
return time.Time{}, errors.New("invalid NTP timestamp received")
|
||||||
|
}
|
||||||
|
|
||||||
|
unixSecs := int64(secs - ntpEpochOffset)
|
||||||
|
nanos := (int64(fraction) * 1e9) >> 32
|
||||||
|
|
||||||
|
return time.Unix(unixSecs, nanos), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user