401 lines
13 KiB
Erlang
401 lines
13 KiB
Erlang
%%% @doc
|
|
%%% GajuMine GUI
|
|
%%% @end
|
|
|
|
-module(gmc_gui).
|
|
-vsn("0.2.0").
|
|
-author("Craig Everett <craigeverett@qpq.swiss>").
|
|
-copyright("Craig Everett <craigeverett@qpq.swiss>").
|
|
-license("GPL-3.0-or-later").
|
|
|
|
-behavior(wx_object).
|
|
-include_lib("wx/include/wx.hrl").
|
|
-export([ask_conf/0, set_account/1, difficulty/1, speed/1, candidate/1, message/1]).
|
|
-export([start_link/1]).
|
|
-export([init/1, terminate/2, code_change/3,
|
|
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
|
|
-include("$zx_include/zx_logger.hrl").
|
|
|
|
|
|
-record(w,
|
|
{name = none :: atom(),
|
|
id = 0 :: integer(),
|
|
wx = none :: none | wx:wx_object()}).
|
|
|
|
-record(s,
|
|
{wx = none :: none | wx:wx_object(),
|
|
frame = none :: none | wx:wx_object(),
|
|
lang = en :: en | jp,
|
|
j = none :: none | fun(),
|
|
id = none :: none | wx:wx_object(),
|
|
diff = none :: none | wx:wx_object(),
|
|
perf = none :: none | wx:wx_object(),
|
|
hist = {ts(), 0} :: {LastTS :: integer(), Average :: integer()},
|
|
candy = none :: none | wx:wx_object(),
|
|
height = none :: none | wx:wx_object(),
|
|
block = none :: none | wx:wx_object(),
|
|
% solved = 0 :: non_neg_integer(), % TODO: Add a widget to show this. Maybe
|
|
mess = none :: none | wx:wx_object(),
|
|
buff = new_buff() :: buff(),
|
|
buttons = [] :: [#w{}]}).
|
|
|
|
ts() ->
|
|
erlang:system_time(microsecond).
|
|
|
|
|
|
new_buff() ->
|
|
{9, 0, 10, 0, []}.
|
|
|
|
-type buff() :: {OMax :: non_neg_integer(),
|
|
OCur :: non_neg_integer(),
|
|
IMax :: non_neg_integer(),
|
|
ICur :: non_neg_integer(),
|
|
Buff :: [[string()]]}.
|
|
|
|
|
|
%%% Interface functions
|
|
|
|
ask_conf() ->
|
|
wx_object:cast(?MODULE, ask_conf).
|
|
|
|
|
|
set_account(ID) ->
|
|
wx_object:cast(?MODULE, {set_account, ID}).
|
|
|
|
|
|
difficulty(N) ->
|
|
wx_object:cast(?MODULE, {difficulty, N}).
|
|
|
|
|
|
speed(N) ->
|
|
wx_object:cast(?MODULE, {speed, N}).
|
|
|
|
|
|
candidate(Block) ->
|
|
wx_object:cast(?MODULE, {candidate, Block}).
|
|
|
|
|
|
message(Terms) ->
|
|
wx_object:cast(?MODULE, {message, Terms}).
|
|
|
|
|
|
|
|
%%% Startup Functions
|
|
|
|
start_link(Title) ->
|
|
wx_object:start_link({local, ?MODULE}, ?MODULE, Title, []).
|
|
|
|
|
|
init(Prefs) ->
|
|
ok = log(info, "GUI starting..."),
|
|
Lang = maps:get(lang, Prefs, en_US),
|
|
Trans = gd_jt:read_translations(?MODULE),
|
|
J = gd_jt:j(Lang, Trans),
|
|
|
|
WX = wx:new(),
|
|
Frame = wxFrame:new(WX, ?wxID_ANY, J("GajuMine")),
|
|
MainSz = wxBoxSizer:new(?wxVERTICAL),
|
|
LeftRight = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Left = wxBoxSizer:new(?wxVERTICAL),
|
|
Right = wxBoxSizer:new(?wxVERTICAL),
|
|
|
|
Labels = [J("ID"), J("Target"), J("Maps/s"), J("Candidate"), J("Height"), J("BlockHash")],
|
|
{Grid, [ID_C, DiffC, PerfC, CandyC, HeightC, BlockC]} = display_box(Frame, Labels),
|
|
|
|
Style = ?wxDEFAULT bor ?wxTE_MULTILINE bor ?wxTE_READONLY,
|
|
MessSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Messages")}]),
|
|
MessC = wxTextCtrl:new(Frame, ?wxID_ANY, [{style, Style}]),
|
|
_ = wxStaticBoxSizer:add(MessSz, MessC, zxw:flags(wide)),
|
|
|
|
ButtonTemplates =
|
|
[{start_stop, J("Start")},
|
|
{gajudesk, J("Open Wallet")},
|
|
{eureka, J("GajuMining")},
|
|
{explorer, J("ChainExplorer")},
|
|
{conf, J("Configure")}],
|
|
|
|
MakeButton =
|
|
fun({Name, Label}) ->
|
|
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]),
|
|
#w{name = Name, id = wxButton:getId(B), wx = B}
|
|
end,
|
|
|
|
Buttons = lists:map(MakeButton, ButtonTemplates),
|
|
|
|
_ = wxBoxSizer:add(Left, Grid, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Left, MessSz, zxw:flags(wide)),
|
|
Add = fun(#w{wx = Button}) -> wxBoxSizer:add(Right, Button, zxw:flags(wide)) end,
|
|
ok = lists:foreach(Add, Buttons),
|
|
_ = wxBoxSizer:add(LeftRight, Left, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(LeftRight, Right, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(MainSz, LeftRight, zxw:flags(wide)),
|
|
|
|
ok = wxFrame:setSizer(Frame, MainSz),
|
|
ok = wxSizer:layout(MainSz),
|
|
|
|
ok = wxFrame:connect(Frame, command_button_clicked),
|
|
ok = wxFrame:connect(Frame, close_window),
|
|
ok = wxFrame:setSize(Frame, {650, 300}),
|
|
ok = wxFrame:center(Frame),
|
|
true = wxFrame:show(Frame),
|
|
State =
|
|
#s{wx = WX, frame = Frame,
|
|
lang = Lang, j = J,
|
|
id = ID_C, diff = DiffC, perf = PerfC, candy = CandyC, height = HeightC, block = BlockC,
|
|
mess = MessC,
|
|
buttons = Buttons},
|
|
{Frame, State}.
|
|
|
|
display_box(Parent, Labels) ->
|
|
Grid = wxFlexGridSizer:new(2, 4, 4),
|
|
ok = wxFlexGridSizer:setFlexibleDirection(Grid, ?wxHORIZONTAL),
|
|
ok = wxFlexGridSizer:addGrowableCol(Grid, 1),
|
|
Make =
|
|
fun(S) ->
|
|
L = [S, ":"],
|
|
T_L = wxStaticText:new(Parent, ?wxID_ANY, L),
|
|
T_C = wxStaticText:new(Parent, ?wxID_ANY, "---", [{style, ?wxALIGN_LEFT}]),
|
|
_ = wxFlexGridSizer:add(Grid, T_L, zxw:flags(base)),
|
|
_ = wxFlexGridSizer:add(Grid, T_C, zxw:flags(wide)),
|
|
T_C
|
|
end,
|
|
Controls = lists:map(Make, Labels),
|
|
{Grid, Controls}.
|
|
|
|
|
|
handle_call(Unexpected, From, State) ->
|
|
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
handle_cast(ask_conf, State) ->
|
|
NewState = conf(State),
|
|
{noreply, NewState};
|
|
handle_cast({set_account, ID}, State) ->
|
|
ok = set_account(ID, State),
|
|
{noreply, State};
|
|
handle_cast({difficulty, N}, State) ->
|
|
ok = difficulty(N, State),
|
|
{noreply, State};
|
|
handle_cast({speed, N}, State) ->
|
|
ok = speed(N, State),
|
|
{noreply, State};
|
|
handle_cast({candidate, Block}, State) ->
|
|
ok = candidate(Block, State),
|
|
{noreply, State};
|
|
handle_cast({message, Terms}, State) ->
|
|
NewState = do_message(Terms, State),
|
|
{noreply, NewState};
|
|
handle_cast(Unexpected, State) ->
|
|
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
handle_info(Unexpected, State) ->
|
|
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
|
|
State = #s{buttons = Buttons}) ->
|
|
NewState =
|
|
case lists:keyfind(ID, #w.id, Buttons) of
|
|
#w{name = start_stop} -> start_stop(State);
|
|
#w{name = gajudesk} -> gajudesk(State);
|
|
#w{name = eureka} -> eureka(State);
|
|
#w{name = explorer} -> explorer(State);
|
|
#w{name = conf} -> conf(State);
|
|
false -> State
|
|
end,
|
|
{noreply, NewState};
|
|
handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame}) ->
|
|
ok = retire(Frame),
|
|
{noreply, State};
|
|
handle_event(Event, State) ->
|
|
ok = tell(info, "Unexpected event ~tp", [Event]),
|
|
{noreply, State}.
|
|
|
|
|
|
code_change(_, State, _) ->
|
|
{ok, State}.
|
|
|
|
|
|
terminate(Reason, State) ->
|
|
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
|
|
wx:destroy().
|
|
|
|
|
|
set_account(ID, #s{id = Widget}) ->
|
|
wxStaticText:setLabel(Widget, ID).
|
|
|
|
|
|
difficulty(N, #s{diff = Widget}) ->
|
|
wxStaticText:setLabel(Widget, integer_to_list(N)).
|
|
|
|
|
|
speed(N, #s{perf = Widget}) ->
|
|
wxStaticText:setLabel(Widget, float_to_list(N)).
|
|
|
|
|
|
candidate(Block, #s{candy = Widget}) ->
|
|
wxStaticText:setLabel(Widget, Block).
|
|
|
|
|
|
do_message({notice, Notice}, State) ->
|
|
Entry = io_lib:format("~n~ts", [Notice]),
|
|
ok = log(info, Entry),
|
|
do_message2(Entry, State);
|
|
do_message({puzzle, #{info := {_, Diff, Nonce, _}}}, State = #s{diff = DiffT}) ->
|
|
DiffS = integer_to_list(Diff),
|
|
ok = wxStaticText:setLabel(DiffT, DiffS),
|
|
Entry = io_lib:format("~nTrying Nonce: ~p", [Nonce]),
|
|
do_message2(Entry, State);
|
|
do_message({result, #{info := Info}}, State = #s{perf = PerfT, hist = {LastTS, AvgDiff}}) ->
|
|
Now = ts(),
|
|
NewAvgDiff = avg20(Now, LastTS, AvgDiff),
|
|
PerfS = io_lib:format("~w", [1_000_000 / NewAvgDiff]),
|
|
ok = wxStaticText:setLabel(PerfT, PerfS),
|
|
NewState = State#s{hist = {Now, NewAvgDiff}},
|
|
case Info of
|
|
{error, no_solution} ->
|
|
NewState;
|
|
{ok, Cycles} ->
|
|
Entry = io_lib:format("~nFound! Reporting ~w cycles to leader.", [length(Cycles)]),
|
|
do_message2(Entry, NewState);
|
|
Other ->
|
|
Entry = io_lib:format("~nUnexpected 'result': ~tp", [Other]),
|
|
do_message2(Entry, NewState)
|
|
end;
|
|
do_message({pool_notification, #{info := #{msg := MSG}}}, State = #s{height = HeightT, block = BlockT, candy = CandT}) ->
|
|
case MSG of
|
|
#{new_generation := #{height := Height, block_hash := BlockHash}} ->
|
|
ok = wxStaticText:setLabel(HeightT, integer_to_list(Height)),
|
|
ok = wxStaticText:setLabel(BlockT, BlockHash),
|
|
State;
|
|
#{candidate := #{candidate := Candidate}} ->
|
|
ok = wxStaticText:setLabel(CandT, Candidate),
|
|
State;
|
|
#{solution_accepted := #{seq := Seq}} ->
|
|
Entry = io_lib:format("~nSolution Accepted! You solved one! Sequence: ~w", [Seq]),
|
|
do_message2(Entry, State);
|
|
Other ->
|
|
Entry = io_lib:format("~nUnexpected 'pool_notification': ~tp", [Other]),
|
|
do_message2(Entry, State)
|
|
end;
|
|
do_message({connected, _}, State) ->
|
|
Entry = "\nConnected!",
|
|
do_message2(Entry, State);
|
|
do_message({disconnected, _}, State) ->
|
|
Entry = "\nDisconnected!",
|
|
do_message2(Entry, State);
|
|
do_message(Terms, State) ->
|
|
tell(info, "~p", [Terms]),
|
|
Entry = io_lib:format("~n~tp", [Terms]),
|
|
do_message2(Entry, State).
|
|
|
|
avg20(Now, LastTS, AvgDiff) ->
|
|
Diff = Now - LastTS,
|
|
((AvgDiff * 19) + Diff) div 20.
|
|
|
|
do_message2(Entry, State = #s{mess = Mess, buff = Buff}) ->
|
|
NewBuff =
|
|
case os:type() of
|
|
{unix, _} ->
|
|
add_message(Entry, Mess, Buff);
|
|
{win32, nt} ->
|
|
ok = wxTextCtrl:freeze(Mess),
|
|
Updated = add_message(Entry, Mess, Buff),
|
|
Last = wxTextCtrl:getLastPosition(Mess),
|
|
ok = wxTextCtrl:showPosition(Mess, Last),
|
|
ok = wxTextCtrl:thaw(Mess),
|
|
Updated
|
|
end,
|
|
State#s{buff = NewBuff}.
|
|
|
|
add_message(Entry, Mess, Buff) ->
|
|
case add_message2(Entry, Buff) of
|
|
{flush, Updated} ->
|
|
String = unicode:characters_to_list(lists:reverse(element(5, Updated))),
|
|
ok = wxTextCtrl:changeValue(Mess, String),
|
|
Last = wxTextCtrl:getLastPosition(Mess),
|
|
ok = wxTextCtrl:showPosition(Mess, Last),
|
|
ok = wxTextCtrl:thaw(Mess),
|
|
Updated;
|
|
{append, Updated} ->
|
|
ok = wxTextCtrl:appendText(Mess, Entry),
|
|
Updated
|
|
end.
|
|
|
|
add_message2(Entry, {OMax, OMax, IMax, IMax, [H | T]}) ->
|
|
{flush, {OMax, OMax, IMax, 1, [[Entry] , lists:reverse(H) | lists:droplast(T)]}};
|
|
add_message2(Entry, {OMax, OMax, IMax, ICur, [H | Buff]}) ->
|
|
{append, {OMax, OMax, IMax, ICur + 1, [[Entry | H] | Buff]}};
|
|
add_message2(Entry, {OMax, OCur, IMax, IMax, [H | T]}) ->
|
|
{append, {OMax, OCur + 1, IMax, 1, [[Entry], lists:reverse(H) | T]}};
|
|
add_message2(Entry, {OMax, OCur, IMax, ICur, [H | Buff]}) ->
|
|
{append, {OMax, OCur, IMax, ICur + 1, [[Entry | H] | Buff]}};
|
|
add_message2(Entry, {OMax, 0, IMax, 0, []}) ->
|
|
{append, {OMax, 1, IMax, 1, [[Entry]]}}.
|
|
|
|
|
|
start_stop(State = #s{buttons = Buttons}) ->
|
|
#w{wx = SSB} = lists:keyfind(start_stop, #w.name, Buttons),
|
|
_ = wxButton:disable(SSB),
|
|
ok = gmc_con:start_stop(),
|
|
State.
|
|
|
|
|
|
gajudesk(State) ->
|
|
ok = gmc_con:gajudesk(),
|
|
State.
|
|
|
|
|
|
eureka(State = #s{frame = Frame, j = J}) ->
|
|
ok = tell(info, "Opening Eureka"),
|
|
ok = open_browser(Frame, J, eureka_url()),
|
|
State.
|
|
|
|
eureka_url() ->
|
|
case gmc_con:network() of
|
|
<<"testnet">> -> "https://test.gajumining.com";
|
|
_ -> "https://gajumining.com"
|
|
end.
|
|
|
|
|
|
explorer(State = #s{frame = Frame, j = J, id = ID}) ->
|
|
ok = tell(info, "Opening Explorer"),
|
|
URL =
|
|
case wxStaticText:getLabel(ID) of
|
|
"" -> explorer_url();
|
|
AccountID -> unicode:characters_to_list([explorer_url(), "/account/", AccountID])
|
|
end,
|
|
ok = open_browser(Frame, J, URL),
|
|
State.
|
|
|
|
explorer_url() ->
|
|
case gmc_con:network() of
|
|
none -> "https://groot.mainnet.gajumaru.io";
|
|
Network -> unicode:characters_to_list(["https://groot.", Network, ".gajumaru.io"])
|
|
end.
|
|
|
|
|
|
conf(State) ->
|
|
ok = gmc_con:conf(),
|
|
State.
|
|
|
|
|
|
open_browser(Frame, J, URL) ->
|
|
case wx_misc:launchDefaultBrowser(URL) of
|
|
true ->
|
|
ok;
|
|
false ->
|
|
Format = J("Trouble launching browser.\nOpen URL here: ~s"),
|
|
Message = io_lib:format(Format, [URL]),
|
|
zxw:show_message(Frame, Message)
|
|
end.
|
|
|
|
|
|
retire(Frame) ->
|
|
ok = gmc_con:stop(),
|
|
wxWindow:destroy(Frame).
|