main.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  1. // Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved.
  2. // Use of this source code is governed by the MIT-license that can be
  3. // found in the LICENSE file.
  4. // A portable Winlink client for amateur radio email.
  5. package main
  6. import (
  7. "bufio"
  8. "bytes"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "log"
  13. "net"
  14. "os"
  15. "os/exec"
  16. "path"
  17. "path/filepath"
  18. "runtime"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "github.com/spf13/pflag"
  23. "github.com/la5nta/wl2k-go/catalog"
  24. "github.com/la5nta/wl2k-go/fbb"
  25. "github.com/la5nta/wl2k-go/mailbox"
  26. "github.com/la5nta/wl2k-go/rigcontrol/hamlib"
  27. "github.com/la5nta/pat/cfg"
  28. "github.com/la5nta/pat/internal/gpsd"
  29. )
  30. const (
  31. MethodWinmor = "winmor"
  32. MethodArdop = "ardop"
  33. MethodTelnet = "telnet"
  34. MethodAX25 = "ax25"
  35. MethodSerialTNC = "serial-tnc"
  36. MethodPactor = "pactor"
  37. )
  38. var commands = []Command{
  39. {
  40. Str: "connect",
  41. Desc: "Connect to a remote station.",
  42. HandleFunc: connectHandle,
  43. Usage: UsageConnect,
  44. Example: ExampleConnect,
  45. MayConnect: true,
  46. },
  47. {
  48. Str: "interactive",
  49. Desc: "Run interactive mode.",
  50. Usage: "[options]",
  51. Options: map[string]string{
  52. "--http, -h": "Start http server for web UI in the background.",
  53. },
  54. HandleFunc: InteractiveHandle,
  55. MayConnect: true,
  56. LongLived: true,
  57. },
  58. {
  59. Str: "http",
  60. Desc: "Run http server for web UI.",
  61. Usage: "[options]",
  62. Options: map[string]string{
  63. "--addr, -a": "Listen address. Default is :8080.",
  64. },
  65. HandleFunc: httpHandle,
  66. MayConnect: true,
  67. LongLived: true,
  68. },
  69. {
  70. Str: "compose",
  71. Desc: "Compose a new message.",
  72. HandleFunc: func(args []string) {
  73. composeMessage(nil)
  74. },
  75. },
  76. {
  77. Str: "read",
  78. Desc: "Read messages.",
  79. HandleFunc: func(args []string) {
  80. readMail()
  81. },
  82. },
  83. {
  84. Str: "position",
  85. Aliases: []string{"pos"},
  86. Desc: "Post a position report (GPSd or manual entry).",
  87. Usage: "[options]",
  88. Options: map[string]string{
  89. "--latlon": "latitude,longitude in decimal degrees for manual entry. Will use GPSd if this is empty.",
  90. "--comment, -c": "Comment to be included in the position report.",
  91. },
  92. Example: ExamplePosition,
  93. HandleFunc: posReportHandle,
  94. },
  95. {
  96. Str: "extract",
  97. Desc: "Extract attachments from a message file.",
  98. Usage: "file",
  99. HandleFunc: extractMessageHandle,
  100. },
  101. {
  102. Str: "rmslist",
  103. Desc: "Print/search in list of RMS nodes.",
  104. Usage: "[options] [search term]",
  105. Options: map[string]string{
  106. "--mode, -m": "Mode filter.",
  107. "--band, -b": "Band filter (e.g. '80m').",
  108. "--force-download, -d": "Force download of latest list from winlink.org.",
  109. "--sort-distance, -s": "Sort by distance",
  110. },
  111. HandleFunc: rmsListHandle,
  112. },
  113. {
  114. Str: "configure",
  115. Desc: "Open configuration file for editing.",
  116. HandleFunc: configureHandle,
  117. },
  118. {
  119. Str: "version",
  120. Desc: "Print the application version",
  121. HandleFunc: func(args []string) {
  122. fmt.Printf("%s %s\n", AppName, versionString())
  123. },
  124. },
  125. {
  126. Str: "help",
  127. Desc: "Print detailed help for a given command.",
  128. // Avoid initialization loop by invoking helpHandler in main
  129. },
  130. }
  131. var (
  132. config cfg.Config
  133. rigs map[string]hamlib.VFO
  134. logWriter io.Writer
  135. eventLog *EventLogger
  136. exchangeChan chan ex // The channel that the exchange loop is listening on
  137. exchangeConn net.Conn // Pointer to the active session connection (exchange)
  138. mbox *mailbox.DirHandler // The mailbox
  139. listenHub *ListenerHub
  140. promptHub *PromptHub
  141. appDir string
  142. )
  143. var fOptions struct {
  144. IgnoreBusy bool // Move to connect?
  145. SendOnly bool // Move to connect?
  146. RadioOnly bool
  147. Robust bool
  148. MyCall string
  149. Listen string
  150. MailboxPath string
  151. ConfigPath string
  152. LogPath string
  153. EventLogPath string
  154. }
  155. func optionsSet() *pflag.FlagSet {
  156. set := pflag.NewFlagSet("options", pflag.ExitOnError)
  157. defaultMBox, _ := mailbox.DefaultMailboxPath()
  158. set.StringVar(&fOptions.MyCall, `mycall`, ``, `Your callsign (winlink user).`)
  159. set.StringVarP(&fOptions.Listen, "listen", "l", "", "Comma-separated list of methods to listen on (e.g. winmor,ardop,telnet,ax25).")
  160. set.StringVar(&fOptions.MailboxPath, "mbox", defaultMBox, "Path to mailbox directory")
  161. set.StringVar(&fOptions.ConfigPath, "config", fOptions.ConfigPath, "Path to config file")
  162. set.StringVar(&fOptions.LogPath, "log", fOptions.LogPath, "Path to log file. The file is truncated on each startup.")
  163. set.StringVar(&fOptions.EventLogPath, "event-log", fOptions.EventLogPath, "Path to event log file.")
  164. set.BoolVarP(&fOptions.SendOnly, `send-only`, "s", false, `Download inbound messages later, send only.`)
  165. set.BoolVarP(&fOptions.RadioOnly, `radio-only`, "", false, `Radio Only mode (Winlink Hybrid RMS only).`)
  166. set.BoolVarP(&fOptions.Robust, `robust`, "r", false, `Use robust modes only. (Useful to improve s/n-ratio at remote winmor station)`)
  167. set.BoolVar(&fOptions.IgnoreBusy, "ignore-busy", false, "Don't wait for clear channel before connecting to a node.")
  168. return set
  169. }
  170. func init() {
  171. listenHub = NewListenerHub()
  172. promptHub = NewPromptHub()
  173. var err error
  174. appDir, err = mailbox.DefaultAppDir()
  175. if err != nil {
  176. log.Fatal(err)
  177. }
  178. fOptions.ConfigPath = path.Join(appDir, "config.json")
  179. fOptions.LogPath = path.Join(appDir, strings.ToLower(AppName+".log"))
  180. fOptions.EventLogPath = path.Join(appDir, "eventlog.json")
  181. pflag.Usage = func() {
  182. fmt.Fprintf(os.Stderr, "%s is a client for the Winlink 2000 Network.\n\n", AppName)
  183. fmt.Fprintf(os.Stderr, "Usage:\n %s [options] command [arguments]\n", os.Args[0])
  184. fmt.Fprintln(os.Stderr, "\nCommands:")
  185. for _, cmd := range commands {
  186. fmt.Fprintf(os.Stderr, " %-15s %s\n", cmd.Str, cmd.Desc)
  187. }
  188. fmt.Fprintln(os.Stderr, "\nOptions:")
  189. optionsSet().PrintDefaults()
  190. fmt.Fprint(os.Stderr, "\n")
  191. }
  192. }
  193. func main() {
  194. cmd, args := parseFlags(os.Args)
  195. // Skip initialization for some commands
  196. switch cmd.Str {
  197. case "help":
  198. helpHandle(args)
  199. return
  200. case "configure", "version":
  201. cmd.HandleFunc(args)
  202. return
  203. }
  204. // Enable the GZIP extension experiment by default
  205. if _, ok := os.LookupEnv("GZIP_EXPERIMENT"); !ok {
  206. os.Setenv("GZIP_EXPERIMENT", "1")
  207. }
  208. // Parse configuration file
  209. var err error
  210. config, err = LoadConfig(fOptions.ConfigPath, cfg.DefaultConfig)
  211. if err != nil {
  212. log.Fatalf("Unable to load/write config: %s", err)
  213. }
  214. // Initialize logger
  215. f, err := os.Create(fOptions.LogPath)
  216. if err != nil {
  217. log.Fatal(err)
  218. }
  219. logWriter = io.MultiWriter(f, os.Stdout)
  220. log.SetOutput(logWriter)
  221. eventLog, err = NewEventLogger(fOptions.EventLogPath)
  222. if err != nil {
  223. log.Fatal("Unable to open event log file:", err)
  224. }
  225. // Read command line options from config if unset
  226. if fOptions.MyCall == "" && config.MyCall == "" {
  227. fmt.Fprint(os.Stderr, "Missing mycall\n")
  228. os.Exit(1)
  229. } else if fOptions.MyCall == "" {
  230. fOptions.MyCall = config.MyCall
  231. }
  232. // Ensure mycall is all upper case.
  233. fOptions.MyCall = strings.ToUpper(fOptions.MyCall)
  234. // Don't use config password if we don't use config mycall
  235. if !strings.EqualFold(fOptions.MyCall, config.MyCall) {
  236. config.SecureLoginPassword = ""
  237. }
  238. // Replace placeholders in connect aliases
  239. for k, v := range config.ConnectAliases {
  240. config.ConnectAliases[k] = strings.Replace(v, cfg.PlaceholderMycall, fOptions.MyCall, -1)
  241. }
  242. if fOptions.Listen == "" && len(config.Listen) > 0 {
  243. fOptions.Listen = strings.Join(config.Listen, ",")
  244. }
  245. // Make sure we clean up on exit, closing any open resources etc.
  246. defer cleanup()
  247. // Load the mailbox handler
  248. loadMBox()
  249. if cmd.MayConnect {
  250. rigs = loadHamlibRigs()
  251. exchangeChan = exchangeLoop()
  252. go func() {
  253. if config.VersionReportingDisabled {
  254. return
  255. }
  256. for { // Check every 6 hours, but it won't post more frequent than 24h.
  257. postVersionUpdate() // Ignore errors
  258. time.Sleep(6 * time.Hour)
  259. }
  260. }()
  261. }
  262. if cmd.LongLived {
  263. if fOptions.Listen != "" {
  264. Listen(fOptions.Listen)
  265. }
  266. scheduleLoop()
  267. }
  268. // Start command execution
  269. cmd.HandleFunc(args)
  270. }
  271. func configureHandle(args []string) {
  272. // Ensure config file has been written
  273. _, err := ReadConfig(fOptions.ConfigPath)
  274. if os.IsNotExist(err) {
  275. err = WriteConfig(cfg.DefaultConfig, fOptions.ConfigPath)
  276. if err != nil {
  277. log.Fatalf("Unable to write default config: %s", err)
  278. }
  279. }
  280. cmd := exec.Command(EditorName(), fOptions.ConfigPath)
  281. cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
  282. if err := cmd.Run(); err != nil {
  283. log.Fatalf("Unable to start editor: %s", err)
  284. }
  285. }
  286. func InteractiveHandle(args []string) {
  287. var http string
  288. set := pflag.NewFlagSet("interactive", pflag.ExitOnError)
  289. set.StringVar(&http, "http", "", "HTTP listen address")
  290. set.Lookup("http").NoOptDefVal = config.HTTPAddr
  291. set.Parse(args)
  292. if http == "" {
  293. Interactive()
  294. return
  295. }
  296. go func() {
  297. if err := ListenAndServe(http); err != nil {
  298. log.Println(err)
  299. }
  300. }()
  301. time.Sleep(time.Second)
  302. Interactive()
  303. }
  304. func httpHandle(args []string) {
  305. addr := config.HTTPAddr
  306. if addr == "" {
  307. addr = ":8080" // For backwards compatibility (remove in future)
  308. }
  309. set := pflag.NewFlagSet("http", pflag.ExitOnError)
  310. set.StringVarP(&addr, "addr", "a", addr, "Listen address.")
  311. set.Parse(args)
  312. if addr == "" {
  313. set.Usage()
  314. os.Exit(1)
  315. }
  316. promptHub.OmitTerminal(true)
  317. if err := ListenAndServe(addr); err != nil {
  318. log.Fatal(err)
  319. }
  320. }
  321. func connectHandle(args []string) {
  322. if args[0] == "" {
  323. fmt.Println("Missing argument, try 'connect help'.")
  324. }
  325. if success := Connect(args[0]); !success {
  326. os.Exit(1)
  327. }
  328. }
  329. func helpHandle(args []string) {
  330. arg := args[0]
  331. var cmd *Command
  332. for _, c := range commands {
  333. if c.Str == arg {
  334. cmd = &c
  335. break
  336. }
  337. }
  338. if arg == "" || cmd == nil {
  339. pflag.Usage()
  340. return
  341. }
  342. cmd.PrintUsage()
  343. }
  344. func cleanup() {
  345. listenHub.Close()
  346. if wmTNC != nil {
  347. if err := wmTNC.Close(); err != nil {
  348. log.Fatalf("Failure to close winmor TNC: %s", err)
  349. }
  350. }
  351. if adTNC != nil {
  352. if err := adTNC.Close(); err != nil {
  353. log.Fatalf("Failure to close ardop TNC: %s", err)
  354. }
  355. }
  356. if pModem != nil {
  357. if err := pModem.Close(); err != nil {
  358. log.Fatalf("Failure to close pactor modem: %s", err)
  359. }
  360. }
  361. eventLog.Close()
  362. }
  363. func loadMBox() {
  364. mbox = mailbox.NewDirHandler(
  365. path.Join(fOptions.MailboxPath, fOptions.MyCall),
  366. fOptions.SendOnly,
  367. )
  368. // Ensure the mailbox handler is ready
  369. if err := mbox.Prepare(); err != nil {
  370. log.Fatal(err)
  371. }
  372. }
  373. func loadHamlibRigs() map[string]hamlib.VFO {
  374. rigs := make(map[string]hamlib.VFO, len(config.HamlibRigs))
  375. for name, cfg := range config.HamlibRigs {
  376. if cfg.Address == "" {
  377. log.Printf("Missing address-field for rig '%s', skipping.", name)
  378. continue
  379. }
  380. rig, err := hamlib.Open(cfg.Network, cfg.Address)
  381. if err != nil {
  382. log.Printf("Initialization hamlib rig %s failed: %s.", name, err)
  383. continue
  384. }
  385. var vfo hamlib.VFO
  386. switch strings.ToUpper(cfg.VFO) {
  387. case "A", "VFOA":
  388. vfo, err = rig.VFOA()
  389. case "B", "VFOB":
  390. vfo, err = rig.VFOB()
  391. case "":
  392. vfo = rig.CurrentVFO()
  393. default:
  394. log.Printf("Cannot load rig '%s': Unrecognized VFO identifier '%s'", name, cfg.VFO)
  395. continue
  396. }
  397. if err != nil {
  398. log.Printf("Cannot load rig '%s': Unable to select VFO: %s", name, err)
  399. continue
  400. }
  401. f, err := vfo.GetFreq()
  402. if err != nil {
  403. log.Printf("Unable to get frequency from rig %s: %s.", name, err)
  404. } else {
  405. log.Printf("%s ready. Dial frequency is %s.", name, Frequency(f))
  406. }
  407. rigs[name] = vfo
  408. }
  409. return rigs
  410. }
  411. func extractMessageHandle(args []string) {
  412. if len(args) == 0 || args[0] == "" {
  413. panic("TODO: usage")
  414. }
  415. file, _ := os.Open(args[0])
  416. defer file.Close()
  417. msg := new(fbb.Message)
  418. if err := msg.ReadFrom(file); err != nil {
  419. log.Fatal(err)
  420. } else {
  421. fmt.Println(msg)
  422. for _, f := range msg.Files() {
  423. if err := ioutil.WriteFile(f.Name(), f.Data(), 0664); err != nil {
  424. log.Fatal(err)
  425. }
  426. }
  427. }
  428. }
  429. func EditorName() string {
  430. if e := os.Getenv("EDITOR"); e != "" {
  431. return e
  432. } else if e := os.Getenv("VISUAL"); e != "" {
  433. return e
  434. }
  435. switch runtime.GOOS {
  436. case "windows":
  437. return "notepad"
  438. case "linux":
  439. if path, err := exec.LookPath("editor"); err == nil {
  440. return path
  441. }
  442. }
  443. return "vi"
  444. }
  445. func composeMessage(replyMsg *fbb.Message) {
  446. msg := fbb.NewMessage(fbb.Private, fOptions.MyCall)
  447. fmt.Printf(`From [%s]: `, fOptions.MyCall)
  448. from := readLine()
  449. if from == "" {
  450. from = fOptions.MyCall
  451. }
  452. msg.SetFrom(from)
  453. fmt.Print(`To`)
  454. if replyMsg != nil {
  455. fmt.Printf(" [%s]", replyMsg.From())
  456. }
  457. fmt.Printf(": ")
  458. to := readLine()
  459. if to == "" && replyMsg != nil {
  460. msg.AddTo(replyMsg.From().String())
  461. } else {
  462. for _, addr := range strings.FieldsFunc(to, SplitFunc) {
  463. msg.AddTo(addr)
  464. }
  465. }
  466. ccCand := make([]fbb.Address, 0)
  467. if replyMsg != nil {
  468. for _, addr := range append(replyMsg.To(), replyMsg.Cc()...) {
  469. if !addr.EqualString(fOptions.MyCall) {
  470. ccCand = append(ccCand, addr)
  471. }
  472. }
  473. }
  474. fmt.Printf("Cc")
  475. if replyMsg != nil {
  476. fmt.Printf(" %s", ccCand)
  477. }
  478. fmt.Print(`: `)
  479. cc := readLine()
  480. if cc == "" && replyMsg != nil {
  481. for _, addr := range ccCand {
  482. msg.AddCc(addr.String())
  483. }
  484. } else {
  485. for _, addr := range strings.FieldsFunc(cc, SplitFunc) {
  486. msg.AddCc(addr)
  487. }
  488. }
  489. switch len(msg.Receivers()) {
  490. case 1:
  491. fmt.Print("P2P only [y/N]: ")
  492. ans := readLine()
  493. if strings.EqualFold("y", ans) {
  494. msg.Header.Set("X-P2POnly", "true")
  495. }
  496. case 0:
  497. fmt.Println("Message must have at least one recipient")
  498. os.Exit(1)
  499. }
  500. fmt.Print(`Subject: `)
  501. if replyMsg != nil {
  502. subject := strings.TrimSpace(strings.TrimPrefix(replyMsg.Subject(), "Re:"))
  503. subject = fmt.Sprintf("Re:%s", subject)
  504. fmt.Println(subject)
  505. msg.SetSubject(subject)
  506. } else {
  507. msg.SetSubject(readLine())
  508. }
  509. // A message without subject is not valid, so let's use a sane default
  510. if msg.Subject() == "" {
  511. msg.SetSubject("<No subject>")
  512. }
  513. // Read body
  514. fmt.Printf(`Press ENTER to start composing the message body. `)
  515. readLine()
  516. f, err := ioutil.TempFile("", strings.ToLower(fmt.Sprintf("%s_new_%d.txt", AppName, time.Now().Unix())))
  517. if err != nil {
  518. log.Fatalf("Unable to prepare temporary file for body: %s", err)
  519. }
  520. if replyMsg != nil {
  521. fmt.Fprintf(f, "--- %s %s wrote: ---\n", replyMsg.Date(), replyMsg.From().Addr)
  522. body, _ := replyMsg.Body()
  523. orig := ">" + strings.Replace(
  524. strings.TrimSpace(body),
  525. "\n",
  526. "\n>",
  527. -1,
  528. ) + "\n"
  529. f.Write([]byte(orig))
  530. f.Sync()
  531. }
  532. // Windows fix: Avoid 'cannot access the file because it is being used by another process' error.
  533. // Close the file before opening the editor.
  534. f.Close()
  535. cmd := exec.Command(EditorName(), f.Name())
  536. cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
  537. if err := cmd.Run(); err != nil {
  538. log.Fatalf("Unable to start body editor: %s", err)
  539. }
  540. f, err = os.OpenFile(f.Name(), os.O_RDWR, 0666)
  541. if err != nil {
  542. log.Fatalf("Unable to read temporary file from editor: %s", err)
  543. }
  544. var buf bytes.Buffer
  545. io.Copy(&buf, f)
  546. msg.SetBody(buf.String())
  547. f.Close()
  548. os.Remove(f.Name())
  549. // An empty message body is illegal. Let's set a sane default.
  550. if msg.BodySize() == 0 {
  551. msg.SetBody("<No message body>\n")
  552. }
  553. // END Read body
  554. fmt.Print("\n")
  555. for {
  556. fmt.Print(`Attachment [empty when done]: `)
  557. path := readLine()
  558. if path == "" {
  559. break
  560. }
  561. file, err := readAttachment(path)
  562. if err != nil {
  563. log.Println(err)
  564. continue
  565. }
  566. msg.AddFile(file)
  567. }
  568. fmt.Println(msg)
  569. postMessage(msg)
  570. }
  571. func readAttachment(path string) (*fbb.File, error) {
  572. f, err := os.Open(path)
  573. if err != nil {
  574. return nil, err
  575. }
  576. defer f.Close()
  577. name := filepath.Base(path)
  578. var resizeImage bool
  579. if isImageMediaType(name, "") {
  580. fmt.Print("This seems to be an image. Auto resize? [Y/n]: ")
  581. ans := readLine()
  582. resizeImage = ans == "" || strings.EqualFold("y", ans)
  583. }
  584. var data []byte
  585. if resizeImage {
  586. data, err = convertImage(f)
  587. ext := filepath.Ext(name)
  588. name = name[:len(name)-len(ext)] + ".jpg"
  589. } else {
  590. data, err = ioutil.ReadAll(f)
  591. }
  592. return fbb.NewFile(name, data), err
  593. }
  594. var stdin *bufio.Reader
  595. func readLine() string {
  596. if stdin == nil {
  597. stdin = bufio.NewReader(os.Stdin)
  598. }
  599. str, _ := stdin.ReadString('\n')
  600. return strings.TrimSpace(str)
  601. }
  602. func posReportHandle(args []string) {
  603. var latlon, comment string
  604. set := pflag.NewFlagSet("position", pflag.ExitOnError)
  605. set.StringVar(&latlon, "latlon", "", "")
  606. set.StringVarP(&comment, "comment", "c", "", "")
  607. set.Parse(args)
  608. report := catalog.PosReport{Comment: comment}
  609. if latlon != "" {
  610. parts := strings.Split(latlon, ",")
  611. if len(parts) != 2 {
  612. log.Fatal(`Invalid position format. Expected "latitude,longitude".`)
  613. }
  614. lat, err := strconv.ParseFloat(parts[0], 64)
  615. if err != nil {
  616. log.Fatal(err)
  617. }
  618. report.Lat = &lat
  619. lon, err := strconv.ParseFloat(parts[1], 64)
  620. if err != nil {
  621. log.Fatal(err)
  622. }
  623. report.Lon = &lon
  624. } else if config.GPSd.Addr != "" {
  625. conn, err := gpsd.Dial(config.GPSd.Addr)
  626. if err != nil {
  627. log.Fatalf("GPSd daemon: %s", err)
  628. }
  629. defer conn.Close()
  630. conn.Watch(true)
  631. log.Println("Waiting for position from GPSd...") //TODO: Spinning bar?
  632. pos, err := conn.NextPos()
  633. if err != nil {
  634. log.Fatalf("GPSd: %s", err)
  635. }
  636. report.Lat = &pos.Lat
  637. report.Lon = &pos.Lon
  638. if config.GPSd.UseServerTime {
  639. report.Date = time.Now()
  640. } else {
  641. report.Date = pos.Time
  642. }
  643. // Course and speed is part of the spec, but does not seem to be
  644. // supported by winlink.org anymore. Ignore it for now.
  645. if false && pos.Track != 0 {
  646. course := CourseFromFloat64(pos.Track, false)
  647. report.Course = &course
  648. }
  649. } else {
  650. fmt.Println("No position available. See --help")
  651. os.Exit(1)
  652. }
  653. if report.Date.IsZero() {
  654. report.Date = time.Now()
  655. }
  656. postMessage(report.Message(fOptions.MyCall))
  657. }
  658. func CourseFromFloat64(f float64, magnetic bool) catalog.Course {
  659. c := catalog.Course{Magnetic: magnetic}
  660. str := fmt.Sprintf("%03.0f", f)
  661. for i := 0; i < 3; i++ {
  662. c.Digits[i] = str[i]
  663. }
  664. return c
  665. }
  666. func postMessage(msg *fbb.Message) {
  667. if err := msg.Validate(); err != nil {
  668. fmt.Printf("WARNING - Message does not validate: %s\n", err)
  669. }
  670. if err := mbox.AddOut(msg); err != nil {
  671. log.Fatal(err)
  672. }
  673. fmt.Println("Message posted")
  674. }
  675. func versionString() string {
  676. return fmt.Sprintf("v%s (%s) %s/%s - %s", Version, GitRev, runtime.GOOS, runtime.GOARCH, runtime.Version())
  677. }