2024-07-01 23:55:56 -07:00
const std = @import ( " std " ) ;
2024-07-06 12:59:37 -07:00
const Config = @import ( " ./Config.zig " ) ;
const config = Config . global ;
2024-07-01 23:55:56 -07:00
const LabjackYaesu = @import ( " ./LabjackYaesu.zig " ) ;
const RotCtl = @This ( ) ;
const log = std . log . scoped ( . RotCtl ) ;
writer : std . io . BufferedWriter ( 512 , std . net . Stream . Writer ) ,
running : bool ,
rotator : LabjackYaesu ,
pub fn run ( allocator : std . mem . Allocator ) ! void {
// var server = std.net.StreamServer.init(.{ .reuse_address = true });
// defer server.deinit();
const listen_addr = try std . net . Address . parseIp (
config . rotctl . listen_address ,
config . rotctl . listen_port ,
) ;
var server = listen_addr . listen ( . { . reuse_address = true } ) catch {
log . err ( " Could not listen on {}. Is it already in use? " , . { listen_addr } ) ;
return ;
} ;
log . info ( " Listening for client on: {} " , . { listen_addr } ) ;
var interface : RotCtl = . {
. writer = undefined ,
. running = true ,
. rotator = try LabjackYaesu . init ( allocator ) ,
} ;
while ( true ) {
const client = try server . accept ( ) ;
defer {
log . info ( " disconnecting client " , . { } ) ;
2024-07-05 00:32:42 -07:00
interface . rotator . stop ( ) ;
2024-07-01 23:55:56 -07:00
client . stream . close ( ) ;
}
interface . writer = . { . unbuffered_writer = client . stream . writer ( ) } ;
interface . running = true ;
defer interface . running = false ;
log . info ( " client connected from {} " , . { client . address } ) ;
var readbuffer = [ _ ] u8 { 0 } * * 512 ;
var fbs = std . io . fixedBufferStream ( & readbuffer ) ;
const reader = client . stream . reader ( ) ;
while ( interface . running ) : ( fbs . reset ( ) ) {
reader . streamUntilDelimiter ( fbs . writer ( ) , '\n' , readbuffer . len ) catch break ;
2024-07-05 00:32:42 -07:00
// note: an error here kills this entire function, which may not be
// desirable. For example, if the client unexpectedly disconnects, we
// probably shouldn't kill the whole runloop.
interface . handleHamlibCommand (
2024-07-03 17:42:36 -07:00
std . mem . trim ( u8 , fbs . getWritten ( ) , & std . ascii . whitespace ) ,
2024-07-05 00:32:42 -07:00
) catch break ;
2024-07-01 23:55:56 -07:00
}
}
}
fn write ( self : * RotCtl , buf : [ ] const u8 ) ! void {
try self . writer . writer ( ) . writeAll ( buf ) ;
try self . writer . flush ( ) ;
}
fn replyStatus ( self : * RotCtl , comptime status : HamlibErrorCode ) ! void {
try self . write ( comptime status . replyFrame ( ) + + " \n " ) ;
}
2024-07-03 17:42:36 -07:00
fn printReply ( self : * RotCtl , comptime fmt : [ ] const u8 , args : anytype ) ! void {
try self . writer . writer ( ) . print ( fmt + + " \n " , args ) ;
try self . writer . flush ( ) ;
}
2024-07-05 00:32:42 -07:00
fn quit ( self : * RotCtl , _ : [ ] const u8 , tokens : * TokenIter ) CommandError ! void {
if ( tokens . next ( ) ! = null ) return error . BadInput ;
self . running = false ;
self . replyStatus ( . okay ) catch { } ;
self . rotator . quit ( ) ;
}
fn stop ( self : * RotCtl , _ : [ ] const u8 , tokens : * TokenIter ) CommandError ! void {
if ( tokens . next ( ) ! = null ) return error . BadInput ;
self . rotator . stop ( ) ;
self . replyStatus ( . okay ) catch return error . BadOutput ;
}
fn park ( self : * RotCtl , _ : [ ] const u8 , tokens : * TokenIter ) CommandError ! void {
if ( tokens . next ( ) ! = null ) return error . BadInput ;
self . rotator . startPark ( ) ;
self . replyStatus ( . okay ) catch return error . BadOutput ;
}
fn blindAck ( self : * RotCtl , _ : [ ] const u8 , _ : * TokenIter ) CommandError ! void {
self . replyStatus ( . okay ) catch return error . BadOutput ;
}
fn notSupported ( self : * RotCtl , _ : [ ] const u8 , _ : * TokenIter ) CommandError ! void {
self . replyStatus ( . not_supported ) catch return error . BadOutput ;
}
fn getPosition ( self : * RotCtl , _ : [ ] const u8 , tokens : * TokenIter ) CommandError ! void {
if ( tokens . next ( ) ! = null ) return error . BadInput ;
const pos = self . rotator . currentPosition ( ) ;
self . printReply ( " {d:.1} \n {d:.1} " , . { pos . azimuth , pos . elevation } ) catch return error . BadOutput ;
}
2024-07-06 12:59:37 -07:00
fn inRange ( request : f64 , comptime dof : enum { azimuth , elevation } ) bool {
return switch ( dof ) {
// zig fmt: off
. azimuth = > request > = (
config . labjack . feedback_calibration . azimuth . minimum . angle
+ config . controller . angle_offset . azimuth
) and request < = (
config . labjack . feedback_calibration . azimuth . maximum . angle
+ config . controller . angle_offset . azimuth
) ,
. elevation = > request > = (
config . labjack . feedback_calibration . elevation . minimum . angle
+ config . controller . angle_offset . elevation
) and request < = (
config . labjack . feedback_calibration . elevation . maximum . angle
+ config . controller . angle_offset . elevation
) ,
// zig fmt: on
} ;
}
2024-07-05 00:32:42 -07:00
fn setPosition ( self : * RotCtl , _ : [ ] const u8 , tokens : * TokenIter ) CommandError ! void {
const azimuth = std . fmt . parseFloat ( f64 , tokens . next ( ) orelse {
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
} ) catch {
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
} ;
2024-07-06 12:59:37 -07:00
if ( ! inRange ( azimuth , . azimuth ) )
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
2024-07-05 00:32:42 -07:00
const elevation = std . fmt . parseFloat ( f64 , tokens . next ( ) orelse {
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
} ) catch {
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
} ;
2024-07-06 12:59:37 -07:00
if ( ! inRange ( elevation , . elevation ) )
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
2024-07-05 00:32:42 -07:00
self . rotator . setTarget ( . { . azimuth = azimuth , . elevation = elevation } ) ;
return self . replyStatus ( . okay ) catch error . BadOutput ;
}
2024-07-01 23:55:56 -07:00
fn handleHamlibCommand (
self : * RotCtl ,
command : [ ] const u8 ,
) ! void {
2024-07-05 00:32:42 -07:00
if ( command . len = = 0 ) {
return self . replyStatus ( . invalid_parameter ) catch error . BadOutput ;
}
2024-07-01 23:55:56 -07:00
var tokens = std . mem . tokenizeScalar ( u8 , command , ' ' ) ;
const first = tokens . next ( ) . ? ;
if ( first . len = = 1 or first [ 0 ] = = '\\' ) {
switch ( first [ 0 ] ) {
2024-07-05 00:32:42 -07:00
// NOTE: this is not technically supported by rotctld.
'q' , 'Q' = > try self . quit ( first , & tokens ) ,
'S' = > try self . stop ( first , & tokens ) ,
'K' = > try self . park ( first , & tokens ) ,
'p' = > try self . getPosition ( first , & tokens ) ,
'P' = > try self . setPosition ( first , & tokens ) ,
'\\' = > try self . handleLongCommand ( first [ 1 . . ] , & tokens ) ,
2024-07-03 17:42:36 -07:00
else = > {
log . err ( " unknown short command '{s}' " , . { command } ) ;
2024-07-05 00:32:42 -07:00
self . replyStatus ( . not_supported ) catch return error . BadOutput ;
2024-07-01 23:55:56 -07:00
} ,
}
2024-07-03 17:42:36 -07:00
} else {
2024-07-05 00:32:42 -07:00
try self . handleLongCommand ( first , & tokens ) ;
2024-07-03 17:42:36 -07:00
}
2024-07-01 23:55:56 -07:00
}
2024-07-05 00:32:42 -07:00
fn handleLongCommand (
2024-07-01 23:55:56 -07:00
self : * RotCtl ,
command : [ ] const u8 ,
2024-07-05 00:32:42 -07:00
tokens : * TokenIter ,
2024-07-01 23:55:56 -07:00
) ! void {
2024-07-05 00:32:42 -07:00
inline for ( rotctl_commands ) | cmdef |
if ( comptime cmdef . long ) | long |
if ( std . mem . eql ( u8 , long , command ) )
return try cmdef . callback ( self , command , tokens ) ;
return self . replyStatus ( . not_supported ) catch error . BadOutput ;
2024-07-01 23:55:56 -07:00
}
const HamlibErrorCode = enum ( u8 ) {
okay = 0 ,
invalid_parameter = 1 ,
invalid_configuration = 2 ,
out_of_memory = 3 ,
not_implemented = 4 ,
timeout = 5 ,
io_error = 6 ,
internal_error = 7 ,
protocol_error = 8 ,
command_rejected = 9 ,
parameter_truncated = 10 ,
not_supported = 11 ,
not_targetable = 12 ,
bus_error = 13 ,
bus_busy = 14 ,
invalid_arg = 15 ,
invalid_vfo = 16 ,
domain_error = 17 ,
deprecated = 18 ,
security = 19 ,
power = 20 ,
fn replyFrame ( comptime self : HamlibErrorCode ) [ ] const u8 {
return std . fmt . comptimePrint (
" RPRT {d} " ,
. { - @as ( i8 , @intCast ( @intFromEnum ( self ) ) ) } ,
) ;
}
} ;
2024-07-05 00:32:42 -07:00
const CommandError = error { BadInput , BadOutput } ;
const TokenIter : type = std . mem . TokenIterator ( u8 , . scalar ) ;
const CommandCallback : type = * const fn ( self : * RotCtl , command : [ ] const u8 , tokens : * TokenIter ) CommandError ! void ;
2024-07-01 23:55:56 -07:00
const HamlibCommand = struct {
short : ? u8 = null ,
long : ? [ ] const u8 = null ,
2024-07-05 00:32:42 -07:00
callback : CommandCallback ,
2024-07-01 23:55:56 -07:00
} ;
const rotctl_commands = [ _ ] HamlibCommand {
2024-07-05 00:32:42 -07:00
. { . short = 'q' , . callback = quit } , // quit
. { . short = 'Q' , . callback = quit } , // quit
. { . long = " AOS " , . callback = blindAck } ,
. { . long = " LOS " , . callback = blindAck } ,
. { . short = 'P' , . long = " set_pos " , . callback = setPosition } , // azimuth: f64, elevation: f64
. { . short = 'p' , . long = " get_pos " , . callback = getPosition } , // return az: f64, el: f64
. { . short = 'M' , . long = " move " , . callback = notSupported } , // direction: enum { up=2, down=4, left=8, right=16 }, speed: i8 (0-100 or -1)
. { . short = 'S' , . long = " stop " , . callback = stop } ,
. { . short = 'K' , . long = " park " , . callback = park } ,
. { . short = 'C' , . long = " set_conf " , . callback = notSupported } , // token: []const u8, value: []const u8
. { . short = 'R' , . long = " reset " , . callback = notSupported } , // u1 (1 is reset all)
. { . short = '_' , . long = " get_info " , . callback = notSupported } , // return Model name
. { . long = " dump_state " , . callback = notSupported } , // ???
. { . short = '1' , . long = " dump_caps " , . callback = notSupported } , // ???
. { . short = 'w' , . long = " send_cmd " , . callback = notSupported } , // []const u8, send serial command directly to the rotator
. { . short = 'L' , . long = " lonlat2loc " , . callback = notSupported } , // return Maidenhead locator for given long: f64 and , .callback = notSupportedlat: f64, locator precision: u4 (2-12)
. { . short = 'l' , . long = " loc2lonlat " , . callback = notSupported } , // the inverse of the above
. { . short = 'D' , . long = " dms2dec " , . callback = notSupported } , // deg, min, sec, 0 (positive) or 1 (negative)
. { . short = 'd' , . long = " dec2dms " , . callback = notSupported } ,
. { . short = 'E' , . long = " dmmm2dec " , . callback = notSupported } ,
. { . short = 'e' , . long = " dec2dmmm " , . callback = notSupported } ,
. { . short = 'B' , . long = " grb " , . callback = notSupported } ,
. { . short = 'A' , . long = " a_sp2a_lp " , . callback = notSupported } ,
. { . short = 'a' , . long = " d_sp2d_lp " , . callback = notSupported } ,
. { . long = " pause " , . callback = notSupported } ,
2024-07-01 23:55:56 -07:00
} ;
// D, dms2dec 'Degrees' 'Minutes' 'Seconds' 'S/W'
// Returns 'Dec Degrees', a signed floating point value.
// 'Degrees' and 'Minutes' are integer values.
// 'Seconds' is a floating point value.
// 'S/W' is a flag with ’ 1’ indicating South latitude or West longitude and ’ 0’ North or East (the flag is needed as computers don’ t recognize a signed zero even though only the 'Degrees' value is typically signed in DMS notation).
// d, dec2dms 'Dec Degrees'
// Returns 'Degrees' 'Minutes' 'Seconds' 'S/W'.
// Values are as in dms2dec above.
// E, dmmm2dec 'Degrees' 'Dec Minutes' 'S/W'
// Returns 'Dec Degrees', a signed floating point value.
// 'Degrees' is an integer value.
// 'Dec Minutes' is a floating point value.
// 'S/W' is a flag as in dms2dec above.
// e, dec2dmmm 'Dec Deg'
// Returns 'Degrees' 'Minutes' 'S/W'.
// Values are as in dmmm2dec above.
// B, qrb 'Lon 1' 'Lat 1' 'Lon 2' 'Lat 2'
// Returns 'Distance' and 'Azimuth'.
// 'Distance' is in km.
// 'Azimuth' is in degrees.
// Supplied Lon/Lat values are signed floating point numbers.
// A, a_sp2a_lp 'Short Path Deg'
// Returns 'Long Path Deg'.
// Both the supplied argument and returned value are floating point values within the range of 0.00 to 360.00.
// Note: Supplying a negative value will return an error message.
// a, d_sp2d_lp 'Short Path km'
// Returns 'Long Path km'.
// Both the supplied argument and returned value are floating point values.
// pause 'Seconds'
// Pause for the given whole (integer) number of 'Seconds' before sending the next command to the rotator.