From 779fec544218782801f62a08d1d73b4f74e93692 Mon Sep 17 00:00:00 2001 From: Philipp Date: Sat, 27 Jul 2024 17:36:08 +0200 Subject: [PATCH] Refactoring. --- Makefile | 6 +- app/dmapi.f90 | 8 +- app/dmbackup.f90 | 40 ++-- app/dmexport.f90 | 6 +- app/dmfs.f90 | 19 +- app/dmgrc.f90 | 1 + app/dmimport.f90 | 18 +- app/dmmbctl.f90 | 46 +++-- app/dmpipe.f90 | 4 +- app/dmrecv.f90 | 25 ++- app/dmreport.f90 | 47 ++--- app/dmserial.f90 | 4 +- config/dmfeed.conf.sample | 17 +- config/dmfs.conf.sample | 7 +- config/dmgrc.conf.sample | 25 +-- config/dmlogger.conf.sample | 4 +- config/dmpipe.conf.sample | 10 +- config/dmplot.conf.sample | 19 +- config/dmreport.conf.sample | 8 +- config/dmsend.conf.sample | 2 +- config/dmsync.conf.sample | 2 +- guide/guide.adoc | 165 +++++++-------- src/dm_beat.f90 | 4 +- src/dm_db.f90 | 389 +++++++++++++----------------------- src/dm_error.f90 | 11 +- src/dm_fcgi.f90 | 28 ++- src/dm_geocom.f90 | 11 +- src/dm_hash_table.f90 | 26 ++- src/dm_inet.f90 | 10 - src/dm_log.f90 | 66 ++++-- src/dm_logger.f90 | 18 +- src/dm_lua_api.f90 | 5 +- src/dm_net.f90 | 10 + src/dm_string.f90 | 37 +++- src/dm_tty.f90 | 4 +- src/dm_util.f90 | 34 +++- src/dm_uuid.f90 | 2 +- src/dmpack.f90 | 4 +- test/dmtestdb.f90 | 21 +- test/dmtestlog.f90 | 10 +- test/dmtestlogger.f90 | 2 + 41 files changed, 598 insertions(+), 577 deletions(-) delete mode 100644 src/dm_inet.f90 create mode 100644 src/dm_net.f90 diff --git a/Makefile b/Makefile index 74687e8..a407a2a 100644 --- a/Makefile +++ b/Makefile @@ -248,7 +248,7 @@ SRC = $(SRCDIR)/dm_ansi.f90 \ $(SRCDIR)/dm_html.f90 \ $(SRCDIR)/dm_http.f90 \ $(SRCDIR)/dm_id.f90 \ - $(SRCDIR)/dm_inet.f90 \ + $(SRCDIR)/dm_net.f90 \ $(SRCDIR)/dm_job.f90 \ $(SRCDIR)/dm_json.f90 \ $(SRCDIR)/dm_jsonl.f90 \ @@ -338,7 +338,7 @@ OBJ = dm_ansi.o \ dm_html.o \ dm_http.o \ dm_id.o \ - dm_inet.o \ + dm_net.o \ dm_job.o \ dm_json.o \ dm_jsonl.o \ @@ -552,7 +552,7 @@ $(OBJ): $(SRC) $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_hash_table.f90 $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_unit.f90 $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_id.f90 - $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_inet.f90 + $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_net.f90 $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_uuid.f90 $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_signal.f90 $(FC) $(FFLAGS) $(LDFLAGS) -c src/dm_system.f90 diff --git a/app/dmapi.f90 b/app/dmapi.f90 index afcb923..ad7f50d 100644 --- a/app/dmapi.f90 +++ b/app/dmapi.f90 @@ -86,13 +86,11 @@ program dmapi if (dm_is_error(rc)) call dm_stop(STOP_FAILURE) ! Run event loop. - do while (dm_fcgi_accept() == E_NONE) + do while (dm_fcgi_accept()) call dm_cgi_env(env) call dm_cgi_router_dispatch(router, env, code) - - if (code /= HTTP_OK) then - call api_error(code, dm_error_message(E_NOT_FOUND), E_NOT_FOUND) - end if + if (code == HTTP_OK) cycle + call api_error(code, dm_error_message(E_NOT_FOUND), E_NOT_FOUND) end do ! Clean up. diff --git a/app/dmbackup.f90 b/app/dmbackup.f90 index a6a5a09..cdbc0b3 100644 --- a/app/dmbackup.f90 +++ b/app/dmbackup.f90 @@ -10,7 +10,7 @@ program dmbackup character(len=*), parameter :: APP_NAME = 'dmbackup' integer, parameter :: APP_MAJOR = 0 integer, parameter :: APP_MINOR = 9 - integer, parameter :: APP_PATCH = 1 + integer, parameter :: APP_PATCH = 2 integer, parameter :: APP_NSTEPS = 500 !! Step size for backup API. integer, parameter :: APP_SLEEP_TIME = 25 !! Sleep time between steps in msec. @@ -19,7 +19,7 @@ program dmbackup !! Command-line arguments. character(len=FILE_PATH_LEN) :: database = ' ' !! Path to database. character(len=FILE_PATH_LEN) :: backup = ' ' !! Path to backup. - logical :: vacuum = .false. !! VACUUM flag. + logical :: vacuum = .false. !! Vacuum flag. logical :: wal = .false. !! WAL flag. logical :: verbose = .false. !! Verbose flag. end type app_type @@ -41,11 +41,16 @@ program dmbackup integer function backup(app) result(rc) !! Creates database backup. type(app_type), intent(inout) :: app - type(db_type) :: db + + type(db_type) :: db ! Open database. rc = dm_db_open(db, app%database) - if (dm_is_error(rc)) return + + if (dm_is_error(rc)) then + call dm_error_out(rc, 'failed to open database') + return + end if backup_block: block ! Use VACUUM INTO. @@ -57,29 +62,25 @@ integer function backup(app) result(rc) ! Use SQLite backup API. if (app%verbose) then ! Using callback. - rc = dm_db_backup(db = db, & - path = app%backup, & - wal = app%wal, & - callback = backup_handler, & - nsteps = APP_NSTEPS, & - sleep_time = APP_SLEEP_TIME) + rc = dm_db_backup(db=db, path=app%backup, wal=app%wal, callback=backup_handler, & + nsteps=APP_NSTEPS, sleep_time=APP_SLEEP_TIME) + print * else ! No callback. - rc = dm_db_backup(db = db, & - path = app%backup, & - wal = app%wal, & - nsteps = APP_NSTEPS, & - sleep_time = APP_SLEEP_TIME) + rc = dm_db_backup(db=db, path=app%backup, wal=app%wal, nsteps=APP_NSTEPS, & + sleep_time=APP_SLEEP_TIME) end if end block backup_block - if (dm_is_error(dm_db_close(db))) rc = E_DB + call dm_error_out(rc, 'backup failed') + rc = dm_db_close(db) end function backup integer function read_args(app) result(rc) !! Reads command-line arguments. type(app_type), intent(out) :: app - type(arg_type) :: args(5) + + type(arg_type) :: args(5) rc = E_NONE @@ -118,9 +119,12 @@ end function read_args subroutine backup_handler(remaining, page_count) !! Prints progess to standard output of SQLite backup API is selected. + !! The cursor is reset to the first column of the line on each + !! invokation. integer, intent(in) :: remaining !! Pages remaining. integer, intent(in) :: page_count !! Total count of pages. - print '("Progress: ", f5.1, " %")', 100.0 * (page_count - remaining) / page_count + write (*, '(a1, "[0GProgress: ", f5.1, " %")', advance='no') & + ASCII_ESC, 100.0 * (page_count - remaining) / page_count end subroutine backup_handler end program dmbackup diff --git a/app/dmexport.f90 b/app/dmexport.f90 index 4b41cf0..6852ff5 100644 --- a/app/dmexport.f90 +++ b/app/dmexport.f90 @@ -105,11 +105,7 @@ integer function export(app) result(rc) ! Open file. if (is_file) then rc = E_IO - open (action = 'write', & - file = trim(app%output), & - iostat = stat, & - newunit = unit, & - status = 'replace') + open (action='write', file=trim(app%output), iostat=stat, newunit=unit, status='replace') if (stat /= 0) return end if diff --git a/app/dmfs.f90 b/app/dmfs.f90 index 2eb695f..03ada2d 100644 --- a/app/dmfs.f90 +++ b/app/dmfs.f90 @@ -179,8 +179,11 @@ integer function read_args(app) result(rc) return end select - app%output_type = OUTPUT_FILE - if (trim(app%output) == '-') app%output_type = OUTPUT_STDOUT + if (trim(app%output) == '-') then + app%output_type = OUTPUT_STDOUT + else + app%output_type = OUTPUT_FILE + end if end if rc = E_EMPTY @@ -231,7 +234,7 @@ integer function read_observ(observ, node_id, sensor_id, source, debug) result(r type(response_type), pointer :: response ! Single response in request. integer :: delay - integer :: fu, stat + integer :: stat, unit integer :: i, j, n logical :: debug_ @@ -278,7 +281,7 @@ integer function read_observ(observ, node_id, sensor_id, source, debug) result(r end if ! Try to open file for reading. - open (action='read', file=trim(request%request), iostat=stat, newunit=fu) + open (action='read', file=trim(request%request), iostat=stat, newunit=unit) if (stat == 0) request%error = E_NONE if (dm_is_error(request%error)) then @@ -289,7 +292,7 @@ integer function read_observ(observ, node_id, sensor_id, source, debug) result(r ! Read until the request pattern matches or end is reached. read_loop: do rc = E_EOF - read (fu, '(a)', iostat=stat) raw + read (unit, '(a)', iostat=stat) raw if (is_iostat_end(stat)) exit read_loop if (stat /= 0) cycle read_loop @@ -328,7 +331,7 @@ integer function read_observ(observ, node_id, sensor_id, source, debug) result(r end do read_loop ! Close file. - close (fu) + close (unit) request%error = rc @@ -352,7 +355,7 @@ integer function read_observ(observ, node_id, sensor_id, source, debug) result(r call logger%debug('next observ in ' // dm_itoa(delay / 1000) // ' sec', observ=observ) end if - call dm_usleep(delay * 1000) ! [msec] to [us]. + call dm_msleep(delay) end do req_loop end function read_observ @@ -454,7 +457,7 @@ subroutine run(app) delay = max(0, job%delay) if (delay <= 0) cycle job_loop if (debug) call logger%debug('next job in ' // dm_itoa(delay / 1000) // ' sec', observ=observ) - call dm_usleep(delay * 1000) + call dm_msleep(delay) end do job_loop end subroutine run diff --git a/app/dmgrc.f90 b/app/dmgrc.f90 index 4b2b5e2..49bdc18 100644 --- a/app/dmgrc.f90 +++ b/app/dmgrc.f90 @@ -176,6 +176,7 @@ integer function read_config(app) result(rc) rc = dm_config_get(config, 'warning', app%levels(LL_WARNING )%codes) rc = dm_config_get(config, 'error', app%levels(LL_ERROR )%codes) rc = dm_config_get(config, 'critical', app%levels(LL_CRITICAL)%codes) + rc = dm_config_get(config, 'user', app%levels(LL_USER) %codes) call dm_config_remove(config) end if diff --git a/app/dmimport.f90 b/app/dmimport.f90 index c2f413f..d07110d 100644 --- a/app/dmimport.f90 +++ b/app/dmimport.f90 @@ -43,7 +43,7 @@ integer function import(app) result(rc) type(app_type), intent(inout) :: app - integer :: er, stat, unit + integer :: error, stat, unit integer(kind=i8) :: nrecs, nrows logical :: exists, valid real(kind=r8) :: dt @@ -155,13 +155,14 @@ integer function import(app) result(rc) exit read_loop end if + ! Any other error. if (dm_is_error(rc)) then call dm_error_out(rc, 'failed to read record in row ' // dm_itoa(nrows)) exit read_loop end if + ! Validate record but skip database insert on dry run. if (app%dry) then - ! Validate record. select case (app%type) case (TYPE_NODE) valid = dm_node_valid(node) @@ -181,7 +182,6 @@ integer function import(app) result(rc) exit read_loop end if - ! Skip database insert on dry run. cycle read_loop end if @@ -222,10 +222,10 @@ integer function import(app) result(rc) if (.not. app%dry) then ! Rollback transaction on error. if (dm_is_error(rc)) then - er = dm_db_rollback(db) + error = dm_db_rollback(db) - if (dm_is_error(er)) then - call dm_error_out(er, 'failed to roll back database transaction') + if (dm_is_error(error)) then + call dm_error_out(error, 'failed to roll back database transaction') exit import_block end if @@ -239,10 +239,10 @@ integer function import(app) result(rc) ! Rollback transaction on error. if (dm_is_error(rc)) then call dm_error_out(rc, 'failed to commit database transaction') - er = dm_db_rollback(db) + error = dm_db_rollback(db) - if (dm_is_error(er)) then - call dm_error_out(er, 'failed to roll back database transaction') + if (dm_is_error(error)) then + call dm_error_out(error, 'failed to roll back database transaction') exit import_block end if diff --git a/app/dmmbctl.f90 b/app/dmmbctl.f90 index b5cd3b9..6513517 100644 --- a/app/dmmbctl.f90 +++ b/app/dmmbctl.f90 @@ -17,23 +17,21 @@ program dmmbctl type :: rtu_type !! Modbus RTU settings. - character(len=FILE_PATH_LEN) :: path = ' ' !! Path (required). - integer :: baud_rate = TTY_B19200 !! Baud rate (required). - integer :: byte_size = TTY_BYTE_SIZE8 !! Byte size (required). - integer :: parity = TTY_PARITY_EVEN !! Parity name (required). - integer :: stop_bits = TTY_STOP_BITS1 !! Stop bits (required). + character(len=FILE_PATH_LEN) :: path = ' ' !! Path (required). + integer :: baud_rate = TTY_B19200 !! Baud rate (required). + integer :: byte_size = TTY_BYTE_SIZE8 !! Byte size (required). + integer :: parity = TTY_PARITY_EVEN !! Parity name (required). + integer :: stop_bits = TTY_STOP_BITS1 !! Stop bits (required). end type rtu_type type :: tcp_type !! Modbus TCP settings. - character(len=INET_IPV4_LEN) :: address = ' ' !! IPv4 address. - integer :: port = 0 !! Port. + character(len=NET_IPV4_LEN) :: address = ' ' !! IPv4 address. + integer :: port = 0 !! Port. end type tcp_type type :: app_type !! Application settings. - type(rtu_type) :: rtu !! Modbus RTU settings. - type(tcp_type) :: tcp !! Modbus TCP settings. integer :: action = ACTION_READ !! Modbus read or write operation. integer :: address = 0 !! Modbus address. integer :: mode = MODBUS_MODE_NONE !! Modbus mode (RTU, TCP). @@ -42,6 +40,8 @@ program dmmbctl integer :: slave = 1 !! Modbus slave id. logical :: float = .false. !! Read or write float value. logical :: verbose = .false. !! Print debug messages to stderr. + type(rtu_type) :: rtu !! Modbus RTU settings. + type(tcp_type) :: tcp !! Modbus TCP settings. end type app_type integer :: rc ! Return code. @@ -64,7 +64,7 @@ integer function read_args(app) result(rc) integer :: stat integer :: read_address, write_address logical :: has_baud_rate, has_byte_size, has_path, has_parity, has_stop_bits - logical :: has_address, has_port + logical :: has_address, has_port, has_registers logical :: has_read, has_write type(arg_type) :: args(13) @@ -76,7 +76,7 @@ integer function read_args(app) result(rc) arg_type('bytesize', short='Z', type=ARG_TYPE_INTEGER), & ! -Z, --bytesize arg_type('parity', short='P', type=ARG_TYPE_STRING), & ! -P, --parity arg_type('stopbits', short='O', type=ARG_TYPE_INTEGER), & ! -O, --stopbits - arg_type('address', short='a', type=ARG_TYPE_STRING, min_len=7, max_len=INET_IPV4_LEN), & ! -a, --address + arg_type('address', short='a', type=ARG_TYPE_STRING, min_len=7, max_len=NET_IPV4_LEN), & ! -a, --address arg_type('port', short='q', type=ARG_TYPE_INTEGER), & ! -q, --port arg_type('slave', short='s', type=ARG_TYPE_INTEGER, required=.true.), & ! -s, --slave arg_type('registers', short='n', type=ARG_TYPE_INTEGER), & ! -n, --registers @@ -98,29 +98,31 @@ integer function read_args(app) result(rc) rc = dm_arg_get(args( 6), app%tcp%address, passed=has_address) rc = dm_arg_get(args( 7), app%tcp%port, passed=has_port) rc = dm_arg_get(args( 8), app%slave) - rc = dm_arg_get(args( 9), app%registers) + rc = dm_arg_get(args( 9), app%registers, passed=has_registers) rc = dm_arg_get(args(10), read_address, passed=has_read) rc = dm_arg_get(args(11), write_address, passed=has_write) rc = dm_arg_get(args(12), byte_order, passed=app%float) rc = dm_arg_get(args(13), app%verbose) ! Modbus RTU or TCP mode. - if (has_path) then - app%mode = MODBUS_MODE_RTU - else if (has_address) then - app%mode = MODBUS_MODE_TCP - end if - rc = E_INVALID if (.not. has_path .and. .not. has_address) then call dm_error_out(rc, 'argument --path or --address required') return - else if (has_path .and. has_address) then + end if + + if (has_path .and. has_address) then call dm_error_out(rc, 'argument --path conflicts with --address') return end if + if (has_path) then + app%mode = MODBUS_MODE_RTU + else if (has_address) then + app%mode = MODBUS_MODE_TCP + end if + select case (app%mode) case (MODBUS_MODE_RTU) ! Required arguments. @@ -225,13 +227,13 @@ integer function read_args(app) result(rc) ! Slave id. if (app%slave < 1) then - call dm_error_out(rc, 'argument --slave must be >= 1') + call dm_error_out(rc, 'argument --slave must be > 0') return end if ! Number of registers to read or write. if (app%registers < 1) then - call dm_error_out(rc, 'argument --registers must be >= 1') + call dm_error_out(rc, 'argument --registers must be > 0') return end if @@ -272,7 +274,7 @@ integer function read_args(app) result(rc) return end if - if (app%registers > 2) then + if (has_registers .and. app%registers /= 2) then call dm_error_out(rc, 'argument --registers must be 2') return end if diff --git a/app/dmpipe.f90 b/app/dmpipe.f90 index f6f7a65..b026ba2 100644 --- a/app/dmpipe.f90 +++ b/app/dmpipe.f90 @@ -344,7 +344,7 @@ integer function read_observ(pipe, observ, node_id, sensor_id, source, debug) re call logger%debug('next observ in ' // dm_itoa(delay / 1000) // ' sec', observ=observ) end if - call dm_usleep(delay * 1000) ! [msec] to [us]. + call dm_msleep(delay) end do req_loop end function read_observ @@ -441,7 +441,7 @@ subroutine run(app) delay = max(0, job%delay) if (delay <= 0) cycle job_loop if (debug) call logger%debug('next job in ' // dm_itoa(delay / 1000) // ' sec', observ=observ) - call dm_usleep(delay * 1000) + call dm_msleep(delay) end do job_loop end subroutine run diff --git a/app/dmrecv.f90 b/app/dmrecv.f90 index de934ee..c3fcc18 100644 --- a/app/dmrecv.f90 +++ b/app/dmrecv.f90 @@ -3,20 +3,21 @@ ! Author: Philipp Engel ! Licence: ISC program dmrecv - !! Receives logs or observations from POSIX message queue and writes them to - !! standard output or file, either in CSV, JSON Lines, or Namelist format. + !! Receives logs or observations from POSIX message queue and writes them + !! to standard output or file, either in CSV, JSON Lines, or Namelist + !! format. !! - !! The output can be piped to a graph tool like trend(1), to show a real-time - !! plot: + !! The output can be piped to a graph tool like _trend(1)_, to show a + !! real-time plot: !! !! ``` - !! $ dmrecv --name dmrecv --type observ --format block --response tz0 | \ - !! awk '{ print $2 | "trend - 60" }' + !! $ dmrecv --name dmrecv --type observ --format block --response tz0 \ + !! | awk '{ print $2 | "trend - 60" }' !! ``` !! !! Another DMPACK process has to send observations to message queue - !! "/dmrecv". Only responses of name "tz0" will be converted to ASCII block - !! format and printed to standard output. + !! "/dmrecv". Only responses of name `tz0` will be converted to ASCII + !! block format and printed to standard output. use :: dmpack implicit none (type, external) @@ -90,7 +91,8 @@ program dmrecv integer function read_args(app) result(rc) !! Reads command-line arguments and settings from configuration file. type(app_type), intent(out) :: app - type(arg_type) :: args(12) + + type(arg_type) :: args(12) rc = E_NONE @@ -185,7 +187,7 @@ integer function read_args(app) result(rc) end if if (.not. dm_id_valid(app%response)) then - call dm_error_out(rc, 'missing response name') + call dm_error_out(rc, 'invalid or missing response name') return end if end if @@ -196,7 +198,8 @@ end function read_args integer function read_config(app) result(rc) !! Reads configuration from (Lua) file if path is not emty. type(app_type), intent(inout) :: app !! App type. - type(config_type) :: config + + type(config_type) :: config rc = E_NONE if (len_trim(app%config) == 0) return diff --git a/app/dmreport.f90 b/app/dmreport.f90 index 531ae99..86049b0 100644 --- a/app/dmreport.f90 +++ b/app/dmreport.f90 @@ -368,7 +368,8 @@ subroutine create_report(report, error) integer, intent(out), optional :: error !! Error code. character(len=:), allocatable :: path, style - integer :: format, fu, i, n, rc, stat + integer :: i, n, rc + integer :: format, stat, unit logical :: is_file type(dp_type), allocatable :: data_points(:) @@ -376,8 +377,8 @@ subroutine create_report(report, error) type(node_type) :: node ! By default, print generated HTML to standard output. - fu = stdout - is_file = (len_trim(report%output) > 0) + unit = stdout + is_file = (len_trim(report%output) > 0 .and. report%output /= '-') report_block: block ! Open output file for writing. @@ -385,7 +386,7 @@ subroutine create_report(report, error) rc = E_WRITE path = dm_path_parsed(report%output) - open (action='write', file=path, iostat=stat, newunit=fu, status='replace') + open (action='write', file=path, iostat=stat, newunit=unit, status='replace') if (stat /= 0) then call dm_error_out(rc, 'failed to open output file ' // path) @@ -405,33 +406,33 @@ subroutine create_report(report, error) ! Add HTML header with optional inline CSS. if (len_trim(style) > 0) then - write (fu, '(a)') dm_html_header(report%title, report%subtitle, internal_style=style) + write (unit, '(a)') dm_html_header(report%title, report%subtitle, internal_style=style) else - write (fu, '(a)') dm_html_header(report%title, report%subtitle) + write (unit, '(a)') dm_html_header(report%title, report%subtitle) end if ! Add report overview table. rc = read_node(node, report%node, report%plot%database) if (dm_is_error(rc)) then - write (fu, '(a)') dm_html_error(rc) + write (unit, '(a)') dm_html_error(rc) else - write (fu, '(a)') html_report_table(node, report%from, report%to) + write (unit, '(a)') html_report_table(node, report%from, report%to) end if ! Add optional report description. if (len_trim(report%meta) > 0) then - write (fu, '(a)') dm_html_p(dm_html_encode(report%meta)) + write (unit, '(a)') dm_html_p(dm_html_encode(report%meta)) end if ! Add plots to HTML document if enabled. plot_if: if (.not. report%plot%disabled) then ! Add plot section heading. - write (fu, '(a)') dm_html_heading(2, report%plot%title) + write (unit, '(a)') dm_html_heading(2, report%plot%title) ! Add meta description. if (len_trim(report%plot%meta) > 0) then - write (fu, '(a)') dm_html_p(dm_html_encode(report%plot%meta)) + write (unit, '(a)') dm_html_p(dm_html_encode(report%plot%meta)) end if if (.not. allocated(report%plot%observs)) exit plot_if @@ -440,7 +441,7 @@ subroutine create_report(report, error) ! Plot loop. do i = 1, n ! Add plot heading. - write (fu, '(a)') dm_html_heading(3, report%plot%observs(i)%title, & + write (unit, '(a)') dm_html_heading(3, report%plot%observs(i)%title, & report%plot%observs(i)%subtitle) plot_block: block @@ -456,12 +457,12 @@ subroutine create_report(report, error) ! Handle errors. if (rc == E_DB_NO_ROWS) then - write (fu, '(a)') dm_html_p('No observations found in database.') + write (unit, '(a)') dm_html_p('No observations found in database.') exit plot_block end if if (dm_is_error(rc)) then - write (fu, '(a)') dm_html_error(rc) + write (unit, '(a)') dm_html_error(rc) exit plot_block end if @@ -471,12 +472,12 @@ subroutine create_report(report, error) if (format /= PLOT_TERM_GIF .and. format /= PLOT_TERM_PNG .and. & format /= PLOT_TERM_PNG_CAIRO .and. format /= PLOT_TERM_SVG) then ! Fail safe: should never occur. - write (fu, '(a)') dm_html_error(E_INVALID, 'invalid plot format') + write (unit, '(a)') dm_html_error(E_INVALID, 'invalid plot format') exit plot_block end if ! Add HTML plot figure. - write (fu, '(a)') html_plot(data_points, & + write (unit, '(a)') html_plot(data_points, & response = report%plot%observs(i)%response, & unit = report%plot%observs(i)%unit, & format = format, & @@ -492,11 +493,11 @@ subroutine create_report(report, error) ! Add table of logs to HTML document if enabled. log_if: if (.not. report%log%disabled) then ! Add section heading. - write (fu, '(a)') dm_html_heading(2, report%log%title) + write (unit, '(a)') dm_html_heading(2, report%log%title) ! Add meta description. if (len_trim(report%log%meta) > 0) then - write (fu, '(a)') dm_html_p(dm_html_encode(report%log%meta)) + write (unit, '(a)') dm_html_p(dm_html_encode(report%log%meta)) end if ! Read logs from database. @@ -510,26 +511,26 @@ subroutine create_report(report, error) ! Handle errors. if (rc == E_DB_NO_ROWS) then - write (fu, '(a)') dm_html_p('No logs found in database.') + write (unit, '(a)') dm_html_p('No logs found in database.') exit log_if end if if (dm_is_error(rc)) then - write (fu, '(a)') dm_html_error(rc) + write (unit, '(a)') dm_html_error(rc) exit log_if end if ! Add logs table. - write (fu, '(a)') dm_html_logs(logs, node=.false.) + write (unit, '(a)') dm_html_logs(logs, node=.false.) end if log_if ! Add HTML footer. - write (fu, '(a)') html_footer() + write (unit, '(a)') html_footer() rc = E_NONE end block report_block - if (is_file) close (fu) + if (is_file) close (unit) if (present(error)) error = rc end subroutine create_report end program dmreport diff --git a/app/dmserial.f90 b/app/dmserial.f90 index f420f0c..39dd4b2 100644 --- a/app/dmserial.f90 +++ b/app/dmserial.f90 @@ -469,7 +469,7 @@ integer function read_observ(tty, observ, node_id, sensor_id, source, debug) res call logger%debug('next observ in ' // dm_itoa(delay / 1000) // ' sec', observ=observ) end if - call dm_usleep(delay * 1000) ! [msec] to [us]. + call dm_msleep(delay) end do req_loop end function read_observ @@ -567,7 +567,7 @@ integer function run(app, tty) result(rc) delay = max(0, job%delay) if (delay == 0) cycle job_loop if (debug) call logger%debug('next job in ' // dm_itoa(delay / 1000) // ' sec', observ=observ) - call dm_usleep(delay * 1000) ! [msec] to [us]. + call dm_msleep(delay) end do job_loop if (dm_tty_connected(tty)) then diff --git a/config/dmfeed.conf.sample b/config/dmfeed.conf.sample index b846099..340e4a7 100644 --- a/config/dmfeed.conf.sample +++ b/config/dmfeed.conf.sample @@ -1,18 +1,25 @@ -- -- Example configuration file for dmfeed. -- --- Each Atom feed should have a distinct id. Run `dmuuid --hyphens` to --- generate a valid feed id. +-- Each Atom feed should have a distinct id. Run dmuuid(1) to generate a valid +-- feed id: +-- +-- $ dmuuid --hyphens +-- 30667c40-6ec4-4c98-9e03-15da86631cbc -- -- The options `minlevel` and `maxlevel` may be set to one of the following -- named constants: -- --- LL_NONE - 0 -- LL_DEBUG - 1 -- LL_INFO - 2 -- LL_WARNING - 3 -- LL_ERROR - 4 -- LL_CRITICAL - 5 +-- LL_USER - 6 +-- +-- Option `xsl` sets the local path of the XSLT style sheet on the web server. +-- If the value is set to `feed.xsl`, the style sheet file must be located in +-- the same directory as the Atom feed `feed.xml`. -- -- Rename table "dmfeed" to the instance name (parameter `--name`). -- @@ -24,8 +31,8 @@ -- entries - Maximum number of entries is feed (default 50, optional). -- force - Force writing of output file even if no new logs are available. -- id - 36 characters long UUID (with hyphens) of the Atom feed. --- maxlevel - Maximum log level (0 to 5, optional). --- minlevel - Minimum log level (0 to 5, optional). +-- maxlevel - Maximum log level (1 to 6, optional). +-- minlevel - Minimum log level (1 to 6, optional). -- node - Sensor node id, selects only related logs (optional). -- output - Path to output file (if empty, feed is printed to standard output). -- subtitle - Subtitle of the feed (optional). diff --git a/config/dmfs.conf.sample b/config/dmfs.conf.sample index 4adb297..14e3f31 100644 --- a/config/dmfs.conf.sample +++ b/config/dmfs.conf.sample @@ -23,7 +23,7 @@ -- -- OWFS path of the temperature sensor. -- -path = "/mnt/1wire/10.DCA98C020800/temperature" +owfs_path = "/mnt/1wire/10.DCA98C020800/temperature" -- -- Table of observations to be used in jobs list. The attribute `receivers` @@ -37,13 +37,14 @@ observs = { requests = { { name = "get-temp" - request = path, + request = owfs_path, pattern = "(?[-+0-9\\.]+)", delay = 500, responses = { { name = "temp", - unit = "degC" + unit = "degC", + type = RESPONSE_TYPE_REAL64 } } } diff --git a/config/dmgrc.conf.sample b/config/dmgrc.conf.sample index 3077817..baa8056 100644 --- a/config/dmgrc.conf.sample +++ b/config/dmgrc.conf.sample @@ -20,25 +20,12 @@ dmgrc = { response = "grc", level = LL_WARNING, levels = { - debug = { - GRC_ABORT, - GRC_SHUT_DOWN, - GRC_NO_EVENT - }, - info = { - GRC_SLEEP_NODE, - GRC_NA, - GRC_STOPPED - }, - warning = { - GRC_TMC_ACCURACY_GUARANTEE, - GRC_AUT_NO_TARGET, - GRC_AUT_ACCURACY - }, - error = { - GRC_FATAL - }, - critical = {} + debug = { GRC_ABORT, GRC_SHUT_DOWN, GRC_NO_EVENT }, + info = { GRC_SLEEP_NODE, GRC_NA, GRC_STOPPED }, + warning = { GRC_TMC_ACCURACY_GUARANTEE, GRC_AUT_NO_TARGET, GRC_AUT_ACCURACY }, + error = { GRC_FATAL }, + critical = {}, + user = {}, }, debug = false, verbose = true diff --git a/config/dmlogger.conf.sample b/config/dmlogger.conf.sample index bae3d47..049915f 100644 --- a/config/dmlogger.conf.sample +++ b/config/dmlogger.conf.sample @@ -4,12 +4,12 @@ -- The option `minlevel` may be set to one of the following -- named constants: -- --- LL_NONE - 0 -- LL_DEBUG - 1 -- LL_INFO - 2 -- LL_WARNING - 3 -- LL_ERROR - 4 -- LL_CRITICAL - 5 +-- LL_USER - 6 -- -- Rename table "dmlogger" to the instance name (parameter `--name`). -- @@ -17,7 +17,7 @@ -- -- database - Path to DMPACK log database. -- ipc - Use POSIX semaphore for process synchronisation. --- minlevel - Minimum level for a log to be stored in the database (0 to 5). +-- minlevel - Minimum level for a log to be stored in the database (1 to 6). -- node - Name of the sensor node. -- verbose - Print received logs to standard error. -- diff --git a/config/dmpipe.conf.sample b/config/dmpipe.conf.sample index 1399abf..bc432a6 100644 --- a/config/dmpipe.conf.sample +++ b/config/dmpipe.conf.sample @@ -34,12 +34,13 @@ observ = { { name = "get-cpu-temp", request = "sysctl hw.acpi.thermal.tz0.temperature", - pattern = "hw\\.acpi\\.thermal\\.tz0\\.temperature: (?[-+0-9\\.,]+)C", + pattern = "[.a-z]+: (?[-+0-9\\.,]+)C", delay = 0, responses = { { name = "tz0", - unit = "degC" + unit = "degC", + type = RESPONSE_TYPE_REAL64 } } }, @@ -49,12 +50,13 @@ observ = { { name = "get-battery", request = "sysctl hw.acpi.battery.life", - pattern = "hw\\.acpi\\.battery\\.life: (?[0-9]+)", + pattern = "[.a-z]+: (?[0-9]+)", delay = 0, responses = { { name = "battery", - unit = "%" + unit = "%", + type = RESPONSE_TYPE_REAL64 } } } diff --git a/config/dmplot.conf.sample b/config/dmplot.conf.sample index 4c55276..beca29f 100644 --- a/config/dmplot.conf.sample +++ b/config/dmplot.conf.sample @@ -3,22 +3,33 @@ -- -- Rename table "dmplot" to the instance name (parameter `--name`). -- +-- The following plot terminals are supported: +-- +-- ansi - ASCII format, in ANSI colours. +-- ascii - ASCII format. +-- gif - GIF format (libgd). +-- png - PNG format (libgd). +-- pngcairo - PNG format (libcairo), created from vector graphics. +-- sixelgd - Sixel format (libgd), originally for DEC terminals. +-- svg - W3C Scalable Vector Graphics (SVG) format. +-- x11 - Persistent X11 window (libX11). +-- -- Settings: -- -- background - Background colour (for example, `#ffffff` or `white`). -- font - Font name (for example, `Open Sans` or `monospace`). -- foreground - Foreground colour (for example, `#ff0000` or `red`). -- database - Path to observations database (required). --- from - Timestamp in ISO 8601 (required). +-- from - Start of time range in ISO 8601 (required). -- height - Plot height. -- node - Node id (required). -- output - Path to output file. -- response - Response name (required). -- sensor - Sensor id (required). -- target - Target id (required). --- terminal - Plot terminal, either `ansi`, `ascii`, `gif`, `png`, `pngcairo`, `sixelgd`, `svg`, `x11` (required). +-- terminal - Plot terminal (required). -- title - Plot title. --- to - Timestamp in ISO 8601 (required). +-- to - End of time range in ISO 8601 (required). -- width - Plot width. -- @@ -34,7 +45,7 @@ dmplot = { terminal = "svg", background = "#ffffff", foreground = "#ff0000", - font = "", + font = "sans", title = "Plot", width = 1200, height = 500 diff --git a/config/dmreport.conf.sample b/config/dmreport.conf.sample index 89d70ec..9ea37cb 100644 --- a/config/dmreport.conf.sample +++ b/config/dmreport.conf.sample @@ -4,27 +4,27 @@ -- The options `minlevel` and `maxlevel` may be set to one of the following -- named constants: -- --- LL_NONE - 0 -- LL_DEBUG - 1 -- LL_INFO - 2 -- LL_WARNING - 3 -- LL_ERROR - 4 -- LL_CRITICAL - 5 +-- LL_USER - 6 -- -- Rename table "dmreport" to the instance name (parameter `--name`). -- -- Settings: -- --- from - Timestamp in ISO 8601 (required). +-- from - Start of time range in ISO 8601 (required). -- logs - Logs settings. -- meta - Optional description text. -- node - Node id (required). --- output - Path to output file (if empty, report is printed to stdout). +-- output - Path to output file (if empty or `-`, report is printed to stdout). -- plots - Plots settings. -- style - Path to (minified) CSS file. -- subtitle - Subtitle of the report. -- title - Title of the report. --- to - Timestamp in ISO 8601 (required). +-- to - End of time range in ISO 8601 (required). -- dmreport = { diff --git a/config/dmsend.conf.sample b/config/dmsend.conf.sample index 451ff7e..ab05e0b 100644 --- a/config/dmsend.conf.sample +++ b/config/dmsend.conf.sample @@ -19,7 +19,7 @@ dmsend = { logger = "", - node = "", + node = "dummy-node", type = "observ", format = "nml", input = "observ.nml", diff --git a/config/dmsync.conf.sample b/config/dmsync.conf.sample index d79cea7..bb3c372 100644 --- a/config/dmsync.conf.sample +++ b/config/dmsync.conf.sample @@ -18,7 +18,7 @@ -- type - Data type to synchronise (`log`, `node`, `observ`, `sensor`, or `target`). -- user - HTTP Basic Auth user name (optional). -- verbose - Print logs to standard error. --- wait - Name of semaphore to wait for, e.g., `dmdb` or `dmlogger` (optional). +-- wait - Name of process to wait for, e.g., `dmdb` or `dmlogger` (optional). -- dmsync = { diff --git a/guide/guide.adoc b/guide/guide.adoc index 90a369b..cb6621b 100644 --- a/guide/guide.adoc +++ b/guide/guide.adoc @@ -198,7 +198,7 @@ package: The <> require a compatible web server, like: * link:https://www.lighttpd.net/[lighttpd] -* link:https://httpd.apache.org/[Apache HTTP Server] +* link:https://httpd.apache.org/[Apache httpd] DMPACK depends on additional interface libraries. If the repository is cloned recursively with Git, or if the project is built using FPM, the submodules will @@ -503,8 +503,8 @@ They contain name, type, value, unit, and an optional error code. === Log Entities <>:: Log message of a sensor node, either of level _debug_, -_info_, _warning_, _error_, or _critical_, and optionally related to a _sensor_, -a _target_, and an _observation_. +_info_, _warning_, _error_, _critical_, or _user_, and optionally related to a +_sensor_, a _target_, and an _observation_. === Beat Entities @@ -613,32 +613,37 @@ client and server. Requires a web server and _gnuplot(1)_. This section contains descriptions of all DMPACK programs with their respective command-line arguments. Some programs read settings from an optional -or mandatory configuration file. Examples are provided in directory -`/usr/local/etc/dmpack/` to be used as templates. The files are ordinary Lua -scripts, i.e., you can add Lua control structures for complex configurations or -access the <> of DMPACK. Set the language in your editor to Lua to -enable syntax highlighting (for instance, `set syntax=lua` in Vim), or use file -ending `.lua` instead of `.conf`. The set-up of the <> is outlined in the next section. +or mandatory configuration file. Example configuration files are provided in +directory `/usr/local/etc/dmpack/`. + +The files are ordinary Lua scripts, i.e., you can add Lua control structures +for complex tables or access the <> of DMPACK. In your editor, set the +language to Lua to enable syntax highlighting (for instance, `set syntax=lua` +in Vim), or use file ending `.lua` instead of `.conf`. The set-up of the +<> is outlined in the next section. === dmapi [[dmapi]] *dmapi* is an HTTP-RPC API service for remote DMPACK database access. The web -application has to be executed through a FastCGI-compatible web server or a -FastCGI spawner. It is recommended to use _lighttpd(1)_. +application has to be executed through a FastCGI-compatible web server. It is +recommended to use _lighttpd(1)_. The service is configured through environment +variables. The web server or FastCGI spawner must be able to pass environment +variables to *dmapi*. The *dmapi* service offers endpoints for clients to insert beats, logs, and observations into the local SQLite database, and to request data in CSV or JSON -format. Authentication and encryption are independent from *dmapi* and have to -be provided by the web server. All POST data has to be serialised in Fortran 95 -Namelist format, with optional link:http://www.zlib.net/[deflate] or -link:http://www.zstd.net/[zstd] compression. - -If HTTP Basic Auth is enabled, the sensor id of each beat, log, node, sensor, -and observation sent to the HTTP-RPC service must match the name of the -authenticated user. For example, to store an observation of a node with the id -`node-1`, the HTTP Basic Auth user name of the client must be `node-1` as well. -If the observation is sent by any other user, it will be rejected (HTTP 401). +format. Only HTTP GET and POST requests are accepted. All POST data has to be +serialised in Fortran 95 Namelist format, with optional +link:http://www.zlib.net/[deflate] or link:http://www.zstd.net/[zstd] +compression. Section <> gives an overview of the available endpoints. + +Authentication and encryption are independent from *dmapi* and have to be +provided by the web server. If HTTP Basic Auth is enabled, the sensor id of +each beat, log, node, sensor, and observation sent to the HTTP-RPC service must +match the name of the authenticated user. For example, to store an observation +of a node with the id `node-1`, the user name of the client must be `node-1` as +well. If the observation is sent by any other user, it will be rejected (HTTP +401). .Environment variables of _dmapi(1)_ [[dmapi-env]] @@ -652,23 +657,21 @@ If the observation is sent by any other user, it will be rejected (HTTP 401). | `DM_READ_ONLY` | Set to `1` to enable read-only database access. |=== -The web application is configured through environment variables. The web server -or FastCGI spawner must be able to pass environment variables to *dmapi*. See -section <> for a basic _lighttpd(1)_ configuration. - -The service accepts HTTP GET and POST requests. Section <> gives an -overview of the available endpoints. The response format depends on the MIME -type set in the HTTP Accept header of the request, either: +The response format depends on the MIME type set in the HTTP Accept header of +the request, either: * `application/json` (JSON) * `application/jsonl` (JSON Lines) * `application/namelist` (Fortran 95 Namelist) * `text/comma-separated-values` (CSV) +* `text/plain` (plain text) By default, responses are in CSV format. The Namelist format is available only for single records. Status messages are returned as key–value pairs, indicated by content type `text/plain`. +See section <> for a basic _lighttpd(1)_ configuration. + === dmbackup [[dmbackup]] The *dmbackup* utility creates an online backup of a running SQLite database. By @@ -1002,8 +1005,8 @@ the web server. An example style sheet `feed.xsl` is located in | `--force` | `-F` | – | Force file output even if no new log records are available. | `--help` | `-h` | – | Output available command-line arguments and quit. | `--id _uuid_` | `-I` | – | UUID of the feed, 36 characters long with hyphens. -| `--maxlevel _level_` | `-K` | `critical` | Select log messages of the given maximum <> (between `debug` or 1 and `critical` or 5). Must be greater or equal the minimum level. -| `--minlevel _level_` | `-L` | `debug` | Select log messages of the given minimum <> (between `debug` or 1 and `critical` or 5). +| `--maxlevel _level_` | `-K` | `critical` | Select log messages of the given maximum <> (between `debug` or 1 and `user` or 6). Must be greater or equal the minimum level. +| `--minlevel _level_` | `-L` | `debug` | Select log messages of the given minimum <> (between `debug` or 1 and `user` or 6). | `--name _name_` | `-n` | `dmfeed` | Name of instance and table in configuration. | `--node _id_` | `-N` | – | Select log messages of the given node id. | `--output _file_` | `-o` | _stdout_ | Path of the output file. If empty or `-`, the Atom feed will be printed to standard output. @@ -1146,7 +1149,8 @@ dmgrc = { info = { GRC_SLEEP_NODE, GRC_NA, GRC_STOPPED }, warning = { GRC_TMC_ACCURACY_GUARANTEE, GRC_AUT_NO_TARGET }, error = { GRC_FATAL }, - critical = {} + critical = {}, + user = {} }, debug = false, verbose = true @@ -1305,13 +1309,14 @@ The log level may be one of the following: [cols="1,2,5"] |=== -| Level | Parameter String | Name +| Level | Parameter String | Description -| 1 | `debug` | Debug -| 2 | `info` | Info -| 3 | `warning` | Warning -| 4 | `error` | Error -| 5 | `critical` | Critical +| 1 | `debug` | Debug message. +| 2 | `info` | Hint or info message. +| 3 | `warning` | Warning message. +| 4 | `error` | Non-critical error message. +| 5 | `critical` | Critical error message. +| 6 | `user` | User-defined log level. |=== Both, parameter strings and literal log level values, are accepted as @@ -1327,7 +1332,7 @@ command-line arguments. For level _warning_, set argument `--level` to `3` or | `--error _n_` | `-e` | 0 | DMPACK <> (optional). | `--help` | `-h` | – | Output available command-line arguments and quit. -| `--level _level_` | `-L` | `info` | <>, from `debug` or 1 to `critical` or 5. +| `--level _level_` | `-L` | `info` | <>, from `debug` or 1 to `user` or 6. | `--logger _name_` | `-l` | `dmlogger` | Name of logger instance and POSIX message queue. | `--message _string_` | `-m` | – | Log message (max. 512 characters). | `--node _id_` | `-N` | – | Node id (optional). @@ -1380,15 +1385,16 @@ through command-line argument `--wait`. The following log levels are accepted: -[cols="1,3,3"] +[cols="1,2,5"] |=== -| Level | Parameter String | Name +| Level | Parameter String | Description -| 1 | `debug` | Debug -| 2 | `info` | Info -| 3 | `warning` | Warning -| 4 | `error` | Error -| 5 | `critical` | Critical +| 1 | `debug` | Debug message. +| 2 | `info` | Hint or info message. +| 3 | `warning` | Warning message. +| 4 | `error` | Non-critical error message. +| 5 | `critical` | Critical error message. +| 6 | `user` | User-defined log level. |=== [discrete] @@ -1413,7 +1419,7 @@ this semaphore. | `--minlevel _level_` | `-L` | `info` -| Minimum level for a log to be stored in the database, from `debug` or 1 to `critical` or 5. +| Minimum level for a log to be stored in the database, from `debug` or 1 to `user` or 6. | `--name _name_` | `-n` @@ -1517,11 +1523,11 @@ $ dmlua --name dmlua --node dummy-node --script script.lua --verbose === dmpipe [[dmpipe]] -The *dmpipe* program reads responses from processes connected via pipe. - -All requests of an observation have to contain the process in attribute -`request`. Response values are extracted by group from the raw response using -the given regular expression pattern. +The *dmpipe* program reads responses from processes connected through a pipe to +read sensor data from a third-party program. Requests of an observation have to +contain the process to call in attribute `request`. Response values are +extracted by group from the raw response using the given regular expression +pattern. If any receivers are specified, observations are forwarded to the next receiver via POSIX message queue. The program can act as a sole data logger if output and @@ -1530,8 +1536,8 @@ _stdout_. A configuration file is mandatory to configure the jobs to perform. Each observation must have a valid target id. Node id, sensor id, and observation id -are added by *dmpipe*. Node, sensor, and target have to be present in the -database for the observation to be stored. +are added by *dmpipe*. If the observation will be stored in a database, the +node, sensor and target ids have to exist in the database. [discrete] ==== Command-Line Options @@ -1593,7 +1599,7 @@ dmpipe = { requests = { -- Pipes to open. { request = "sysctl hw.acpi.battery.life", -- Command to execute. - pattern = "hw\\.acpi\\.battery\\.life: (?[0-9]+)", -- RegEx. + pattern = "[.a-z]+: (?[0-9]+)", -- RegEx pattern. delay = 0, -- Delay in mseconds. responses = { { @@ -1717,9 +1723,9 @@ Create a plot of observations selected from database `observ.sqlite` in PNG format, and write the file to `/tmp/plot.png`: .... -$ dmplot --node dummy-node --sensor dummy-sensor --target dummy-target \ - --response dummy --from 2020 --to 2024 --database observ.sqlite \ - --terminal pngcairo --output /tmp/plot.png +$ dmplot --database /var/dmpack/observ.sqlite --terminal pngcairo --output /tmp/plot.png \ + --node dummy-node --sensor dummy-sensor --target dummy-target --response dummy \ + --from 2024 --to 2025 .... Output the plot directly to terminal, using the configuration in `dmplot.conf`: @@ -3708,7 +3714,7 @@ the log, otherwise, the request will be rejected as unauthorised (HTTP 401). |=== | GET Parameter | Type | Description -| `id` | string | Log id (UUID). +| `id` | string | Log id (UUIDv4). |=== [discrete] @@ -3900,7 +3906,7 @@ the observation, otherwise, the request will be rejected as unauthorised (HTTP |=== | GET Parameter | Type | Description -| `id` | string | Observation id (UUID). +| `id` | string | Observation id (UUIDv4). |=== [discrete] @@ -4889,6 +4895,7 @@ performed observations are forwarded to <>. | 3 | `LL_WARNING` | warning level | 4 | `LL_ERROR` | error level | 5 | `LL_CRITICAL` | critical level +| 6 | `LL_USER` | user-defined level |=== .Named response value type parameters @@ -6200,7 +6207,7 @@ JSON objects or JSON arrays. JSON Lines:: Export of beat, log, node, observation, sensor, and target data in link:https://jsonlines.org/[JSON Lines]/link:http://ndjson.org/[Newline Delimited JSON] format. -Lua:: Converting observations from and to Lua tables. Import of observations +Lua:: Converting observations from and to Lua tables: import of observations from Lua file or stack-based data exchange between Fortran and Lua. Namelist:: Import from and export to Fortran 95 Namelist (NML) format of single beat, log, node, observation, sensor, and target types. The syntax is @@ -6210,7 +6217,9 @@ Text:: Status messages of the HTTP-RPC API are returned as key–value pairs in plain text format The JSON Lines format equals the JSON format, except that multiple records are -separated by new line. +separated by new line. All string attributes of the derived types are 8 bit +only and limited to the ASCII character set, UUIDv4 identifiers and date-time +strings in ISO 8601 format are always 32 characters long. === API Status [[data-api]] @@ -6225,7 +6234,7 @@ separated by new line. | `host` | string | 32 | Server host name. | `server` | string | 32 | Server software (web server). | `timestamp` | string | 32 | Server date and time in ISO 8601. -| `message` | string | 32 | Server status message (optional). +| `message` | string | 32 | Server status message. | `error` | integer | 4 | <>. |=== @@ -6286,8 +6295,8 @@ error=0 "time_sent": "1970-01-01T00:00:00.000000+00:00", "time_recv": "1970-01-01T00:00:00.000000+00:00", "error": 0, - "interval": 0, - "uptime": 0 + "interval": 60, + "uptime": 3600 } .... @@ -6302,8 +6311,8 @@ BEAT%CLIENT="dmbeat 1.0.0 (DMPACK 1.0.0)", BEAT%TIME_SENT="1970-01-01T00:00:00.000000+00:00", BEAT%TIME_RECV="1970-01-01T00:00:00.000000+00:00", BEAT%ERROR=0, -BEAT%INTERVAL=0, -BEAT%UPTIME=0, +BEAT%INTERVAL=60, +BEAT%UPTIME=3600, / .... @@ -6348,15 +6357,16 @@ BEAT%UPTIME=0, === Log [[data-log]] .Log level [[data-log-level]] -[cols="1,2,2,2"] +[cols="1,2,2,5"] |=== -| Level | Parameter | Parameter String | Name +| Level | Parameter | Parameter String | Description -| 1 | `LL_DEBUG` | `debug` | Debug -| 2 | `LL_INFO` | `info` | Info -| 3 | `LL_WARNING` | `warning` | Warning -| 4 | `LL_ERROR` | `error` | Error -| 5 | `LL_CRITICAL` | `critical` | Critical +| 1 | `LL_DEBUG` | `debug` | Debug message. +| 2 | `LL_INFO` | `info` | Hint or info message. +| 3 | `LL_WARNING` | `warning` | Warning message. +| 4 | `LL_ERROR` | `error` | Non-critical error message. +| 5 | `LL_CRITICAL` | `critical` | Critical error message. +| 6 | `LL_USER` | `user` | User-defined log level. |=== ==== Derived Type @@ -6365,7 +6375,7 @@ BEAT%UPTIME=0, |=== | Attribute | Type | Size | Description -| `id` | string | 32 | Log id (UUID). +| `id` | string | 32 | Log id (UUIDv4). | `level` | integer | 4 | <>. | `error` | integer | 4 | <>. | `timestamp` | string | 32 | Date and time (ISO 8601). @@ -6582,7 +6592,7 @@ NODE%Z=0.0, |=== | Attribute | Type | Size | Description -| `id` | string | 32 | Observation id (UUID). +| `id` | string | 32 | Observation id (UUIDv4). | `node_id` | string | 32 | Node id (`-0-9A-Z_a-z`). | `sensor_id` | string | 32 | Sensor id (`-0-9A-Z_a-z`). | `target_id` | string | 32 | Target id (`-0-9A-Z_a-z`). @@ -7187,9 +7197,8 @@ TARGET%Z=0.0, | 125 | `E_LUA_ERROR` | Lua message handling error. | 126 | `E_LUA_FILE` | Lua file I/O error. | 130 | `E_LIB` | Generic library error. -| 131 | `E_FCGI` | FastCGI library error. +| 131 | `E_MODBUS` | Modbus library error. | 132 | `E_HDF5` | HDF5 library error. | 133 | `E_ZLIB` | Zlib library error. | 134 | `E_ZSTD` | Zstandard library error. -| 135 | `E_MODBUS` | Modbus library error. |=== diff --git a/src/dm_beat.f90 b/src/dm_beat.f90 index 541b71b..146612f 100644 --- a/src/dm_beat.f90 +++ b/src/dm_beat.f90 @@ -4,8 +4,8 @@ module dm_beat !! Heartbeat message type. use :: dm_error use :: dm_id - use :: dm_inet use :: dm_kind + use :: dm_net use :: dm_node use :: dm_time implicit none (type, external) @@ -16,7 +16,7 @@ module dm_beat type, public :: beat_type !! Status message (heartbeat) type. character(len=NODE_ID_LEN) :: node_id = ' ' !! Node id (`-0-9A-Z_a-z`). - character(len=INET_IPV6_LEN) :: address = ' ' !! Client IP address (IPv4, IPv6). + character(len=NET_IPV6_LEN) :: address = ' ' !! Client IP address (IPv4, IPv6). character(len=BEAT_CLIENT_LEN) :: client = ' ' !! Client software name and version. character(len=TIME_LEN) :: time_sent = TIME_DEFAULT !! Time heartbeat was sent. character(len=TIME_LEN) :: time_recv = TIME_DEFAULT !! Time heartbeat was received. diff --git a/src/dm_db.f90 b/src/dm_db.f90 index ff2e5c0..3053b6e 100644 --- a/src/dm_db.f90 +++ b/src/dm_db.f90 @@ -43,6 +43,7 @@ module dm_db use :: dm_id use :: dm_kind use :: dm_sql + use :: dm_string use :: dm_time use :: dm_uuid use :: dm_util @@ -130,7 +131,7 @@ end subroutine dm_db_update_handler end interface interface db_next_row - !! Generic table row access function. + !! Private generic table row access function. module procedure :: db_next_row_allocatable module procedure :: db_next_row_character module procedure :: db_next_row_beat @@ -145,6 +146,19 @@ end subroutine dm_db_update_handler module procedure :: db_next_row_target end interface db_next_row + interface dm_db_begin + !! Starts a transaction. Public alias for `db_begin()`. + !! + !! Optional argument `mode` may be one of: + !! + !! * `DB_TRANS_DEFERRED` + !! * `DB_TRANS_IMMEDIATE` + !! * `DB_TRANS_EXCLUSIVE` + !! + !! The default mode is `DB_TRANS_DEFERRED`. + module procedure :: db_begin + end interface dm_db_begin + interface dm_db_insert !! Generic database insert function. module procedure :: dm_db_insert_beat @@ -538,26 +552,6 @@ integer function dm_db_backup(db, path, wal, callback, nsteps, sleep_time) resul if (dm_is_error(dm_db_close(backup))) rc = E_DB end function dm_db_backup - integer function dm_db_begin(db, mode) result(rc) - !! Starts a transaction. Public wrapper for `db_begin()`. - !! - !! Optional argument `mode` may be one of: - !! - !! * `DB_TRANS_DEFERRED` - !! * `DB_TRANS_IMMEDIATE` - !! * `DB_TRANS_EXCLUSIVE` - !! - !! The default mode is `DB_TRANS_DEFERRED`. - type(db_type), intent(inout) :: db !! Database type. - integer, intent(in), optional :: mode !! Transaction mode. - - if (present(mode)) then - rc = db_begin(db, mode) - else - rc = db_begin(db) - end if - end function dm_db_begin - integer function dm_db_close(db, optimize) result(rc) !! Closes connection to SQLite database. Optimises the database if !! `optimize` is `.true.`. Returns `E_DB` on error. @@ -2732,39 +2726,29 @@ integer function dm_db_select_observ_ids(db, ids, node_id, sensor_id, target_id, has_target_id = .false.; has_from = .false.; has_to = .false. has_limit = .false.; desc_order = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(limit)) has_limit = .true. @@ -3263,12 +3247,7 @@ integer function dm_db_select_sync_logs(db, syncs, nsyncs, limit) result(rc) integer(kind=i8), intent(in), optional :: limit !! Max. number of sync data to fetch. integer(kind=i8) :: n - if (present(limit)) then - rc = db_select_syncs(db, SYNC_TYPE_LOG, SQL_SELECT_NSYNC_LOGS, SQL_SELECT_SYNC_LOGS, syncs, n, limit) - else - rc = db_select_syncs(db, SYNC_TYPE_LOG, SQL_SELECT_NSYNC_LOGS, SQL_SELECT_SYNC_LOGS, syncs, n) - end if - + rc = db_select_syncs(db, SYNC_TYPE_LOG, SQL_SELECT_NSYNC_LOGS, SQL_SELECT_SYNC_LOGS, syncs, n, limit) if (present(nsyncs)) nsyncs = n end function dm_db_select_sync_logs @@ -3307,12 +3286,7 @@ integer function dm_db_select_sync_nodes(db, syncs, nsyncs, limit) result(rc) integer(kind=i8), intent(in), optional :: limit !! Max. number of sync data to fetch. integer(kind=i8) :: n - if (present(limit)) then - rc = db_select_syncs(db, SYNC_TYPE_NODE, SQL_SELECT_NSYNC_NODES, SQL_SELECT_SYNC_NODES, syncs, n, limit) - else - rc = db_select_syncs(db, SYNC_TYPE_NODE, SQL_SELECT_NSYNC_NODES, SQL_SELECT_SYNC_NODES, syncs, n) - end if - + rc = db_select_syncs(db, SYNC_TYPE_NODE, SQL_SELECT_NSYNC_NODES, SQL_SELECT_SYNC_NODES, syncs, n, limit) if (present(nsyncs)) nsyncs = n end function dm_db_select_sync_nodes @@ -3352,12 +3326,7 @@ integer function dm_db_select_sync_observs(db, syncs, nsyncs, limit) result(rc) integer(kind=i8), intent(in), optional :: limit !! Max. number of sync data to fetch. integer(kind=i8) :: n - if (present(limit)) then - rc = db_select_syncs(db, SYNC_TYPE_OBSERV, SQL_SELECT_NSYNC_OBSERVS, SQL_SELECT_SYNC_OBSERVS, syncs, n, limit) - else - rc = db_select_syncs(db, SYNC_TYPE_OBSERV, SQL_SELECT_NSYNC_OBSERVS, SQL_SELECT_SYNC_OBSERVS, syncs, n) - end if - + rc = db_select_syncs(db, SYNC_TYPE_OBSERV, SQL_SELECT_NSYNC_OBSERVS, SQL_SELECT_SYNC_OBSERVS, syncs, n, limit) if (present(nsyncs)) nsyncs = n end function dm_db_select_sync_observs @@ -3396,12 +3365,7 @@ integer function dm_db_select_sync_sensors(db, syncs, nsyncs, limit) result(rc) integer(kind=i8), intent(in), optional :: limit !! Max. number of sync data to fetch. integer(kind=i8) :: n - if (present(limit)) then - rc = db_select_syncs(db, SYNC_TYPE_SENSOR, SQL_SELECT_NSYNC_SENSORS, SQL_SELECT_SYNC_SENSORS, syncs, n, limit) - else - rc = db_select_syncs(db, SYNC_TYPE_SENSOR, SQL_SELECT_NSYNC_SENSORS, SQL_SELECT_SYNC_SENSORS, syncs, n) - end if - + rc = db_select_syncs(db, SYNC_TYPE_SENSOR, SQL_SELECT_NSYNC_SENSORS, SQL_SELECT_SYNC_SENSORS, syncs, n, limit) if (present(nsyncs)) nsyncs = n end function dm_db_select_sync_sensors @@ -3440,12 +3404,7 @@ integer function dm_db_select_sync_targets(db, syncs, nsyncs, limit) result(rc) integer(kind=i8), intent(in), optional :: limit !! Max. number of sync data to fetch. integer(kind=i8) :: n - if (present(limit)) then - rc = db_select_syncs(db, SYNC_TYPE_TARGET, SQL_SELECT_NSYNC_TARGETS, SQL_SELECT_SYNC_TARGETS, syncs, n, limit) - else - rc = db_select_syncs(db, SYNC_TYPE_TARGET, SQL_SELECT_NSYNC_TARGETS, SQL_SELECT_SYNC_TARGETS, syncs, n) - end if - + rc = db_select_syncs(db, SYNC_TYPE_TARGET, SQL_SELECT_NSYNC_TARGETS, SQL_SELECT_SYNC_TARGETS, syncs, n, limit) if (present(nsyncs)) nsyncs = n end function dm_db_select_sync_targets @@ -4434,11 +4393,7 @@ integer function db_exec(db, query, err_msg) result(rc) integer :: stat rc = E_DB_EXEC - if (present(err_msg)) then - stat = sqlite3_exec(db%ptr, query, c_null_funptr, c_null_ptr, err_msg) - else - stat = sqlite3_exec(db%ptr, query, c_null_funptr, c_null_ptr) - end if + stat = sqlite3_exec(db%ptr, query, c_null_funptr, c_null_ptr, err_msg) if (stat /= SQLITE_OK) return rc = E_NONE @@ -5578,46 +5533,34 @@ integer function db_select_json_logs_array(db, strings, node_id, sensor_id, targ has_to = .false.; has_min_level = .false.; has_max_level = .false. has_error = .false.; has_limit = .false.; desc_order = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(source)) then - if (len_trim(source) > 0) then - has_param = .true. - has_source = .true. - end if + if (dm_string_is_present(source)) then + has_param = .true. + has_source = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(min_level)) then @@ -5820,46 +5763,34 @@ integer function db_select_json_logs_iter(db, db_stmt, json, node_id, sensor_id, has_to = .false.; has_min_level = .false.; has_max_level = .false. has_error = .false.; has_limit = .false.; desc_order = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(source)) then - if (len_trim(source) > 0) then - has_param = .true. - has_source = .true. - end if + if (dm_string_is_present(source)) then + has_param = .true. + has_source = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(min_level)) then @@ -6138,46 +6069,34 @@ integer function db_select_logs_array(db, logs, node_id, sensor_id, target_id, s has_to = .false.; has_min_level = .false.; has_max_level = .false. has_error = .false.; has_limit = .false.; desc_order = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(source)) then - if (len_trim(source) > 0) then - has_param = .true. - has_source = .true. - end if + if (dm_string_is_present(source)) then + has_param = .true. + has_source = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(min_level)) then @@ -6374,46 +6293,34 @@ integer function db_select_logs_iter(db, db_stmt, log, node_id, sensor_id, targe has_to = .false.; has_min_level = .false.; has_max_level = .false. has_error = .false.; has_limit = .false.; desc_order = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(source)) then - if (len_trim(source) > 0) then - has_param = .true. - has_source = .true. - end if + if (dm_string_is_present(source)) then + has_param = .true. + has_source = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(min_level)) then @@ -6703,39 +6610,29 @@ integer function db_select_observs_array(db, observs, node_id, sensor_id, target has_target_id = .false.; has_from = .false.; has_to = .false. has_limit = .false.; desc_order = .false.; stub_view = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(limit)) has_limit = .true. @@ -6899,39 +6796,29 @@ integer function db_select_observs_iter(db, db_stmt, observ, node_id, sensor_id, has_target_id = .false.; has_from = .false.; has_to = .false. has_limit = .false.; desc_order = .false.; stub_view = .false. - if (present(node_id)) then - if (len_trim(node_id) > 0) then - has_param = .true. - has_node_id = .true. - end if + if (dm_string_is_present(node_id)) then + has_param = .true. + has_node_id = .true. end if - if (present(sensor_id)) then - if (len_trim(sensor_id) > 0) then - has_param = .true. - has_sensor_id = .true. - end if + if (dm_string_is_present(sensor_id)) then + has_param = .true. + has_sensor_id = .true. end if - if (present(target_id)) then - if (len_trim(target_id) > 0) then - has_param = .true. - has_target_id = .true. - end if + if (dm_string_is_present(target_id)) then + has_param = .true. + has_target_id = .true. end if - if (present(from)) then - if (len_trim(from) > 0) then - has_param = .true. - has_from = .true. - end if + if (dm_string_is_present(from)) then + has_param = .true. + has_from = .true. end if - if (present(to)) then - if (len_trim(to) > 0) then - has_param = .true. - has_to = .true. - end if + if (dm_string_is_present(to)) then + has_param = .true. + has_to = .true. end if if (present(limit)) has_limit = .true. diff --git a/src/dm_error.f90 b/src/dm_error.f90 index 961e1d0..24c6f46 100644 --- a/src/dm_error.f90 +++ b/src/dm_error.f90 @@ -115,12 +115,11 @@ module dm_error ! Additional errors. integer, parameter, public :: E_LIB = 130 !! Generic library error. - integer, parameter, public :: E_FCGI = 131 !! FastCGI error. + integer, parameter, public :: E_MODBUS = 131 !! Modbus error. integer, parameter, public :: E_HDF5 = 132 !! HDF5 error. integer, parameter, public :: E_ZLIB = 133 !! zlib error. integer, parameter, public :: E_ZSTD = 134 !! Zstandard error. - integer, parameter, public :: E_MODBUS = 135 !! Modbus error. - integer, parameter, public :: E_LAST = 135 !! Never use this. + integer, parameter, public :: E_LAST = 134 !! Never use this. ! Exit status codes for `dm_stop(stat)`. integer, parameter, public :: STOP_SUCCESS = 0 !! Exit status 0. @@ -328,16 +327,14 @@ pure function dm_error_message(error) result(message) ! Libraries. case (E_LIB) message = 'library error' - case (E_FCGI) - message = 'FastCGI error' + case (E_MODBUS) + message = 'Modbus error' case (E_HDF5) message = 'HDF5 error' case (E_ZLIB) message = 'zlib error' case (E_ZSTD) message = 'zstd error' - case (E_MODBUS) - message = 'Modbus error' case default message = 'unknown error' diff --git a/src/dm_fcgi.f90 b/src/dm_fcgi.f90 index f532495..05ca084 100644 --- a/src/dm_fcgi.f90 +++ b/src/dm_fcgi.f90 @@ -38,11 +38,28 @@ end function fcgi_puts public :: dm_fcgi_header public :: dm_fcgi_out contains - integer function dm_fcgi_accept() result(rc) + logical function dm_fcgi_accept() result(accept) !! Accepts new FastCGI connection (blocking). The function returns - !! `E_FCGI` on error. - rc = E_FCGI - if (fcgi_accept() >= 0) rc = E_NONE + !! `.false.` on error. + !! + !! The function accepts a new request from the HTTP server and creates a + !! CGI-compatible execution environment for the request. + !! + !! If the application was invoked as a CGI program, the first call to + !! `dm_fcgi_accept()` is essentially a no-op and the second call returns + !! `.false.`. This causes a correctly coded FastCGI Responder + !! application to run a single request and exit, giving CGI behaviour. + !! + !! If the application was invoked as a FastCGI server, the first call to + !! the function indicates that the application has completed its + !! initialisation and is ready to accept its first request. Subsequent + !! calls indicate that the application has completed processing its + !! current request and is ready to accept a new request. + !! + !! In completing the current request, the called FastCGI function may + !! detect errors, e.g. a broken pipe to a client who has disconnected + !! early. The API function ignores such errors. + accept = (fcgi_accept() == 0) end function dm_fcgi_accept integer function dm_fcgi_content(env, content) result(rc) @@ -107,7 +124,8 @@ subroutine dm_fcgi_header(content_type, http_status) end subroutine dm_fcgi_header subroutine dm_fcgi_out(content) - !! Writes given content as response. + !! Writes given content as response. The argument will be + !! null-terminated. character(len=*), intent(in) :: content !! Response content. integer :: stat diff --git a/src/dm_geocom.f90 b/src/dm_geocom.f90 index 120a0b9..31f5c8d 100644 --- a/src/dm_geocom.f90 +++ b/src/dm_geocom.f90 @@ -21,12 +21,15 @@ module dm_geocom !! integer :: rc ! DMPACK return code. !! type(geocom_class) :: geocom ! GeoCOM object. !! + !! ! Open connection to instrument, quit on error. !! call geocom%open('/dev/ttyUSB0', GEOCOM_COM_BAUD_115200, verbose=.true., error=rc) - !! dm_error_out(rc, fatal=.true.) + !! call dm_error_out(rc, fatal=.true.) !! + !! ! Call remote procedure COM_NullProc and output result. !! call geocom%null() !! print '(i0, ": ", a)', geocom%code(), geocom%message() !! + !! ! Close connection. !! call geocom%close() !! ``` !! @@ -390,9 +393,9 @@ module dm_geocom !! !! ### GEOCOM_TMC_INCLINE_PRG !! - !! * `GEOCOM_TMC_MEA_INC` – Use sensor (a priori sigma). - !! * `GEOCOM_TMC_AUTO_INC` – Automatic mode (sensor/plane). - !! * `GEOCOM_TMC_PLANE_INC` – Use plane (a priori sigma). + !! * `GEOCOM_TMC_MEA_INC` – Use sensor (a priori sigma). + !! * `GEOCOM_TMC_AUTO_INC` – Automatic mode (sensor/plane). + !! * `GEOCOM_TMC_PLANE_INC` – Use plane (a priori sigma). !! !! ### GEOCOM_TMC_MEASURE_PRG !! diff --git a/src/dm_hash_table.f90 b/src/dm_hash_table.f90 index 8504dbb..778958b 100644 --- a/src/dm_hash_table.f90 +++ b/src/dm_hash_table.f90 @@ -53,7 +53,8 @@ integer function dm_hash_table_create(hash_table, max_entries) result(rc) !! Create a new hash table with maximum number of entries. type(hash_table_type), intent(inout) :: hash_table !! Hash table type. integer, intent(in) :: max_entries !! Maximum number of entries. - integer :: stat + + integer :: stat rc = E_INVALID if (max_entries < 1) return @@ -65,8 +66,8 @@ integer function dm_hash_table_create(hash_table, max_entries) result(rc) allocate (hash_table%values(max_entries), stat=stat) if (stat /= 0) return - hash_table%hashes(:) = 0_i8 rc = E_NONE + hash_table%hashes(:) = 0_i8 end function dm_hash_table_create integer function dm_hash_table_set(hash_table, key, value) result(rc) @@ -98,7 +99,8 @@ subroutine dm_hash_table_destroy(hash_table) !! Finalises hash table. If the hash table items contain allocatable !! data types, you have to deallocate them manually beforehand. type(hash_table_type), intent(inout) :: hash_table !! Hash table type. - integer :: i + + integer :: i if (allocated(hash_table%values)) then do i = 1, size(hash_table%values) @@ -132,6 +134,12 @@ end subroutine dm_hash_table_size integer function hash_table_get_index(hash_table, loc, value) result(rc) !! Returns pointer to element in hash table by index `i`. On error, !! `value` will point to null. + !! + !! The function returns the following error codes: + !! + !! * `E_BOUNDS` if the location in outside the array bounds. + !! * `E_NULL` if the hash table value pointer is not associated. + !! type(hash_table_type), intent(inout) :: hash_table !! Hash table type. integer, intent(in) :: loc !! Hash value index. class(*), pointer, intent(out) :: value !! Associated value. @@ -141,7 +149,7 @@ integer function hash_table_get_index(hash_table, loc, value) result(rc) rc = E_BOUNDS if (loc < 1 .or. loc > size(hash_table%values)) return - rc = E_INVALID + rc = E_NULL if (.not. associated(hash_table%values(loc)%ptr)) return rc = E_NONE @@ -153,6 +161,12 @@ integer function hash_table_get_key(hash_table, key, value) result(rc) !! will point to null. The intrinsic `findloc()` should be sufficient for !! a small number of elements. For larger hash tables, buckets have to be !! added. + !! + !! The function returns the following error codes: + !! + !! * `E_NOT_FOUND` if the key was not found. + !! * `E_NULL` if the hash table value pointer is not associated. + !! type(hash_table_type), intent(inout) :: hash_table !! Hash table type. character(len=*), intent(in) :: key !! Hash table key. class(*), pointer, intent(out) :: value !! Associated value. @@ -162,12 +176,12 @@ integer function hash_table_get_key(hash_table, key, value) result(rc) value => null() - rc = E_EMPTY + rc = E_NOT_FOUND hash = hash_table_hash(key) loc = findloc(hash_table%hashes, hash, dim=1) if (loc == 0) return - rc = E_INVALID + rc = E_NULL if (.not. associated(hash_table%values(loc)%ptr)) return rc = E_NONE diff --git a/src/dm_inet.f90 b/src/dm_inet.f90 deleted file mode 100644 index 66e3e85..0000000 --- a/src/dm_inet.f90 +++ /dev/null @@ -1,10 +0,0 @@ -! Author: Philipp Engel -! Licence: ISC -module dm_inet - !! TCP/IP parameters. - implicit none (type, external) - private - - integer, parameter, public :: INET_IPV4_LEN = 15 !! IPv4 address length. - integer, parameter, public :: INET_IPV6_LEN = 45 !! IPv6 address length. -end module dm_inet diff --git a/src/dm_log.f90 b/src/dm_log.f90 index 62943af..8ca9f4b 100644 --- a/src/dm_log.f90 +++ b/src/dm_log.f90 @@ -2,6 +2,22 @@ ! Licence: ISC module dm_log !! Log type and log level declaration. + !! + !! The following log levels are supported: + !! + !! | Level | String | Description | + !! |-------|------------|----------------------------------------------| + !! | 0 | `none` | Invalid log level (unused). | + !! | 1 | `debug` | Debug messages. | + !! | 2 | `info` | Hint message. | + !! | 3 | `warning` | Warning message. | + !! | 5 | `error` | Non-critical error message. | + !! | 4 | `critical` | Critical error message (not used by DMPACK). | + !! | 5 | `user` | User-defined level (not used by DMPACK). | + !! + !! Log level _critical_ is reserved for monitoring events and not used by + !! DMPACK internally. Level _user_ is reserved for user-defined events and + !! also not used. use :: dm_error use :: dm_id use :: dm_kind @@ -21,22 +37,23 @@ module dm_log integer, parameter, public :: LL_WARNING = 3 !! For events requiring the attention of the system operator. integer, parameter, public :: LL_ERROR = 4 !! Unexpected behaviour, may indicate failure. integer, parameter, public :: LL_CRITICAL = 5 !! Reserved for monitoring events, not used by DMPACK internally. - integer, parameter, public :: LL_LAST = 5 !! Never use this. + integer, parameter, public :: LL_USER = 6 !! User-defined level, not used by DMPACK. + integer, parameter, public :: LL_LAST = 6 !! Never use this. ! Log parameters. - integer, parameter, public :: LOG_NLEVEL = 6 !! Number of log level. - integer, parameter, public :: LOG_ID_LEN = UUID_LEN !! Max. log id length. - integer, parameter, public :: LOG_SOURCE_LEN = ID_LEN !! Max. log source length. - integer, parameter, public :: LOG_MESSAGE_LEN = 512 !! Max. log message length. + integer, parameter, public :: LOG_NLEVEL = LL_LAST + 1 !! Number of log level. + integer, parameter, public :: LOG_ID_LEN = UUID_LEN !! Max. log id length. + integer, parameter, public :: LOG_SOURCE_LEN = ID_LEN !! Max. log source length. + integer, parameter, public :: LOG_MESSAGE_LEN = 512 !! Max. log message length. integer, parameter, public :: LOG_LEVEL_NAME_LEN = 8 character(len=*), parameter, public :: LOG_LEVEL_NAMES(0:LL_LAST) = [ & - character(len=LOG_LEVEL_NAME_LEN) :: 'NONE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' & + character(len=LOG_LEVEL_NAME_LEN) :: 'NONE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'USER' & ] !! Log level strings. character(len=*), parameter, public :: LOG_LEVEL_NAMES_LOWER(0:LL_LAST) = [ & - character(len=LOG_LEVEL_NAME_LEN) :: 'none', 'debug', 'info', 'warning', 'error', 'critical' & + character(len=LOG_LEVEL_NAME_LEN) :: 'none', 'debug', 'info', 'warning', 'error', 'critical', 'user' & ] !! Log level strings in lower-case. type, public :: log_type @@ -103,8 +120,8 @@ end function dm_log_equals pure elemental integer function dm_log_level_from_name(name) result(level) !! Returns log level from string argument `name`. The string is !! converted to lower-case before. If `name` neither matches `none`, - !! `debug`, `warning`, `error`, nor `critical`, this function returns - !! `LL_NONE`. + !! `debug`, `warning`, `error`, `critical`, nor `user` this function + !! returns `LL_NONE`. use :: dm_string, only: dm_lower character(len=*), intent(in) :: name !! Log level name. @@ -115,8 +132,6 @@ pure elemental integer function dm_log_level_from_name(name) result(level) name_ = dm_lower(name) select case (name_) - case (LOG_LEVEL_NAMES_LOWER(LL_NONE)) - level = LL_NONE case (LOG_LEVEL_NAMES_LOWER(LL_DEBUG)) level = LL_DEBUG case (LOG_LEVEL_NAMES_LOWER(LL_INFO)) @@ -127,6 +142,8 @@ pure elemental integer function dm_log_level_from_name(name) result(level) level = LL_ERROR case (LOG_LEVEL_NAMES_LOWER(LL_CRITICAL)) level = LL_CRITICAL + case (LOG_LEVEL_NAMES_LOWER(LL_USER)) + level = LL_USER case default level = LL_NONE end select @@ -158,14 +175,19 @@ end subroutine dm_log_out ! PRIVATE PROCEDURES. ! ****************************************************************** pure elemental logical function dm_log_valid_level(level) result(valid) - !! Returns `.true.` if given log level is valid, i.e., either - !! `LL_DEBUG`, `LL_WARNING`, `LL_ERROR`, or `LL_CRITICAL`. The level - !! `LL_NONE` is invalid. + !! Returns `.true.` if given log level is valid. The following level + !! are valid: + !! + !! * `LL_DEBUG` + !! * `LL_WARNING` + !! * `LL_ERROR` + !! * `LL_CRITICAL` + !! * `LL_USER` + !! + !! The level `LL_NONE` is invalid. integer, intent(in) :: level !! Log level. - valid = .false. - if (level <= LL_NONE .or. level > LL_LAST) return - valid = .true. + valid = (level > LL_NONE .and. level <= LL_LAST) end function dm_log_valid_level pure elemental logical function dm_log_valid_log(log) result(valid) @@ -183,11 +205,11 @@ pure elemental logical function dm_log_valid_log(log) result(valid) valid = .false. - if (.not. dm_log_valid(log%level)) return - if (.not. dm_error_valid(log%error)) return - if (log%id == UUID_DEFAULT) return - if (.not. dm_uuid4_valid(log%id)) return - if (.not. dm_time_valid(log%timestamp)) return + if (.not. dm_log_valid(log%level)) return + if (.not. dm_error_valid(log%error)) return + if (log%id == UUID_DEFAULT) return + if (.not. dm_uuid4_valid(log%id)) return + if (.not. dm_time_valid(log%timestamp)) return if (.not. dm_string_is_printable(log%message)) return valid = .true. diff --git a/src/dm_logger.f90 b/src/dm_logger.f90 index db6487f..ea1eefd 100644 --- a/src/dm_logger.f90 +++ b/src/dm_logger.f90 @@ -36,7 +36,7 @@ module dm_logger ! ANSI colours of log level. integer, parameter :: LOGGER_COLORS(LL_NONE:LL_LAST) = [ & - COLOR_RESET, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_RED, COLOR_RED & + COLOR_RESET, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_RED, COLOR_RED, COLOR_CYAN & ] !! Colours associated with log level. type, public :: logger_class @@ -66,6 +66,7 @@ module dm_logger procedure, public :: debug => logger_log_debug procedure, public :: error => logger_log_error procedure, public :: info => logger_log_info + procedure, public :: user => logger_log_user procedure, public :: warning => logger_log_warning end type logger_class @@ -84,6 +85,7 @@ module dm_logger private :: logger_log_error private :: logger_log_info private :: logger_log_type + private :: logger_log_user private :: logger_log_warning private :: logger_out private :: logger_send @@ -327,6 +329,20 @@ subroutine logger_log_type(this, log) if (this%ipc) call this%send(log) end subroutine logger_log_type + subroutine logger_log_user(this, message, source, observ, timestamp, error, escape, verbose) + !! Sends a user-defined log message to the message queue. + class(logger_class), intent(inout) :: this !! Logger object. + character(len=*), intent(in) :: message !! Log message. + character(len=*), intent(in), optional :: source !! Optional source of log. + type(observ_type), intent(inout), optional :: observ !! Optional observation data. + character(len=*), intent(in), optional :: timestamp !! Optional timestamp of log. + integer, intent(in), optional :: error !! Optional error code. + logical, intent(in), optional :: escape !! Escape non-printable characters in message. + logical, intent(in), optional :: verbose !! Create log if `error` is `E_NONE`. + + call this%log(LL_USER, message, source, observ, timestamp, error, escape, verbose) + end subroutine logger_log_user + subroutine logger_log_warning(this, message, source, observ, timestamp, error, escape, verbose) !! Sends a warning log message to the message queue. class(logger_class), intent(inout) :: this !! Logger object. diff --git a/src/dm_lua_api.f90 b/src/dm_lua_api.f90 index e31de99..00581fa 100644 --- a/src/dm_lua_api.f90 +++ b/src/dm_lua_api.f90 @@ -42,6 +42,7 @@ integer function dm_lua_api_register(lua, errors, log_levels, procedures, respon !! * `LL_WARNING` !! * `LL_ERROR` !! * `LL_CRITICAL` + !! * `LL_USER` !! !! The following Lua procedures are registered if `procedures` is not !! `.false.`: @@ -186,11 +187,10 @@ integer function dm_lua_api_register(lua, errors, log_levels, procedures, respon rc = dm_lua_set(lua, 'E_LUA_FILE', E_LUA_FILE); if (dm_is_error(rc)) return rc = dm_lua_set(lua, 'E_LIB', E_LIB); if (dm_is_error(rc)) return - rc = dm_lua_set(lua, 'E_FCGI', E_FCGI); if (dm_is_error(rc)) return + rc = dm_lua_set(lua, 'E_MODBUS', E_MODBUS); if (dm_is_error(rc)) return rc = dm_lua_set(lua, 'E_HDF5', E_HDF5); if (dm_is_error(rc)) return rc = dm_lua_set(lua, 'E_ZLIB', E_ZLIB); if (dm_is_error(rc)) return rc = dm_lua_set(lua, 'E_ZSTD', E_ZSTD); if (dm_is_error(rc)) return - rc = dm_lua_set(lua, 'E_MODBUS', E_MODBUS); if (dm_is_error(rc)) return end if ! Add log levels. @@ -201,6 +201,7 @@ integer function dm_lua_api_register(lua, errors, log_levels, procedures, respon rc = dm_lua_set(lua, 'LL_WARNING', LL_WARNING); if (dm_is_error(rc)) return rc = dm_lua_set(lua, 'LL_ERROR', LL_ERROR); if (dm_is_error(rc)) return rc = dm_lua_set(lua, 'LL_CRITICAL', LL_CRITICAL); if (dm_is_error(rc)) return + rc = dm_lua_set(lua, 'LL_USER', LL_USER); if (dm_is_error(rc)) return end if ! Register response type parameters. diff --git a/src/dm_net.f90 b/src/dm_net.f90 new file mode 100644 index 0000000..3fc8d21 --- /dev/null +++ b/src/dm_net.f90 @@ -0,0 +1,10 @@ +! Author: Philipp Engel +! Licence: ISC +module dm_net + !! TCP/IP parameters. + implicit none (type, external) + private + + integer, parameter, public :: NET_IPV4_LEN = 15 !! IPv4 address length. + integer, parameter, public :: NET_IPV6_LEN = 45 !! IPv6 address length. +end module dm_net diff --git a/src/dm_string.f90 b/src/dm_string.f90 index 19e8378..c41d81d 100644 --- a/src/dm_string.f90 +++ b/src/dm_string.f90 @@ -7,6 +7,9 @@ module dm_string implicit none (type, external) private + character(len=*), parameter :: FMT_INTEGER = '(i0)' + character(len=*), parameter :: FMT_REAL = '(f0.12)' + type, public :: string_type !! Derived type of allocatable character to be stored in an array. character(len=:), allocatable :: data @@ -61,6 +64,7 @@ module dm_string public :: dm_string_count_lines public :: dm_string_count_substring public :: dm_string_is_empty + public :: dm_string_is_present public :: dm_string_is_printable public :: dm_string_lower public :: dm_string_split @@ -136,19 +140,29 @@ pure elemental integer function dm_string_count_substring(s1, s2) result(n) end do end function dm_string_count_substring - logical function dm_string_is_empty(str) result(empty) + logical function dm_string_is_empty(str) result(is) !! Returns `.true.` if given allocatable string is not passed, not - !! allocated, or empty. + !! allocated, or contains only white spaces. character(len=:), allocatable, intent(inout), optional :: str !! Input string. - empty = .true. + is = .true. if (.not. present(str)) return if (.not. allocated(str)) return if (len_trim(str) == 0) return - empty = .false. + is = .false. end function dm_string_is_empty - pure logical function dm_string_is_printable(str) result(printable) + pure logical function dm_string_is_present(str) result(is) + !! Returns `.true.` if given string is present and not empty. + character(len=*), intent(in), optional :: str !! Input string. + + is = .false. + if (.not. present(str)) return + if (len_trim(str) == 0) return + is = .true. + end function dm_string_is_present + + pure logical function dm_string_is_printable(str) result(is) !! Returns `.true.` if all characters is given string are printable !! ASCII characters. use :: dm_ascii, only: dm_ascii_is_printable @@ -156,13 +170,13 @@ pure logical function dm_string_is_printable(str) result(printable) character(len=*), intent(in) :: str !! String to validate. integer :: i - printable = .false. + is = .false. do i = 1, len_trim(str) if (.not. dm_ascii_is_printable(str(i:i))) return end do - printable = .true. + is = .true. end function dm_string_is_printable pure elemental function dm_string_lower(str) result(lower) @@ -208,6 +222,7 @@ pure elemental subroutine dm_string_allocate(string, n) n_ = 0 if (present(n)) n_ = max(0, n) + if (.not. allocated(string%data)) allocate (character(len=n_) :: string%data) end subroutine dm_string_allocate @@ -294,7 +309,7 @@ pure subroutine string_from_int32(i, str, error) if (present(error)) error = E_FORMAT allocate (character(len=n) :: str) - write (str, '(i0)', iostat=stat) i + write (str, FMT_INTEGER, iostat=stat) i if (stat /= 0) return if (present(error)) error = E_NONE end subroutine string_from_int32 @@ -316,7 +331,7 @@ pure subroutine string_from_int64(i, str, error) if (present(error)) error = E_FORMAT allocate (character(len=n) :: str) - write (str, '(i0)', iostat=stat) i + write (str, FMT_INTEGER, iostat=stat) i if (stat /= 0) return if (present(error)) error = E_NONE end subroutine string_from_int64 @@ -331,7 +346,7 @@ pure subroutine string_from_real32(f, str, error) character(len=20) :: buf if (present(error)) error = E_FORMAT - write (buf, '(f0.12)', iostat=stat) f + write (buf, FMT_REAL, iostat=stat) f if (stat /= 0) then str = '' return @@ -350,7 +365,7 @@ pure subroutine string_from_real64(f, str, error) character(len=20) :: buf if (present(error)) error = E_FORMAT - write (buf, '(f0.12)', iostat=stat) f + write (buf, FMT_REAL, iostat=stat) f if (stat /= 0) then str = '' return diff --git a/src/dm_tty.f90 b/src/dm_tty.f90 index 96fb2a1..6ffa14e 100644 --- a/src/dm_tty.f90 +++ b/src/dm_tty.f90 @@ -39,12 +39,12 @@ module dm_tty integer, parameter, public :: TTY_B921600 = 921600 ! Parity. + integer, parameter, public :: TTY_PARITY_NAME_LEN = 4 !! Parity string length. + integer, parameter, public :: TTY_PARITY_NONE = 1 !! No parity. integer, parameter, public :: TTY_PARITY_EVEN = 2 !! Even parity. integer, parameter, public :: TTY_PARITY_ODD = 3 !! Odd parity. - integer, parameter, public :: TTY_PARITY_NAME_LEN = 4 !! Parity string length. - ! Byte size. integer, parameter, public :: TTY_BYTE_SIZE5 = 1 !! 5 bits. integer, parameter, public :: TTY_BYTE_SIZE6 = 2 !! 6 bits. diff --git a/src/dm_util.f90 b/src/dm_util.f90 index 17291b1..ff8b300 100644 --- a/src/dm_util.f90 +++ b/src/dm_util.f90 @@ -7,6 +7,10 @@ module dm_util implicit none (type, external) private + + character(len=*), parameter :: FMT_INTEGER = '(i0)' + character(len=*), parameter :: FMT_REAL = '(1pg0.12)' + interface dm_array_has !! Returns whether array contains an integer value. module procedure :: array_has_int32 @@ -64,6 +68,7 @@ module dm_util public :: dm_array_has public :: dm_equals public :: dm_inc + public :: dm_msleep public :: dm_sleep public :: dm_usleep @@ -231,12 +236,24 @@ pure elemental function dm_logical_to_real64(l) result(r) end if end function dm_logical_to_real64 + subroutine dm_msleep(sec) + !! Pauses program execution for given time in mseconds. + use :: unix, only: c_useconds_t, c_usleep + + integer, intent(in) :: sec !! Delay in seconds [msec]. + + integer :: rc + + rc = c_usleep(int(sec * 1000, kind=c_useconds_t)) + end subroutine dm_msleep + subroutine dm_sleep(sec) !! Pauses program execution for given time in seconds. use :: unix, only: c_useconds_t, c_usleep - integer, intent(in) :: sec !! Delay in seconds [s]. - integer :: rc + integer, intent(in) :: sec !! Delay in seconds [sec]. + + integer :: rc rc = c_usleep(int(sec * 10**6, kind=c_useconds_t)) end subroutine dm_sleep @@ -245,8 +262,9 @@ subroutine dm_usleep(usec) !! Pauses program execution for given time in useconds. use :: unix, only: c_useconds_t, c_usleep - integer, intent(in) :: usec !! Delay in useconds [us]. - integer :: rc + integer, intent(in) :: usec !! Delay in useconds [usec]. + + integer :: rc rc = c_usleep(int(usec, kind=c_useconds_t)) end subroutine dm_usleep @@ -372,7 +390,7 @@ pure function int32_to_string(i) result(str) end if allocate (character(len=n) :: str) - write (str, '(i0)', iostat=stat) i + write (str, FMT_INTEGER, iostat=stat) i end function int32_to_string pure function int64_to_string(i) result(str) @@ -390,7 +408,7 @@ pure function int64_to_string(i) result(str) end if allocate (character(len=n) :: str) - write (str, '(i0)', iostat=stat) i + write (str, FMT_INTEGER, iostat=stat) i end function int64_to_string pure function real32_to_string(f) result(str) @@ -402,7 +420,7 @@ pure function real32_to_string(f) result(str) integer :: stat str = '' - write (buf, '(1pg0.12)', iostat=stat) f + write (buf, FMT_REAL, iostat=stat) f if (stat /= 0) return str = trim(buf) end function real32_to_string @@ -416,7 +434,7 @@ pure function real64_to_string(f) result(str) integer :: stat str = '' - write (buf, '(1pg0.12)', iostat=stat) f + write (buf, FMT_REAL, iostat=stat) f if (stat /= 0) return str = trim(buf) end function real64_to_string diff --git a/src/dm_uuid.f90 b/src/dm_uuid.f90 index bb4d4c3..d665350 100644 --- a/src/dm_uuid.f90 +++ b/src/dm_uuid.f90 @@ -7,7 +7,7 @@ module dm_uuid private integer, parameter, public :: UUID_LEN = 32 !! Hex UUIDv4 length. - integer, parameter, public :: UUID_FULL_LEN = 36 !! Full UUIDv4 length (with hypens). + integer, parameter, public :: UUID_FULL_LEN = 36 !! Full UUIDv4 length (with hyphens). character(len=*), parameter, public :: UUID_DEFAULT = repeat('0', UUID_LEN) !! Default ID (hex). character(len=*), parameter :: UUID_SET = '0123456789abcdef' diff --git a/src/dmpack.f90 b/src/dmpack.f90 index 5d9d654..db793d0 100644 --- a/src/dmpack.f90 +++ b/src/dmpack.f90 @@ -20,7 +20,7 @@ module dmpack !! end program main !! ``` !! - !! Link the program against `libdmpack.a`: + !! Link the program against static library `libdmpack.a`: !! !! ``` !! $ gfortran -I/usr/local/include/dmpack -o app app.f90 /usr/local/lib/libdmpack.a @@ -59,7 +59,6 @@ module dmpack use :: dm_html use :: dm_http use :: dm_id - use :: dm_inet use :: dm_job use :: dm_json use :: dm_jsonl @@ -78,6 +77,7 @@ module dmpack use :: dm_mqueue use :: dm_mqueue_util use :: dm_mutex + use :: dm_net use :: dm_nml use :: dm_node use :: dm_observ diff --git a/test/dmtestdb.f90 b/test/dmtestdb.f90 index c6655b2..577778d 100644 --- a/test/dmtestdb.f90 +++ b/test/dmtestdb.f90 @@ -19,7 +19,6 @@ program dmtestdb integer, parameter :: NLOGS = 100 integer, parameter :: NOBSERVS = 100 - type(test_type) :: tests(NTESTS) logical :: stats(NTESTS) @@ -61,7 +60,7 @@ logical function test01() result(stat) print *, 'Checking for stale database "' // DB_OBSERV // '" ...' if (dm_file_exists(DB_OBSERV)) then - print *, 'Deleting old database ...' + print *, 'Deleting stale database ...' call dm_file_delete(DB_OBSERV) end if @@ -111,9 +110,11 @@ logical function test02() result(stat) allocate (in(3)) ! Sensor nodes. - in = [ node_type('a', 'Test Node A', 'A test node.'), & - node_type('b', 'Test Node B', 'A test node.'), & - node_type('z', 'Test Node Z', 'A test node.') ] + in = [ & + node_type('a', 'Test Node A', 'Description of test node.'), & + node_type('b', 'Test Node B', 'Description of test node.'), & + node_type('z', 'Test Node Z', 'Description of test node.') & + ] print *, 'Opening database "' // DB_OBSERV // '" ...' rc = dm_db_open(db, DB_OBSERV) @@ -575,7 +576,7 @@ logical function test08() result(stat) if (dm_file_exists(DB_LOG)) then call dm_file_delete(DB_LOG) - print *, 'Deleted old database' + print *, 'Deleted stale database' end if print *, 'Creating database "' // DB_LOG // '" ...' @@ -730,7 +731,7 @@ logical function test12() result(stat) if (dm_file_exists(DB_OBSERV_BACKUP)) then call dm_file_delete(DB_OBSERV_BACKUP) - print *, 'Deleted old database' + print *, 'Deleted stale database' end if print *, 'Opening database "' // DB_OBSERV // '" ...' @@ -763,7 +764,7 @@ logical function test13() result(stat) if (dm_file_exists(DB_OBSERV_VACUUM)) then call dm_file_delete(DB_OBSERV_VACUUM) - print *, 'Deleted old database' + print *, 'Deleted stale database' end if print *, 'Opening database "' // DB_OBSERV // '" ...' @@ -793,7 +794,7 @@ logical function test14() result(stat) if (dm_file_exists(DB_BEAT)) then call dm_file_delete(DB_BEAT) - print *, 'Deleted old database' + print *, 'Deleted stale database' end if print *, 'Creating database "' // DB_BEAT // '" ...' @@ -1063,7 +1064,7 @@ subroutine backup_handler(remaining, page_count) integer, intent(in) :: remaining integer, intent(in) :: page_count - print '("Progress: ", f5.1, " %")', 100.0 * (page_count - remaining) / page_count + print '(" *** Progress: ", f5.1, " %")', 100.0 * (page_count - remaining) / page_count end subroutine backup_handler subroutine log_handler(client_data, err_code, err_msg_ptr) bind(c) diff --git a/test/dmtestlog.f90 b/test/dmtestlog.f90 index ac37706..770209b 100644 --- a/test/dmtestlog.f90 +++ b/test/dmtestlog.f90 @@ -26,12 +26,13 @@ logical function test01() result(stat) print *, 'Testing utility functions ...' if (dm_log_level_from_name('abc') /= LL_NONE) return - if (dm_log_level_from_name('NONE ') /= LL_NONE) return - if (dm_log_level_from_name('DEBUG ') /= LL_DEBUG) return - if (dm_log_level_from_name('INFO ') /= LL_INFO) return + if (dm_log_level_from_name('NONE') /= LL_NONE) return + if (dm_log_level_from_name('DEBUG') /= LL_DEBUG) return + if (dm_log_level_from_name('INFO') /= LL_INFO) return if (dm_log_level_from_name('WARNING ') /= LL_WARNING) return - if (dm_log_level_from_name('ERROR ') /= LL_ERROR) return + if (dm_log_level_from_name('ERROR') /= LL_ERROR) return if (dm_log_level_from_name('CRITICAL') /= LL_CRITICAL) return + if (dm_log_level_from_name('USER') /= LL_USER) return if (dm_log_valid(LL_NONE)) return @@ -40,6 +41,7 @@ logical function test01() result(stat) if (.not. dm_log_valid(LL_WARNING)) return if (.not. dm_log_valid(LL_ERROR)) return if (.not. dm_log_valid(LL_CRITICAL)) return + if (.not. dm_log_valid(LL_USER)) return stat = TEST_PASSED end function test01 diff --git a/test/dmtestlogger.f90 b/test/dmtestlogger.f90 index 67e02b8..9f158ae 100644 --- a/test/dmtestlogger.f90 +++ b/test/dmtestlogger.f90 @@ -21,6 +21,8 @@ program dmtestlogger call dm_test_run(TEST_NAME, tests, stats, dm_env_has('NO_COLOR')) contains logical function test01() result(stat) + !! Sends and receives log message through POSIX message queue. + !! Validates serialisation to JSON. character(len=*), parameter :: JSON = & '{"id":"f5ec2dd3870a47b5be3ae397552706fe","level":4,"error":2,"timestamp":' // & '"1970-01-01T00:00:00.000000+00:00","node_id":"test-node","sensor_id":"test-sensor",' // &