diff --git a/.gitignore b/.gitignore index 4820a171..0f49e032 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ tools/esp8266/html/h/* /tools/nano/AhoyUL/_libs2/HoymilesMAC.py /tools/nano/AhoyUL/myTest_app.py_old.py /tools/nano/AhoyUL/myTest_app.py +tools/nano/AhoyUL/FHEM/34_mbStamp02.pm diff --git a/tools/nano/AhoyUL/FHEM/34_ahoyUL.pm b/tools/nano/AhoyUL/FHEM/34_ahoyUL.pm new file mode 100644 index 00000000..79be9bd2 --- /dev/null +++ b/tools/nano/AhoyUL/FHEM/34_ahoyUL.pm @@ -0,0 +1,186 @@ + +package main; + +use strict; +use warnings; +use DevIo; # load DevIo.pm if not already loaded + +# called upon loading the module MY_MODULE +sub AHOYUL_Initialize($) +{ + my ($hash) = @_; + + $hash->{DefFn} = "ahoyUL_Define"; + $hash->{UndefFn} = "ahoyUL_Undef"; + $hash->{SetFn} = "ahoyUL_Set"; + $hash->{ReadFn} = "ahoyUL_Read"; + $hash->{ReadyFn} = "ahoyUL_Ready"; + + $hash->{ParseFn} = "ahoyUL_Parse"; + +} + +# called when a new definition is created (by hand or from configuration read on FHEM startup) +sub ahoyUL_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t]+", $def); + + my $name = $a[0]; + + # $a[1] is always equals the module name "MY_MODULE" + + # first argument is a serial device (e.g. "/dev/ttyUSB0@57600,8,N,1") + my $dev = $a[2]; + + return "no device given" unless($dev); + + # close connection if maybe open (on definition modify) + DevIo_CloseDev($hash) if(DevIo_IsOpen($hash)); + + # add a default baud rate (9600), if not given by user + $dev .= '@57600,8,N,1' if(not $dev =~ m/\@\d+$/); + + # set the device to open + $hash->{DeviceName} = $dev; + + # open connection with custom init function + my $ret = DevIo_OpenDev($hash, 0, "ahoyUL_Init"); + + return undef; +} + +# called when definition is undefined +# (config reload, shutdown or delete of definition) +sub ahoyUL_Undef($$) +{ + my ($hash, $name) = @_; + + # close the connection + DevIo_CloseDev($hash); + + return undef; +} + +# called repeatedly if device disappeared +sub ahoyUL_Ready($) +{ + my ($hash) = @_; + + # try to reopen the connection in case the connection is lost + return DevIo_OpenDev($hash, 1, "ahoyUL_Init"); +} + +# called when data was received +sub ahoyUL_Read($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + # read the available data + my $buf = DevIo_SimpleRead($hash); + # stop processing if no data is available (device disconnected) + return if(!defined($buf)); + #Log3 $name, 5, "ahoyUL ($name) - received: $buf"; + + my $pandata = $hash->{PARTIAL}; + Log3 $name, 5, "ahoyUL/RAW: $pandata + $buf"; + $pandata .= $buf; + + while ( $pandata =~ m/\n/ ) { # while-loop as long as "\n" in $pandata + my $rmsg; + ( $rmsg, $pandata ) = split( "\n", $pandata, 2 ); + $rmsg =~ s/\r//; # substitution, replace "\r" by nothing + ahoyUL_Parse( $hash, $hash, $name, $rmsg ) if ($rmsg); + } + $hash->{PARTIAL} = $pandata; +} + + +# called when one line of data was received +sub ahoyUL_Parse($$$$) +{ + my ( $hash, $iohash, $name, $rmsg) = @_; + Log3 $name, 3, "ahoyUL: $rmsg"; + + if($rmsg =~ m/rMAC/) { + # handle rmac responses + + + } elsif($rmsg =~ m/payload/) { + # payload + + + } elsif($rmsg =~ m/ch00/) { + # AC channel + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, "myPV1", $rmsg , 1); + readingsEndUpdate($hash, 1); + + } elsif($rmsg =~ m/ch0[1-4]/) { + # one DC channel + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, "myPV1", $rmsg , 1); + readingsEndUpdate($hash, 1); + } + +} + + + +# called if set command is executed +sub ahoyUL_Set($$@) +{ + my ($hash, $name, @params) = @_; + #my @a = split("[ \t]+", @params); + + return "ahoyul_set needs at least a command" if(@params < 1); + my $cmd = $params[0]; + + my $usage = "unknown argument $cmd, choose one of a:c:d:iadd:idel:ilst:sMAC:?"; + + # get command overview from ahoy-nano device + if($cmd eq "?") + { + DevIo_SimpleWrite($hash, "$cmd\r\n", 2); + } + elsif($cmd eq "a") + { + DevIo_SimpleWrite($hash, "a:{$params[1]}:{$params[2]}:{$params[3]}:\r\n", 2); + } + elsif($cmd eq "c") + { + DevIo_SimpleWrite($hash, "c:{$params[1]}:\r\n", 2); + } + elsif($cmd eq "d") + { + DevIo_SimpleWrite($hash, "d:{$params[1]}:\r\n", 2); + } + elsif($cmd eq "i") + { + #todo + #DevIo_SimpleWrite($hash, "off\r\n", 2); + } + elsif($cmd eq "sMAC") + { + #todo + DevIo_SimpleWrite($hash, "$cmd\r\n", 2); + } + else + { + return $usage; + } +} + +# will be executed upon successful connection establishment (see DevIo_OpenDev()) +sub ahoyUL_Init($) +{ + my ($hash) = @_; + + # send init to device, here e.g. enable automode to send DevInfoReq (0x15 ... 0x0B ....) every 120sec and enable simple decoding in ahoy-nano + DevIo_SimpleWrite($hash, "a120:::::d1:\r\n", 2); + + return undef; +} + +1; \ No newline at end of file diff --git a/tools/nano/AhoyUL/FHEM/ahoyUL.pm b/tools/nano/AhoyUL/FHEM/ahoyUL.pm deleted file mode 100644 index 1b475c8e..00000000 --- a/tools/nano/AhoyUL/FHEM/ahoyUL.pm +++ /dev/null @@ -1,151 +0,0 @@ - -package main; - -use strict; -use warnings; -use DevIo; # load DevIo.pm if not already loaded - -# called upon loading the module MY_MODULE -sub AHOYUL_Initialize($) -{ - my ($hash) = @_; - - $hash->{DefFn} = "AHOYUL_Define"; - $hash->{UndefFn} = "AHOYUL_Undef"; - $hash->{SetFn} = "AHOYUL_Set"; - $hash->{ReadFn} = "AHOYUL_Read"; - $hash->{ReadyFn} = "AHOYUL_Ready"; - - $hash->{ParseFn} = "AHOYUL_Parse"; - -} - -# called when a new definition is created (by hand or from configuration read on FHEM startup) -sub AHOYUL_Define($$) -{ - my ($hash, $def) = @_; - my @a = split("[ \t]+", $def); - - my $name = $a[0]; - - # $a[1] is always equals the module name "MY_MODULE" - - # first argument is a serial device (e.g. "/dev/ttyUSB0@57600,8,N,1") - my $dev = $a[2]; - - return "no device given" unless($dev); - - # close connection if maybe open (on definition modify) - DevIo_CloseDev($hash) if(DevIo_IsOpen($hash)); - - # add a default baud rate (9600), if not given by user - $dev .= '@57600,8,N,1' if(not $dev =~ m/\@\d+$/); - - # set the device to open - $hash->{DeviceName} = $dev; - - # open connection with custom init function - my $ret = DevIo_OpenDev($hash, 0, "AHOYUL_Init"); - - return undef; -} - -# called when definition is undefined -# (config reload, shutdown or delete of definition) -sub AHOYUL_Undef($$) -{ - my ($hash, $name) = @_; - - # close the connection - DevIo_CloseDev($hash); - - return undef; -} - -# called repeatedly if device disappeared -sub AHOYUL_Ready($) -{ - my ($hash) = @_; - - # try to reopen the connection in case the connection is lost - return DevIo_OpenDev($hash, 1, "AHOYUL_Init"); -} - -# called when data was received -sub AHOYUL_Read($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - - # read the available data - my $buf = DevIo_SimpleRead($hash); - - # stop processing if no data is available (device disconnected) - return if(!defined($buf)); - - Log3 $name, 5, "AHOYUL ($name) - received: $buf"; - - # - # do something with $buf, e.g. generate readings, send answers via DevIo_SimpleWrite(), ... - # - -} - -# called if set command is executed -sub AHOYUL_Set($$@) -{ - my ($hash, $name, $params) = @_; - my @a = split("[ \t]+", $params); - $cmd = $params[0] - - my $usage = "unknown argument $cmd, choose one of statusRequest:noArg on:noArg off:noArg"; - - # get command overview from ahoy-nano device - if($cmd eq "?") - { - #todo - DevIo_SimpleWrite($hash, "?\r\n", 2); - } - elsif($cmd eq "a") - { - #todo handle automode and send command to ahoy-nano via cmd a[[:]:<12 digit inverter id>:] - DevIo_SimpleWrite($hash, "a:{$params[1]}:{$params[2]}:\r\n", 2); - } - elsif($cmd eq "c") - { - #todo - #DevIo_SimpleWrite($hash, "off\r\n", 2); - } - elsif($cmd eq "d") - { - #todo - #DevIo_SimpleWrite($hash, "off\r\n", 2); - } - elsif($cmd eq "i") - { - #todo - #DevIo_SimpleWrite($hash, "off\r\n", 2); - } - elsif($cmd eq "s") - { - #todo - #DevIo_SimpleWrite($hash, "off\r\n", 2); - } - else - { - return $usage; - } -} - -# will be executed upon successful connection establishment (see DevIo_OpenDev()) -sub AHOYUL_Init($) -{ - my ($hash) = @_; - - # send init to device, here e.g. enable automode to send DevInfoReq (0x15 ... 0x0B ....) every 120sec and enable simple decoding in ahoy-nano - DevIo_SimpleWrite($hash, "a120:::::d1:\r\n", 2); - - return undef; -} - -1; \ No newline at end of file diff --git a/tools/nano/AhoyUL/smac_examples.txt b/tools/nano/AhoyUL/smac_examples.txt new file mode 100644 index 00000000..5314ff4b --- /dev/null +++ b/tools/nano/AhoyUL/smac_examples.txt @@ -0,0 +1,33 @@ + +TX Ch40 27B | 15 8180595678563412800B 0063492CEE0000000000 000000E21A88 +TX Ch03 27B | 15 8180595678563412800B 0063492D010000000000 00000013C34E +TX Ch61 27B | 15 8180595678563412800B 0063492D110000000000 000000D30E53 +TX Ch23 27B | 15 8180595678563412800B 0063492D990000000000 000000D508DB +TX Ch40 27B | 15 8180595678563412800B 0063492DAD0000000000 000000146E48 +TX Ch61 27B | 15 8180595678563412800B 0063492DBC0000000000 00000044AEC9 +TX Ch03 27B | 15 8180595678563412800B 0063492DCB0000000000 000000B6ED0F +TX Ch23 27B | 15 8180595678563412800B 0063492DE00000000000 00000047073F +TX Ch75 27B | 15 8180595678563412800B 0063492DF00000000000 00000087CA22 +TX Ch23 27B | 15 8180595678563412800B 0063492DFF0000000000 000000778B9C +TX Ch40 27B | 15 8180595678563412800B 0063492E0E0000000000 000000EC720C +TX Ch61 27B | 15 8180595678563412800B 0063492E1D0000000000 000000DCABF6 +TX Ch03 27B | 15 8180595678563412800B 0063492E2D0000000000 000000DDFF93 +TX Ch40 27B | 15 8180595678563412800B 0063492E3C0000000000 0000008D3F12 + + +# test sequence for sending: +smac:ch03:15 8180595678563412800B 0063492CEE0000000000 000000E21A88:rx40: +smac:Ch23:15 8180595678563412800B 0063492D010000000000 00000013C34E:rx61: +smac:Ch40:15 8180595678563412800B 0063492D110000000000 000000D30E53:rx75: +smac:ch61:15 8180595678563412800B 0063492D990000000000 000000D508DB:rx03: +smac:ch75:15 8180595678563412800B 0063492DAD0000000000 000000146E48:rx23: +smac:ch03:15 8180595678563412800B 0063492DBC0000000000 00000044AEC9:rx40: +smac:Ch23:15 8180595678563412800B 0063492DCB0000000000 000000B6ED0F:rx61: +smac:ch40:15 8180595678563412800B 0063492DE00000000000 00000047073F:rx75: + +smac:ch61:15 8180595678563412800B 0063492DF00000000000 00000087CA22:rx03: +smac:ch75:15 8180595678563412800B 0063492DFF0000000000 000000778B9C:rx23: +smac:ch03:15 8180595678563412800B 0063492E0E0000000000 000000EC720C:rx40: +smac:ch23:15 8180595678563412800B 0063492E1D0000000000 000000DCABF6:rx61: +smac:ch40:15 8180595678563412800B 0063492E2D0000000000 000000DDFF93:rx75: +smac:ch63:15 8180595678563412800B 0063492E3C0000000000 0000008D3F12:rx03: diff --git a/tools/nano/AhoyUL/src/config.h b/tools/nano/AhoyUL/src/config.h index 17c696e1..03f53c42 100644 --- a/tools/nano/AhoyUL/src/config.h +++ b/tools/nano/AhoyUL/src/config.h @@ -8,9 +8,6 @@ #include -//------------------------------------- -// CONFIGURATION - COMPILE TIME -//------------------------------------- #if defined(ESP8266) // for esp8266 environment @@ -26,9 +23,6 @@ #define DEF_RF24_IRQ_PIN (3) #endif -#define DEF_VERSION "\n version 2022-12-01 21:45" - - // default radio ID #define DTU_RADIO_ID ((uint64_t)0x1234567801ULL) diff --git a/tools/nano/AhoyUL/src/main.cpp b/tools/nano/AhoyUL/src/main.cpp index cfd448db..d7a86303 100644 --- a/tools/nano/AhoyUL/src/main.cpp +++ b/tools/nano/AhoyUL/src/main.cpp @@ -11,7 +11,7 @@ // There are two modes of operation: // - automode: one REQUEST message is polled periodically and decoded payload is given by serial-IF (@57600baud), some comfig inputs possible // - mac-mode: -> The hoymiles specific REQUEST messages must be given as input via serial-IF (@57600baud) smac-packet -// <- The full sorted RESPONSE is given to the serial-IF with as rmac-packet (to be used with python, fhem, etc.) +// <- The full sorted RESPONSE is given to the serial-IF as rmac-packet (to be used with python, fhem, etc.) -- todo // #include @@ -82,6 +82,8 @@ static RadioType hmRadio; // static uint8_t radio_id[5]; //todo: use the mPayload[].id field ,this defines the radio-id (domain) of the rf24 transmission, will be derived from inverter id // static uint64_t radio_id64 = 0ULL; + +#define DEF_VERSION "\n version 2022-12-03 11:05" #define P(x) (__FlashStringHelper *)(x) // PROGMEM-Makro for variables static const char COMPILE_DATE[] PROGMEM = {__DATE__}; static const char COMPILE_TIME[] PROGMEM = {__TIME__}; @@ -104,9 +106,10 @@ static uint8_t rxch; // keeps the current RX channel // volatile static uint32_t current_millis = 0; -static volatile uint32_t timer1_millis = 0L; // general loop timer -static volatile uint32_t timer2_millis = 0L; // send Request timer +static volatile uint32_t timer1_millis = 0L; // general loop timer +static volatile uint32_t timer2_millis = 0L; // send Request timer static volatile uint32_t lastRx_millis = 0L; +static volatile uint32_t tcmd_millis = 0L; //timer for smac cmd #define ONE_SECOND (1000L) #define ONE_MINUTE (60L * ONE_SECOND) #define QUARTER_HOUR (15L * ONE_MINUTE) @@ -122,6 +125,7 @@ static bool automode = true; static bool sendNow = false; static bool doDecode = true; static bool showMAC = true; +static bool smac_send = false; //cmd smac was send static volatile uint32_t polling_inv_msec = SEND_INTERVAL_ms; static volatile uint16_t tmp16 = 0; static uint8_t tmp8 = 0; @@ -209,7 +213,7 @@ void loop() { if (Serial.available()) { // wait char inSer = Serial.read(); - delay(2); + //delay(1); switch (inSer) { case (char)'a': { // enable automode with REQ polling interval via a10 => 10sec, a100 => 100sec or other range 5....3600sec @@ -283,22 +287,24 @@ void loop() { tmp8 = utSer.read_uart_cmd_param(mParams); if (tmp8 > 0) { if (strstr(&mParams[0][0], "mac")) { - if (utSer.uart_cmd_smac_request_parser(mParams, tmp8, &rfTX_packet, &rxch)) { + if (utSer.uart_cmd_smac_request_parsing(mParams, tmp8, &rfTX_packet, &rxch)) { if (rxch == 0) { // if rxchannel not given, then set automatically rxch = hmRadio.getRxChannel(rfTX_packet.rfch); } hmRadio.setRxChanIdx(hmRadio.getChanIdx(rxch)); - // compare inv-id from packet data with all registerd inv-id of the payload_t struct array + // compare inv-id from packet data with all registered inv-id of the payload_t struct array m_inv_ix = getInvIX(mPayload, MAX_NUM_INVERTERS, &rfTX_packet.data[0]); if (m_inv_ix == 0xFF) { DPRINT(DBG_DEBUG, F("inv_id no match")); m_inv_ix = MAX_NUM_INVERTERS - 1; //use last possition } DPRINT(DBG_DEBUG, F("m_inv_ix ")); _DPRINT(DBG_DEBUG, m_inv_ix); + tcmd_millis = millis(); + smac_send = true; payload_used[m_inv_ix] = !resetPayload(&mPayload[m_inv_ix]); - mPayload[m_inv_ix].isMACPacket = true; // MAC must be enabled to show the full MAC packet, no need for user_payload only + mPayload[m_inv_ix].isMACPacket = true; // MAC must be enabled to show the full MAC packet, no need for user_payload mPayload[m_inv_ix].receive = false; hmRadio.sendPacket_raw(&mPayload[0].invId[0], &rfTX_packet, rxch); // 2022-10-30: byte array transfer working mPayload[m_inv_ix].requested = true; @@ -307,36 +313,6 @@ void loop() { }//end if(utSer.uart_cmd_smac_request_parser(...)) } // end if(mac) } // end if(tmp8) - - /* - sread_len = utSer.serBlockRead_ms(utSer.mSerBuffer); - if (utSer.eval_uart_smac_request(utSer.mSerBuffer, sread_len, &rfTX_packet, &rxch)) { - // send on Tx channel and receive on Rx channel - if (rxch == 0) { - // if rxchannel not given, then set automatically - rxch = hmRadio.getRxChannel(rfTX_packet.rfch); - } - hmRadio.setRxChanIdx(hmRadio.getChanIdx(rxch)); - - // compare inv-id from packet data with all registerd inv-id of the payload_t struct array - m_inv_ix = getInvIX(mPayload, MAX_NUM_INVERTERS, &rfTX_packet.data[0]); - if (m_inv_ix != 0xFF) { - if (m_inv_ix < MAX_NUM_INVERTERS) { - } - DPRINT(DBG_DEBUG, F("match, m_inv_ix ")); - _DPRINT(DBG_DEBUG, m_inv_ix); - payload_used[m_inv_ix] = !resetPayload(&mPayload[m_inv_ix]); - mPayload[m_inv_ix].isMACPacket = true; // MAC must be enabled to show the full MAC packet, no need for user_payload only - mPayload[m_inv_ix].receive = false; - hmRadio.sendPacket_raw(&mPayload[0].invId[0], &rfTX_packet, rxch); // 2022-10-30: byte array transfer working - mPayload[m_inv_ix].requested = true; - } else { - // no matching inverter, do nothing - m_inv_ix = 0; - } - } // end if - */ - break; } // end case s @@ -469,7 +445,7 @@ void loop() { } // end case t case (char)'?':{ - Serial.print(F("\ncmds: a, c, d, iadd, idel, ilst, m, p, s, rxch, t, ?")); + Serial.print(F("\ncmds: a:, c_, d, iadd:, idel:, ilst:, m_, p:, s, smac:, rxch_, t_, ?")); break; } //end case '?' @@ -565,7 +541,16 @@ void loop() { } } } - } + + } else { + //no receive to smac cmd within timeout (of about 10 retransmissions) --> command_timeout == 4000 + if (smac_send && (millis()-tcmd_millis > 4000)) { + smac_send = false; + //shows number of retransmits, if rt>0 some response, but incomplete + Serial.print(F("rmac:rt")); Serial.print(mPayload[m_inv_ix].retransmits); Serial.println(F(":ERR")); + }//end if() + + } } // end loop() //----------------------------------------------------------------------------- @@ -911,7 +896,7 @@ static bool out_uart_smac_resp(invPayload_t *_payload) { Serial.print(F("\nrMAC:ch")); if (_payload->rxChIdx < 10) Serial.print(F("0")); Serial.print(_payload->rxChIdx); - Serial.print(F(":{")); + Serial.println(F(":{")); for (uint8_t i = 0; i < (_payload->maxPackId); i++) { hmRadio.dumpBuf(NULL, &_payload->data[i][0], _payload->len[i]); if (i != _payload->maxPackId - 1) @@ -921,7 +906,7 @@ static bool out_uart_smac_resp(invPayload_t *_payload) { } // end for() Serial.print(F("rt")); Serial.print(_payload->retransmits); - Serial.print(F(":")); + Serial.println(F(":OK")); return true; } @@ -1020,6 +1005,8 @@ static void decodePayload(uint8_t _cmd, uint8_t *_user_payload, uint8_t _ulen, u // Serial.print(F("not yet")); } } // end for() + Serial.println(); + } else { Serial.print(F("NO DECODER ")); Serial.print(_cmd, HEX); diff --git a/tools/nano/AhoyUL/src/utils_serial.h b/tools/nano/AhoyUL/src/utils_serial.h index afc05a99..3df2fb34 100644 --- a/tools/nano/AhoyUL/src/utils_serial.h +++ b/tools/nano/AhoyUL/src/utils_serial.h @@ -330,12 +330,12 @@ class SerialUtils { * * parses the uart smac command parameter and writes into the mac-packet sending structure */ - static boolean uart_cmd_smac_request_parser(char **_cmd_params, uint8_t _clen, packet_t *packet, uint8_t *rxch) { + boolean uart_cmd_smac_request_parsing(char **_cmd_params, uint8_t _numPara, packet_t *packet, uint8_t *rxch) { //example cmd "smac:ch03:958180....:rc40:" - if (_clen < 2) { - DPRINT(DBG_ERROR, F("clen low")); - _DPRINT(DBG_ERROR, _clen); + if (_numPara < 2) { + DPRINT(DBG_ERROR, F(" numPara low")); + _DPRINT(DBG_ERROR, _numPara); return false; } @@ -355,10 +355,15 @@ class SerialUtils { packet->data[_i / 2] = (uint8_t)x2b(&_cmd_params[2][_i]); } // end for() packet->plen = _i / 2; + + if (DBG_DEBUG <= DEBUG_LEVEL) { + Serial.print(F("\ndata ")); + print_bytes( packet->data, packet->plen, "", true); + } // parse rx channel input *rxch = 0; - if (_clen == 3) { + if (_numPara == 3) { m = strstr(_cmd_params[3], "rc"); if(m) { m += 2; @@ -366,8 +371,7 @@ class SerialUtils { } // end if(m) } // end if() - DPRINT(DBG_INFO, F("rxch ")); - _DPRINT(DBG_DEBUG, *rxch); + DPRINT(DBG_DEBUG, F("rxch ")); _DPRINT(DBG_DEBUG, *rxch); return true; }// end uart_cmd_smac_request_parser() @@ -413,8 +417,9 @@ class SerialUtils { invID5[i] = strtol(strtmp5, NULL, 16); // convert 2char to one byte } - DPRINT(DBG_INFO, F(" InvID5 ")); - if (DBG_INFO) { + + if (DBG_INFO <= DEBUG_LEVEL) { + DPRINT(DBG_INFO, F(" InvID5 ")); print_bytes(&invID5[0], 5, "", true); } _res = true; @@ -456,8 +461,10 @@ class SerialUtils { return _res; } // end uart_inverter_del_check() - //////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // output functions + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * *