Kilku kolegów pytało mnie, czy możliwe jest (w domyśle – proste) skorzystanie w skryptach pisanych w AHK z danych, udostępnianych przez OmniRIG – bardzo popularne rozwiązanie stosowane do wymiany danych pomiędzy programami a naszym radiem. Odpowiedź jest prosta – tak, jak najbardziej.
OmniRIG oferuje API, dzięki któremu mamy możliwość programowego odczytu danych radia, np. częstotliwości VFO A/B, stanu nadajnika, czy też np. stanu RIT/XIT albo aktualnej częstotliwości pitch dla CW itp. Bardzo wiele programów (logery, wsjx, itp itd) ma zaimplementowaną obsługę OmniRIG i generalnie to w miarę dobrze działa.
Dlaczego nie skorzystać z OmniRIG we własnych skryptach AHK?
OmniRIG API oferuje nie tylko dane do odczytu. W prosty sposób poprzez to API możemy sami wysłać do radia dowolną komendę CAT ustawiającą wybrany parametr radia. Nawet więcej – możemy wysłać dowolne komendy CAT typu Read/Answer (porównaj wpis https://www.radiohotkey.pl/komendy-cat/); OmniRIG wyśle zapytanie do radia, a poprzez API zwróci nam odpowiedź, którą będziemy mogli przetworzyć w skrycie.
Przykładowo – chcemy z poziomu skryptu zapytać „Która antena aktualnie jest podłączona do MainBand / VFOA ?”
Wysyłamy poprzez OmniRIG API komendę (np. wg CAT Yaesu) AN0; OmniRIG w odpowiedzi generuje odpowiedź: AN0;AN01; API zwraca dwie informacje: wysłaną przez nas komendę CAT + odpowiedź CAT (zwracanie takiej pary bardzo ułatwia rozpoznanie w skryptach, czego dotyczyło pytanie. Więcej o tym dalszej części wpisu, przy opisie tzw. CustomReply z radia)
W kolejnych krokach przestawię, jak stworzyć mini skrypt AHK, którego zadaniem będzie prezentacja na ekranie aktualnej częstotliwości odczytywanej z aktywnego VFO i wybrany aktualnie Mode, i na przykładzie przełączania anteny kombinacją klawiszy sposób korzystania z OmniRIG API do obsługi dowolnych komend CAT
Wszystko korzystając tylko z funkcji OmniRIG API (już bez użycia pisania do portów COM :-), porównaj z wpisem dotyczącym SerialSend)
Czyli chcemy uzyskać taki prosty efekt – co w VFO-A, to na ekranie komputera, w postaci tekstu wyświetlanego w trybie OSD (On-Screen-Display):
Na początek może trochę podstaw, co daje nam OmniRIG API oskryptowane w AHK 🙂
Zacznijmy od takiego mini skryptu:
; AHK 1.1x (v1.1.31+) ; tested on Win7 ; OmniRIG ver. 1.20 ; www.radiohotkey.pl Omni := ComObjCreate("OmniRig.OmniRigX") RADIO1 := Omni.Rig1 while 1 { VFOA := RADIO1.FreqA VFOB := RADIO1.FreqB VFOA_txt := Format("{1:0.3f}", VFOA/1000) VFOB_txt := Format("{1:0.3f}", VFOB/1000) Tooltip, % "A: " . VFOA_txt " kHz B: " . VFOB_txt . " kHz" } Esc:: ExitApp
Jest to w pełni funkcjonalny skrypt, który na bieżąco pokazuje przy kursorze myszy aktualne częstotliwości naszego radia – VFO A i VFO B! >>>
Oczywiście do poprawnej pracy wymaga prawidłowo zainstalowanego OmniRIG w komputerze i działającego TRX.
Analiza kodu:
Omni := ComObjCreate("OmniRig.OmniRigX") RADIO1 := Omni.Rig1
bez wdawania się w technikalia 🙂 w/w deklarujemy w naszym skrypcie chęć podłączenia się do API naszego OmniRIG ( ComObjCreate() ) poprzez obiekt COM ( nasza nazwa Omni), oraz od razu tworzymy obiekt RADIO1, dający nam pełny dostęp do danych (frq, mode, itd) pierwszego radia (RIG1), zdefiniowanego w OmniRIG Settings:
Analogicznie możemy odwoływać się do RIG2 – poprzez definicję
Omni := ComObjCreate("OmniRig.OmniRigX") RADIO1 := Omni.Rig1 RADIO2 := Omni.Rig2
Ważne: Powyższe działa TYLKO z „oryginalnym” OmniRIG 1.x (nie zadziała z „klonem” v.2)
Od tego momentu mamy już pełną możliwość pobierania parametrów radia, np. w przysłowiowe „kółko” 🙂
co w powyższym przykładzie realizujemy w pętli
while 1 { ;komendy do wykonania }
(nieskończona pętla w AHK)
A co w pętli?
VFOA := RADIO1.FreqA VFOB := RADIO1.FreqB
oraz
VFOA_txt := Format("{1:0.3f}", VFOA/1000) VFOB_txt := Format("{1:0.3f}", VFOB/1000)
VFOA := RADIO1.FreqA
to nic innego, jak pobranie częstotliwości z radia VFO-A i przypisanie jej do zmiennej VFOA !! Proste? Proste 🙂
VFOA_txt := Format("{1:0.3f}", VFOA/1000)
to wprowadzenie do zmiennej VFO_txt sformatowanej wersji wartości, odczytanej z OmniRIG (OmniRIG zwraca wartość w postaci np. 14000000 (integer, w Hz) a my chcemy zobaczyć ją w kHz – dzielimy więc przez 1000 i formatujemy funkcją Format() z AHK
Analogicznie mamy dla VFO B.
Jak już mamy przygotowane dane, wyświetlamy je jako „tooltip” funkcją… ToolTip 😉 dostępną w AHK – wraz z dodatkowym formatowaniem tekstu (kHz):
Tooltip, % "A: " . VFOA_txt " kHz B: " . VFOB_txt . " kHz"
Na koniec standardowy AHK Hotkey, żeby móc szybko przerwać skrypt:
Esc:: ExitApp
Wszystko. Pierwszy skrypt działa!
Zapraszam do części II, będzie troszkę poważniej.
Bonus:
Lista kilku parametrów zwracana przez OmniRIG API, które możemy odczytywać bezpośrednio, w/w sposobem ( np. jako zmienna := RADIO1.parametr ) z obiektu reprezentującego nasze radio, czyli RADIO1 w przykładzie powyżej:
RADIO1.FreqA
RADIO1.FreqB
RADIO1.GetRxFrequency
RADIO1.GetTxFrequency
RADIO1.RigType
RADIO1.RitOffset
RADIO1.Pitch
RADIO1.StatusStr
mamy też:
RADIO1.mode << uwaga! wartość reprezentująca MODE jest zakodowana, zobacz opis w dalszej częsci opracowania
II. Zdarzenia (Events) generowane przez OmniRIG :: ParamsChange
Metoda pokazana w skrypcie 1 (pętla While i odczytywanie „w kółko” parametrów z OmniRIG) do prostych zastosowań może być wystarczająca, ale niepotrzebnie obciąża nasz skrypt (pętla nieskończona rotuje tak szybko, jak to możliwe 🙂 )
Na szczęście OmniRIG potrafi wygenerować zdarzenie w systemie, które programowo wyłapiemy i podejmiemy właściwą akcję we właściwym momencie.
Takim zdarzeniem może być zmiana częstotliwości radia (zakręciliśmy gałką VFO) czy zmiana CW na USB w radiu. Gdy nie dotykamy radia, zdarzenia nie są generowane, a skrypt może robić wtedy inne fajne rzeczy.
Do dzieła – poniżej skrypt podobny do pierwszego, ale zmodyfikowany do wersji „zdarzeniowej”:
Omni := ComObjCreate("OmniRig.OmniRigX") ComObjConnect(Omni, "OR_") RADIO1 := Omni.Rig1 OR_ParamsChange(1, 0x00000004 ) OR_ParamsChange(1, RADIO1.mode) ;--procedures ---------------------------------------------- OR_ParamsChange(par1, par2) { global switch { Case (par2 & 0x00000004): VFO_txt := Format("{1:0.3f}", RADIO1.FreqA/1000) Case (par2 & 0x00800000): mode_txt := "CW-U" Case (par2 & 0x01000000): mode_txt := "CW-L" Case (par2 & 0x02000000): mode_txt := "USB" Case (par2 & 0x04000000): mode_txt := "LSB" Case (par2 & 0x10000000): mode_txt := "RTTY-L" Case (par2 & 0x08000000): mode_txt := "RTTY-U" ;etc.. } Tooltip % VFO_txt " kHz mode: " . mode_txt Return } Esc:: ExitApp
Analiza kodu:
Omni := ComObjCreate("OmniRig.OmniRigX") ComObjConnect(Omni, "OR_") RADIO1 := Omni.Rig1
Część rzeczy już znamy, nowy jest fragment:
ComObjConnect(Omni, "OR_")
Zgodnie z definicją w dokumentacji AHK , ComObjConnect() łączy źródła zdarzeń obiektu COM z funkcjami o danym prefiksie.
W naszym przypadku procedury zaczynające się od prefiksu OR_ będą odpowiedzialne za obsługę zdarzeń generowanych przez OmniRIG.
Najczęściej będzie przez nas wykorzystywana procedura nazwie OR_ParamsChange(par1, par2) – jak można się domyśleć, jest ona wywoływana przy każdym zdarzeniu dotyczącym zmiany parametrów radia (czyli prawie zawsze, jak zrobimy COŚ WAŻNEGO w radiu i OmniRIG to wychwyci 🙂 )
Procedura obsługi zdarzenia:
OR_ParamsChange(par1, par2) { global switch { Case (par2 & 0x00000004): VFO_txt := Format("{1:0.3f}", RADIO1.FreqA/1000) Case (par2 & 0x00800000): mode_txt := "CW-U" Case (par2 & 0x01000000): mode_txt := "CW-L" ;etc. } Tooltip % VFO_txt " kHz mode: " . mode_txt Return }
O co tu chodzi?
OmniRIG, wywołując zdarzenie dotyczące zmiany parametrów radia, przekazuje nam dwa parametry: par1 oraz par2
par1 – przyjmuje wartość 1 lub 2 – jest to numer radia RIGx w konfiguracji OmniRIG które wygenerowało zdarzenie;
par2 – wartość liczbowa (integer) reprezentująca sumę zdarzeń, które wydarzyły się w ostatnim slocie czasowym
np:
par2 = 0x00800000 oznacza zdarzenie „nastąpiła zmiana mode na CW-U”
par2 = 0x00000004 oznacza zdarzenie „nastąpiła zmiana częstotliwości VFO-A”
OmniRIG pracuje w pętli, czas jednej pętli – tzw. pool – mamy zdefiniowany w setup OmniRIGa, wartość w [ms]
W związku z tym, może się zdarzyć, że wartość par2 przekazana do procedury, będzie opisywała więcej niż jedno zdarzenie:
np. co oznacza, gdy par2 = 0x00800004 ? 😉 „sumę” obu powyższych -> „nastąpiła zmiana częstotliwości VFOA ORAZ zmiana mode na CW-U”
W związku z tym badając rodzaj zdarzenia, dokonujemy sprytnego maskowania bitowego interesujących nas w danym momencie wartości/zdarzeń.
Takie sprawdzenie można zrobić w kodzie funkcją IF, lub wykorzystać funkcję Switch dostępną w AHK:
switch { Case (par2 & 0x00000004): VFO_txt := Format("{1:0.3f}", RADIO1.FreqA/1000) Case (par2 & 0x00800000): mode_txt := "CW-U" Case (par2 & 0x01000000): mode_txt := "CW-L" ;etc. }
Switch/case działa w taki sposób, że skrypt wykona tylko te instrukcje po „:” w danym wierszu, dla których CASE (warunek) będzie spełniony (w naszym wypadku <> 0)
czyli w naszej procedurze:
jeżeli par2 = 0x00000004 to na pewno (par2 & 0x00000004) <> 0 , więc podstawienie VFO_txt := … będzie wykonane
Można zrezygnować ze Switch i zastąpić IF’ami:
If (par2 & 0x00000004) VFO_txt := Format("{1:0.3f}", RADIO1.FreqA/1000) If (par2 & 0x00800000) mode_txt := "CW-U"
Na końcu procedury następują uaktualnienie wartości wyświetlanej przez ToolTip
Tooltip % VFO_txt ” kHz mode: ” . mode_txt
Wracając jeszcze do ostatniego skryptu. Zaraz po inicjowaniu OmniRIGa mamy dwie linie postaci:
OR_ParamsChange(1, 0x00000004 ) OR_ParamsChange(1, RADIO1.mode)
Ponieważ procedury obsługi zdarzenia OR_ są „normalnymi” procedurami języka AHK, więc możemy je wywoływać samodzielnie!
Czyli tak na prawdę udać, że wydarzyło się jakieś zdarzenie.
W naszym przypadku:
OR_ParamsChange(1, 0x00000004 )
..oznacza przekazanie do procedury „oszukanej” informacji o zmianie częstotliwości VFO A (co spowoduje uaktualnienie wartości zmienne VFO_txt) oraz:
OR_ParamsChange(1, RADIO1.mode)
..oznacza przekazanie do procedury informacji (par2 = RADIO1.mode) o konieczności uaktualnienia informacji o aktualnie ustawionym Mode
Powyższe jest wykorzystane do pobrania wartości początkowych z radia, zaraz po wystartowaniu skryptu.
WAŻNE: wartość par2 określa zawsze kod zdarzenia, nie przekazuje wartości danego parametru radia !
W związku z tym po pojawianiu się wartości w par2 należy podjąć dodatkowe czynności, aby odczytać faktyczną wartość interesującej nas wielkości, np:
pojawienie się par2 = 0x00000004 informuje tylko o zdarzeniu „zmiana częstotliwości”
Wartość częstotliwości musimy odczytać np. komendą: RADIO1.FreqA
Parametry OmniRIG & OmniRIG Client
Ilość parametrów, które mogą być przekazane do naszego skryptu jest do podejrzenia na stronie autora programu OmniRIG (patrz niżej)
Do testów, kiedy i jakie parametry generuje OmniRIG osobiście bardzo polecam programik dostępny na stronie OmniRIG – OmniRIG Client (tu wersja ZIP)
Program pokazuje, co się dzieje dokładnie z w/w parametrem par2, gdy bawimy się radiem – a teraz już wiemy, co i jak, i możemy obsłużyć te zdarzenia swoim własnym skryptem AHK
Jeżeli jesteś zainteresowany, pełną listę wartości parametru par2 znajdziemy w kodzie źródłowym OmniRIG – dokładnie w pliku dostępnym u twórcy pod adresem:
https://github.com/VE3NEA/OmniRig/blob/master/OmniRig_TLB.pas (plik tekstowy) – wiersze 59 do 89 – polecam analizę 🙂
Jak przykład wyprzedzająco – w docelowym skrypcie z OSD na pewno będziemy badać zdarzenia dotyczące stanu nadajnika / PTT i np. podświetlimy częstotliwość na czerwono w trakcie nadawania:
Case (par2 & 0x00200000): TX := 0 Case (par2 & 0x00400000): TX := 1
III. SendCustomCommand i zdarzenia CustomReply
Zapis i odczyt dowolnych parametrów radia z użyciem AHK i OmniRIG API
Tym razem pokażę, jak wysyłać do radia – przy użyciu AHK i OmniRIG API – dowolne komendy CAT, ustawiające i/lub odczytujące wybrane funkcje radia.
Więcej o komendach CAT https://www.radiohotkey.pl/komendy-cat/
Na początek kolejny działający, akademicki mini skrypt:
Alt-1 oraz Alt-2 przełącza anteny, Alt-3 zwraca proste info o antenie powiązanej z VFOA:
Omni := ComObjCreate("OmniRig.OmniRigX") ComObjConnect(Omni, "OR_") RADIO1 := Omni.Rig1 ;--procedures ---------------------------------------------- OR_ParamsChange(par1, par2) { ;tu np kod z poprzednich przykładów } OR_CustomReply(par1, par2, par3) { MsgBox % Array2Str(par3) } Array2Str(CATarray) { wyn := Chr(CATarray[0]) Loop % CATarray.MaxIndex() wyn .= Chr(CATarray[A_index]) Return wyn } !1:: { RADIO1.SendCustomCommand("AN01;",0,"") Return } !2:: { RADIO1.SendCustomCommand("AN02;",0,"") Return } !3:: { RADIO1.SendCustomCommand("AN0;",5,";") Return } Esc:: { ExitApp }
Na początek fragment kodu, uruchamiany naciśnięciem kombinacji Alt-1 na klawiaturze:
!1:: { RADIO1.SendCustomCommand("AN01;",0,"") Return }
Powyższy kod wysyła do radia komendę sterującą AN01; (ustaw antenę nr 1 dla MainBand VFO) Proste? 🙂
OmniRIG API oferuje metodę wysyłania dowolnej komendu CAT do naszego radia, poprzez object.SendCustomCommand(par1, par2, par3)
Co należy przekazać w wywołaniu?
par1 – komenda CAT, którą wysyłamy do radia, zgodnie z CAT reference
par2 – Oczekiwana ilość znaków odpowiedzi z radia
par3 – znak rozdzielający
Tak jak pisałem tu, komendy CAT mogą być sterujące (SET), lub odpytujące (READ/ANSWER).
Dla komend sterujących nie występuje zwrotna odpowiedź z radia, więc par2 = 0 oraz par3 = „” i wywołanie jest postaci:
RADIO1.SendCustomCommand("AN01;",0,"")
Ale jeżeli chcemy zapytać radia „która antena jest podłączona do MainBand VFO” – przykładowo komenda CAT z listy Yaesu to: AN0; to robimy to tak:
RADIO1.SendCustomCommand("AN0;",5,";")
ponieważ par2=5 oraz par3 = „;” to informujemy w ten sposób OmniRIGa, że radio odpowie nam 5-cioma znakami + znak średnika „;”
W naszym przykładzie jest to zgodne z CAT Reference:
A co z odpowiedzią z radia?
OmniRIG przekierowuje wszystkie nasze zapytania do radia poprzez port COM, i w miarę nadchodzących odpowiedzi generuje zdarzenia CustomReply, które wyłapujemy procedurą OR_CustomReply
OmniRIG przekazuje nam tu 3 parametry:
OR_CustomReply(par1, par2, par3)
par1 – numer radia
par2 – komenda CAT, którą wysłaliśmy jako zapytanie (tekst)
par3 – odpowiedź „Anwer” z radia (zobacz przykład z tabeli CAT na rysunku powyżej)
Ważne:
par3 nie jest wartością tekstową, ale tablicą z wartości od 0 do 255. Dlaczego?
W uproszczeniu – dla systemów CAT Yaesu i Kenwood te wartości generalnie odpowiadają znakom ASCII, ale są systemy CAT, które zwracają wartości nie związane ze znakami ASCII, stąd takie podejście.
Aby zamienić par3 – czyli tablicę wartości znakowych, w typowy tekst/string, w skrypcie mamy dodatkową procedurę ułatwiającą to zadanie:
Array2Str(CATarray) { wyn := Chr(CATarray[0]) Loop % CATarray.MaxIndex() wyn .= Chr(CATarray[A_index]) Return wyn }
Powodzenia w zabawie!
IV. On-Screen-Display VFO – docelowy skrypt
Tak jak wspomniałem na wstępnie, chcemy pokazać na ekranie naszego komputera częstotliwość odczytywaną na bieżąco z radia, pobieraną bezpośrednio Omni RIG.
Klocki powiązane z OmniRIG już mamy.
Wyświetlanie w trybie OSD bazuje na rozwiązaniu z help’a AHK, link tu: https://www.autohotkey.com/docs/v1/lib/Gui.htm#OSD
Poniżej pełny skrypt, realizujący nasze zadanie:
; AHK 1.1x (v1.1.31+) ; tested on Win7 ; OmniRIG ver. 1.20 ; www.radiohotkey.pl ;=== Connect to OmniRIG ====================================================== Omni := ComObjCreate("OmniRig.OmniRigX") ; OmniRIG object ComObjConnect(Omni, "OR_") ; setting prefix of event handling procedures: OR_xxxxx RADIO1 := Omni.Rig1 ; setting variable RADIO1 as object representing TRX #1 from OmniRIG ;RADIO2 := Omni.Rig2 ; (omniRig-2nd_radio object, if you want :-) ) ;set initial partameters ================================================== Textcolor := 0x4499AA ; setting text color TextcolorTX := 0xCC2222 ; text color when TX! ;setting x,y OSD Window position ( adjust to your screen ) xpoz :=1000 ypoz := 100 ;=== create OSD Gui == see more: https://www.autohotkey.com/docs/v1/lib/Gui.htm#OSD CustomColor := 0x000000 ; expected background color for OSD (e.g. your desktop's color) Gui, 1: +AlwaysOnTop +LastFound +Owner Gui, 1: Color, %CustomColor% Gui, 1: Font, s80 Gui, 1: Add, Text, x100 y0 w700 Left c%Textcolor% BackgroundTrans vDATA1, xx Gui, 1: Font, s20 Gui, 1: Add, Text, x0 y30 w100 Left c%Textcolor% BackgroundTrans vDATA2, 0.000 Gui, 1: Font, s10 Gui, 1: Add, Text, x275 y110 w300 Right c%Textcolor% BackgroundTrans vDATA3, WinSet, TransColor, %CustomColor% 250 Gui, 1: -Caption Gui, 1: Show, NoActivate x%xpoz% y%ypoz% w700 ; get/set initial parameters OR_ParamsChange(1, 0x00000004) ; frq VFO-A OR_ParamsChange(1, RADIO1.Mode) ; radio Mode radio_type := RADIO1.RigType ; get name of #1 radio from OmniRig config.. GuiControl ,1:, DATA3, %radio_type% ; and Set GUI text Return ;=== OmniRIG events procedures ==================================== OR_ParamsChange(par1, par2) { ;Event "OmniRIG parameters change" (e.g. frq, mode, rit, xit, TX/RX etc.) ;par1 = radio number ( =1 or 2) ;par2 = event type (number / integer) global switch ;testing event type { Case (par2 & 0x00800000): mode_txt := "CW-U" Case (par2 & 0x01000000): mode_txt := "CW-L" Case (par2 & 0x02000000): mode_txt := "USB" Case (par2 & 0x04000000): mode_txt := "LSB" Case (par2 & 0x10000000): mode_txt := "RTTY-L" Case (par2 & 0x08000000): mode_txt := "RTTY-U" Case (par2 & 0x00200000): TX := 0 ; TX is On Case (par2 & 0x00400000): TX := 1 ; TX is Off Case (par2 & 0x00000004): { VFOA := RADIO1.FreqA VFO_txt := Format("{1:0.3f}", VFOA/1000) } ;Case etc... } ;Refresh GUI text.. GuiControl ,1:, DATA2, %mode_txt% GuiControl ,1:, DATA1, %VFO_txt% ;...and change frq color if TX! :-) IF (TX) GuiControl, 1: +c%TextcolorTX% +Redraw, DATA1 Else GuiControl, 1: +c%Textcolor% +Redraw, DATA1 Return } ;event handling after sent SendCustomCommand() ;see section "keyboard action samples" in the code below OR_CustomReply(par1, par2, par3) { ; par1 - radio number ; par2 - sent CAT command ; par3 - CAT resporse (as char-array) MsgBox % "Sent: " CATresponseStr(par2) " Received: " CATresponseStr(par3) } ;function: conversion char-array to string CATresponseStr(CATarray) { cat_res := Chr(CATarray[0]) Loop % CATarray.MaxIndex() cat_res .= Chr(CATarray[A_index]) Return cat_res } ;====================================================================================== ;=== keyboard action samples / how to send any CAT command to TRX via OmniRIG ;=== check your CAT command list in radio documentation ;alt-1 direct send CAT-command (here CAT-Yaesu) >> set 1st antenna for Main-Band VFO !1:: RADIO1.SendCustomCommand("AN01;",0,"") Return ;alt-2 ...or set 2nd antenna for Main-Band VFO !2:: RADIO1.SendCustomCommand("AN02;",0,"") Return ;alt-3 CAT question-command (Yaesu): AN0; which antenna Main-Band VFO?? !3:: RADIO1.SendCustomCommand("AN0;",5,";") ;The response from the radio we have in the ;event handler >> OR_CustomReply(par1, par2, par3) Return Esc:: ExitApp
Powodzenia w zabawie!