feat(icom): Wake-on-LAN for a deck that sleeps with its server
A 7760 switched fully off takes its LAN server down, and no session means no console and no ON button — the one thing remote operation cannot tolerate. The rig's MAC is learned from every login and persisted; when a dial finds nobody home, the magic packet goes out to it before the next attempt. The rig wakes into standby, the session opens, and the ON button does the rest.
This commit is contained in:
@@ -159,6 +159,7 @@ const (
|
||||
keyCATIcomNetUser = "cat.icom.net.user" // Icom network: Network User1 ID
|
||||
keyCATIcomNetPass = "cat.icom.net.pass" // Icom network: Network User1 password
|
||||
keyCATIcomNetAudio = "cat.icom.net.audio" // Icom network: stream RX audio on 50003 (experimental)
|
||||
keyCATIcomNetMAC = "cat.icom.net.mac" // the rig's MAC, learned at login, for Wake-on-LAN
|
||||
keyAudioMonitorOn = "audio.monitor.on" // play the network RX audio through the Listening device (the stream itself stays open for the recorder either way)
|
||||
keyCATTCIHost = "cat.tci.host" // TCI host (Expert Electronics SunSDR / ExpertSDR2)
|
||||
keyCATTCIPort = "cat.tci.port" // TCI WebSocket port (default 40001)
|
||||
@@ -1296,6 +1297,12 @@ func (a *App) startup(ctx context.Context) {
|
||||
}
|
||||
a.startQSORecorderIfEnabled()
|
||||
|
||||
// The Icom network backend remembers the rig's MAC so a deck switched
|
||||
// fully off can be woken with a magic packet when a session is wanted.
|
||||
cat.SetIcomMACStore(
|
||||
func(mac string) { a.setSetting(keyCATIcomNetMAC, mac) },
|
||||
func() string { return a.settingOr(keyCATIcomNetMAC, "") },
|
||||
)
|
||||
a.reloadCAT()
|
||||
|
||||
// The QSO logbook lives where the ACTIVE PROFILE points it: the local SQLite
|
||||
|
||||
+4
-2
@@ -36,7 +36,8 @@
|
||||
"Icom console: the TX meters get needle inertia — power holds its peak instead of flickering to 0 W between syllables, SWR shows the real peak then returns to the live value — and the wattage readout no longer wraps at three digits.",
|
||||
"Icom console: the meters use the same LED bars as the Flex and Elecraft consoles, and the mode badge says USB or LSB instead of an ambiguous SSB.",
|
||||
"Cluster: spots on the standard FT8/FT4 frequencies of every band now read FT8/FT4 when the comment names no mode — 30 m, 60 m, 80 m FT4, 17 m FT4 and 12 m were falling into the generic DATA bucket.",
|
||||
"OmniRig (Yaesu): with split engaged, tuning to a spot moves BOTH VFOs — on an FT-2000 the write landed on the TX VFO only, so the transmitter QSYed and the receiver stayed behind."
|
||||
"OmniRig (Yaesu): with split engaged, tuning to a spot moves BOTH VFOs — on an FT-2000 the write landed on the TX VFO only, so the transmitter QSYed and the receiver stayed behind.",
|
||||
"Icom network: Wake-on-LAN — a rig switched fully off (LAN server down) is woken with a magic packet to its remembered MAC whenever a session is wanted, so the console and its ON button come back without a walk to the shack."
|
||||
],
|
||||
"fr": [
|
||||
"Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à l’affichage de la radio (il lisait environ deux points S trop bas).",
|
||||
@@ -72,7 +73,8 @@
|
||||
"Console Icom : les mètres TX gagnent une inertie d’aiguille — la puissance tient sa crête au lieu de retomber à 0 W entre les syllabes, le ROS montre la vraie crête puis revient à la valeur vive — et l’affichage en watts ne passe plus à la ligne à trois chiffres.",
|
||||
"Console Icom : les mètres utilisent les mêmes barres LED que les consoles Flex et Elecraft, et le badge de mode dit USB ou LSB au lieu d’un SSB ambigu.",
|
||||
"Cluster : les spots sur les fréquences standard FT8/FT4 de chaque bande lisent désormais FT8/FT4 quand le commentaire ne nomme pas de mode — 30 m, 60 m, FT4 80 m, FT4 17 m et 12 m tombaient dans le bloc DATA générique.",
|
||||
"OmniRig (Yaesu) : avec le split actif, se rendre sur un spot déplace LES DEUX VFO — sur un FT-2000 l’écriture n’atteignait que le VFO TX, donc l’émetteur QSYait et le récepteur restait derrière."
|
||||
"OmniRig (Yaesu) : avec le split actif, se rendre sur un spot déplace LES DEUX VFO — sur un FT-2000 l’écriture n’atteignait que le VFO TX, donc l’émetteur QSYait et le récepteur restait derrière.",
|
||||
"Réseau Icom : Wake-on-LAN — une radio complètement éteinte (serveur LAN coupé) est réveillée par un paquet magique vers sa MAC mémorisée dès qu’une session est voulue, et la console avec son bouton ON revient sans aller au shack."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ package cat
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -84,6 +85,23 @@ func NewIcomNet(host, user, pass string, civAddr int, digitalDefault string, aud
|
||||
tr, err := dialIcomNet(host, user, pass, "OpsLog", b.rigAddr, cancel, audioSink)
|
||||
if err == nil {
|
||||
lastNetConnect = time.Now()
|
||||
} else {
|
||||
// The deck did not answer — a 7760 switched OFF takes its LAN
|
||||
// server down entirely. Send Wake-on-LAN to the remembered MAC so
|
||||
// the NEXT attempt finds a server: the rig wakes into standby, the
|
||||
// session opens, and the console's ON button does the rest.
|
||||
icnMACMu.Lock()
|
||||
load := icnLoadMAC
|
||||
icnMACMu.Unlock()
|
||||
if load != nil {
|
||||
if mac := load(); mac != "" {
|
||||
if werr := icnWakeOnLAN(mac); werr != nil {
|
||||
debugLog.Printf("icom net: wake-on-LAN to %s failed: %v", mac, werr)
|
||||
} else {
|
||||
debugLog.Printf("icom net: deck unreachable — sent Wake-on-LAN to %s", mac)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tr, err
|
||||
}
|
||||
@@ -740,6 +758,11 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte, cancel <-chan
|
||||
return nil, err
|
||||
}
|
||||
debugLog.Printf("icom net: conninfo sent (rig mac % X) — opening CI-V stream", rigMAC)
|
||||
icnMACMu.Lock()
|
||||
if icnSaveMAC != nil {
|
||||
icnSaveMAC(hex.EncodeToString(rigMAC))
|
||||
}
|
||||
icnMACMu.Unlock()
|
||||
civ, err := net.DialUDP("udp4", &net.UDPAddr{Port: 50002}, vraddr)
|
||||
if err != nil {
|
||||
_ = ctrl.Close()
|
||||
@@ -1004,6 +1027,47 @@ func (n *icomNet) TXAudioSender() (func([]byte) error, error) {
|
||||
return n.audio.SendTXChunk, nil
|
||||
}
|
||||
|
||||
// The rig's MAC, learned from the login exchange and REMEMBERED (the app
|
||||
// persists it via these hooks): a 7760 that is switched off takes its LAN
|
||||
// server down with it, and the only way back in is Wake-on-LAN — which needs
|
||||
// the MAC when nothing else is answering.
|
||||
var (
|
||||
icnMACMu sync.Mutex
|
||||
icnSaveMAC func(mac string)
|
||||
icnLoadMAC func() string
|
||||
)
|
||||
|
||||
// SetIcomMACStore installs the persistence hooks for the rig's MAC address.
|
||||
func SetIcomMACStore(save func(string), load func() string) {
|
||||
icnMACMu.Lock()
|
||||
icnSaveMAC, icnLoadMAC = save, load
|
||||
icnMACMu.Unlock()
|
||||
}
|
||||
|
||||
// icnWakeOnLAN broadcasts the magic packet for mac ("aabbccddeeff" hex) so a
|
||||
// rig whose LAN server sleeps with it can be brought back without a walk to
|
||||
// the shack. Errors are returned for the log only — WOL is fire-and-forget.
|
||||
func icnWakeOnLAN(mac string) error {
|
||||
raw, err := hex.DecodeString(mac)
|
||||
if err != nil || len(raw) != 6 {
|
||||
return fmt.Errorf("bad MAC %q", mac)
|
||||
}
|
||||
pkt := make([]byte, 6+16*6)
|
||||
for i := 0; i < 6; i++ {
|
||||
pkt[i] = 0xFF
|
||||
}
|
||||
for i := 0; i < 16; i++ {
|
||||
copy(pkt[6+i*6:], raw)
|
||||
}
|
||||
conn, err := net.Dial("udp4", "255.255.255.255:9")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Write(pkt)
|
||||
return err
|
||||
}
|
||||
|
||||
func icnOpenClose(seq uint16, sentid, rcvdid uint32, civSeq uint16, magic byte) []byte {
|
||||
b := make([]byte, 0x16)
|
||||
icnLE.PutUint32(b[0:], 0x16)
|
||||
|
||||
Reference in New Issue
Block a user