void mpipedrv_tx(ot_bool blocking, mpipe_priority data_priority) { /// @note Using blocking: OpenTag currently does not implement blocking TX, /// because it can interfere with time-critical radio processes. You can /// achieve a similar affect by calling "mpipedrv_wait()" after a logging /// function call, if you need blocking on certain transmissions. if (mpipe.state == MPIPE_Idle) { ot_u8* data; ot_u16 scratch; mpipe.state = MPIPE_Tx_Done; q_writeshort(mpipe.alp.outq, tty.seq.ushort); // Sequence Number ///@todo remove CRC part, not necessary for USB. /// Requires coordination with OTcom, though: need to add a checkbox scratch = mpipe.alp.outq->putcursor - mpipe.alp.outq->getcursor; //data length scratch = crc16drv_block(mpipe.alp.outq->getcursor, scratch); //CRC value q_writeshort(mpipe.alp.outq, scratch); //Put CRC scratch = mpipe.alp.outq->putcursor \ - mpipe.alp.outq->getcursor; //data length w/ CRC data = mpipe.alp.outq->getcursor; //data start mpipe.alp.outq->getcursor = mpipe.alp.outq->putcursor; //move queue past packet usbcdc_txdata(data, scratch, CDC0_INTFNUM); // Wait for the USB transmission to complete (optional). if (blocking) { mpipedrv_wait(); } } }
OT_WEAK void alp_stream_dialog_tmpl(ot_queue* out_q, void* data_type) { if _PTR_TEST(data_type) { q_writeshort(out_q, ((dialog_tmpl*)data_type)->timeout); q_writeshort(out_q, ((dialog_tmpl*)data_type)->channels); q_writestring(out_q, ((dialog_tmpl*)data_type)->chanlist, ((dialog_tmpl*)data_type)->channels); } }
ot_int sub_fileheaders( alp_tmpl* alp, id_tmpl* user_id, ot_u8 respond, ot_u8 cmd_in, ot_int data_in ) { ot_int data_out = 0; vlBLOCK file_block = (vlBLOCK)((cmd_in >> 4) & 0x07); /// Only run if respond bit is set! if (respond) { while ((data_in > 0) && sub_qnotfull(respond, 6, alp->outq)) { vaddr header; ot_bool allow_output = True; data_in--; // one for the file id allow_output = (ot_bool)(vl_getheader_vaddr(&header, file_block, \ q_readbyte(alp->inq), VL_ACCESS_R, NULL) == 0); if (allow_output) { q_writeshort_be(alp->outq, vworm_read(header + 4)); // id & mod q_writeshort(alp->outq, vworm_read(header + 0)); // length q_writeshort(alp->outq, vworm_read(header + 2)); // alloc data_out += 6; } } //alp->BOOKMARK_IN = (void*)sub_testchunk(data_in); } return data_out; }
OT_WEAK void alp_stream_queue(ot_queue* out_q, void* data_type) { if _PTR_TEST(data_type) { ot_int length; length = q_length((ot_queue*)data_type); q_writeshort(out_q, ((ot_queue*)data_type)->alloc); q_writeshort(out_q, ((ot_queue*)data_type)->options.ushort); q_writeshort(out_q, length); q_writestring(out_q, ((ot_queue*)data_type)->front, length); } }
void sub_build_uhfmsg(ot_int* buffer) { /// This is the routine that builds the DASH7 UDP generic protocol message. /// The protocol has data elements marked by a letter (T, V, R, E, D) that /// signify Temperature, Voltage, RSSI (LF), PaLFi wake Event, and RX Data. /// The elements are fixed/known length. command_tmpl c_tmpl; ot_u8* data_start; ot_u8 status; // Broadcast request (takes no 2nd argument) otapi_open_request(ADDR_broadcast, NULL); // Insert Transport-Layer headers c_tmpl.type = CMDTYPE_na2p_request; c_tmpl.opcode = CMD_udp_on_file; c_tmpl.extension= CMDEXT_no_response; otapi_put_command_tmpl(&status, &c_tmpl); otapi_put_dialog_tmpl(&status, NULL); // NULL = defaults // UDP Header q_writebyte(&txq, 255); // Source Port: 255 (custom application port) q_writebyte(&txq, 255); // Destination Port (same value) data_start = txq.putcursor; // Place temperature data q_writebyte(&txq, 'T'); q_writeshort(&txq, buffer[0]); // Place Voltage data q_writebyte(&txq, 'V'); q_writeshort(&txq, buffer[1]); // Place RSSI data q_writebyte(&txq, 'R'); q_writeshort(&txq, radio.last_rssi); // Store this information into the Port 255 file for continuous, automated // reporting by DASH7/OpenTag until it is updated next time. The length of // this information is always 6 bytes. { vlFILE* fp; fp = ISF_open_su(255); if (fp != NULL) { vl_store(fp, 6, data_start); vl_close(fp); } } // Finish Message otapi_close_request(); }
void mpipe_txndef(ot_u8* data, ot_bool blocking, mpipe_priority data_priority) { /// Data TX will only occur if this function is called when the MPipe state is /// idle. The exception is when the function is called with ACK priority, in /// which case the state doesn't need to be Idle. Lastly, if you specify the /// blocking parameter, the function will not return until the packet is /// completely transmitted. ot_int data_length; #if (MPIPE_USE_ACKS) if (data_priority == MPIPE_Ack)) { mpipe.priority = data_priority; goto mpipe_txndef_SETUP; } #endif if (mpipe.state == MPIPE_Idle) { mpipe.state = MPIPE_Tx_Wait; mpipe_txndef_SETUP: MPIPE_DMAEN(OFF); # if (MPIPE_DMANUM == 0) DMA->CTL0 |= MPIPE_UART_TXTRIG; # elif (MPIPE_DMANUM == 1) DMA->CTL0 |= (MPIPE_UART_TXTRIG << 8); # elif (MPIPE_DMANUM == 2) DMA->CTL1 = MPIPE_UART_TXTRIG; # endif DMA->CTL4 = ( DMA_Options_RMWDisable | \ DMA_Options_RoundRobinDisable | \ DMA_Options_ENMIEnable ); // Sequence Number q_writeshort(mpipe_alp.outq, mpipe.sequence.ushort); // Data alignment data = mpipe_alp.outq->getcursor; data_length = mpipe_alp.outq->putcursor \ - mpipe_alp.outq->getcursor; // CRC q_writeshort(mpipe_alp.outq, platform_crc_block(data, data_length)); data_length += 2; mpipe_alp.outq->getcursor = mpipe_alp.outq->putcursor; MPIPE_DMA_TXCONFIG(data, data_length, ON); UART_OPEN(); MPIPE_DMA_TXTRIGGER(); if (blocking) { mpipe_wait(); } } }
void mpipe_txndef(ot_u8* data, ot_bool blocking, mpipe_priority data_priority) { /// Data TX will only occur if this function is called when the MPipe state is /// idle. The exception is when the function is called with ACK priority, in /// which case the state doesn't need to be Idle. Lastly, if you specify the /// blocking parameter, the function will not return until the packet is /// completely transmitted. ot_u16 scratch; #if (MPIPE_USE_ACKS) if (data_priority == MPIPE_Ack)) { mpipe.priority = data_priority; goto mpipe_txndef_SETUP; } #endif if (mpipe.state == MPIPE_Idle) { mpipe.state = MPIPE_Tx_Done; //MPIPE_Tx_Wait; mpipe_txndef_SETUP: MPIPE_DMAEN(OFF); # if (MPIPE_DMANUM == 0) DMA->CTL0 |= MPIPE_UART_TXTRIG; # elif (MPIPE_DMANUM == 1) DMA->CTL0 |= (MPIPE_UART_TXTRIG << 8); # elif (MPIPE_DMANUM == 2) DMA->CTL1 = MPIPE_UART_TXTRIG; # endif q_writeshort(mpipe_alp.outq, mpipe.sequence.ushort); // Sequence Number scratch = mpipe_alp.outq->putcursor - mpipe_alp.outq->getcursor; //data length scratch = platform_crc_block(mpipe_alp.outq->getcursor, scratch); //CRC value q_writeshort(mpipe_alp.outq, scratch); //Put CRC scratch = mpipe_alp.outq->putcursor \ - mpipe_alp.outq->getcursor; //data length w/ CRC data = mpipe_alp.outq->getcursor; //data start mpipe_alp.outq->getcursor = mpipe_alp.outq->putcursor; //move queue past packet // DMA setup MPIPE_DMA_TXCONFIG(data, scratch, ON); UART_OPEN(); MPIPE_DMA_TXTRIGGER(); if (blocking) { mpipe_wait(); } } }
OT_WEAK void alp_stream_isfcomp_tmpl(ot_queue* out_q, void* data_type) { if _PTR_TEST(data_type) { q_writebyte(out_q, ((isfcomp_tmpl*)data_type)->is_series); q_writebyte(out_q, ((isfcomp_tmpl*)data_type)->isf_id); q_writeshort(out_q, ((isfcomp_tmpl*)data_type)->offset); } }
OT_WEAK void alp_stream_udp_tmpl(ot_queue* out_q, void* data_type) { if _PTR_TEST(data_type) { q_writeshort(out_q, ((udp_tmpl*)data_type)->data_length); q_writebyte(out_q, ((udp_tmpl*)data_type)->dst_port); q_writebyte(out_q, ((udp_tmpl*)data_type)->src_port); q_writestring(out_q, ((udp_tmpl*)data_type)->data, ((udp_tmpl*)data_type)->data_length); } }
void m2advp_open(m2session* session) { q_start(&txq, 1, 0); txq.front[0] = 7; q_writebyte(&txq, session->subnet); q_writebyte(&txq, 0xF0); q_writebyte(&txq, session->channel); q_writeshort(&txq, session->counter); }
OT_WEAK void sub_put_isf_offset(ot_u8 is_series, ot_u16 offset) { if (is_series) { q_writeshort(&txq, offset); } else { q_writebyte(&txq, (ot_u8)offset); } }
ot_bool m2qp_sig_udp(ot_u8 srcport, ot_u8 dstport, id_tmpl* user_id) { static const char* label[] = { "PongID: ", ", RSSI: ", ", Link: " }; ot_u16 pongval; ot_u8 i; ot_u8 scratch; //1. Read the PONG VAL pongval = q_readshort(&rxq); // Request: Copy PING VAL to PONG if (dstport == 254) { q_writeshort(&txq, pongval); return True; } # if defined(BOARD_eZ430Chronos) // Chronos doesn't have a normal MPipe, so print-out responses on the LCD # else // Response: Compare PING Val to PONG Val and write output to MPipe if ((dstport == 255) && (app.pingval == pongval)) { // Prepare logging header: UTF8 (text log) is subcode 1, dummy length is 0 otapi_log_header(1, 0); // Print out the three parameters for PongLT, one at a time. // If you are new to OpenTag, this is a common example of a state- // based code structure JP likes to use. i = 0; while (1) { q_writestring(mpipe.alp.outq, (ot_u8*)label[i], 8); switch (i++) { case 0: scratch = otutils_bin2hex( mpipe.alp.outq->putcursor, user_id->value, user_id->length ); break; case 1: scratch = otutils_int2dec(mpipe.alp.outq->putcursor, radio.last_rssi); break; case 2: scratch = otutils_int2dec(mpipe.alp.outq->putcursor, dll.last_nrssi); break; case 3: goto m2qp_sig_udp_PRINTDONE; } mpipe.alp.outq->putcursor += scratch; mpipe.alp.outq->length += scratch; } // Close the log file, send it out, return success m2qp_sig_udp_PRINTDONE: otapi_log_direct(); return True; } # endif return False; }
void applet_send_query(m2session* session) { /// The C-API for building commands can be bypassed in favor of directly /// putting data to the queue. That way is more efficient, but it also requires /// you to know more about DASH7 than just what order the templates should be. /// /// The query that we build will collect sensor configuration data back from /// all devices that support the sensor protocol. Much more interesting queries /// are possible. ot_u8 status; { //open request for single hop anycast query routing_tmpl routing; routing.hop_code = 0; otapi_open_request(ADDR_anycast, &routing); } { //use a command template for collection of single file from single file search command_tmpl command; command.opcode = (ot_u8)CMD_udp_on_file; command.type = (ot_u8)CMDTYPE_na2p_request; command.extension = (ot_u8)CMDEXT_none; otapi_put_command_tmpl(&status, &command); } { //write the dialog information (timeout, channels to use) dialog_tmpl dialog; dialog.channels = 0; //use same channel as request for response dialog.timeout = 0x41; //same as otutils_encode_timeout(512) -- 512 tick response slot otapi_put_dialog_tmpl(&status, &dialog); } { //write the query to search for the sensor protocol id static const ot_u8 query_str[10] = "APP=PongLT"; query_tmpl query; query.code = M2QC_COR_SEARCH + 10; // do a 100% length=10 correlation search query.mask = NULL; // don't do any masking (no partial matching) query.length = 10; // query_str is 10 bytes query.value = (ot_u8*)query_str; otapi_put_query_tmpl(&status, &query); } { //put in the information of the file to search (the user id) isfcomp_tmpl isfcomp; isfcomp.is_series = False; isfcomp.isf_id = ISF_ID(user_id); isfcomp.offset = 0; otapi_put_isf_comp(&status, &isfcomp); } { //put in UDP ports (from 254 to 255) and Ping ID q_writebyte(&txq, 254); q_writebyte(&txq, 255); q_writeshort(&txq, app.pingval); } //Done building command, close the request and send the dialog otapi_close_request(); }
void q_writeshort_be(ot_queue* q, uint16_t short_in) { # ifdef __BIG_ENDIAN__ q_writeshort(q, short_in); # else uint8_t* data; data = (uint8_t*)&short_in; *q->putcursor++ = data[0]; *q->putcursor++ = data[1]; //#q->length += 2; # endif }
OT_WEAK ot_bool alp_load_retval(alp_tmpl* alp, ot_u16 retval) { /// This function is for writing a two-byte integer to the return record. It /// is useful for some types of API return sequences ot_bool respond = (ot_bool)(alp->OUTREC(CMD) & 0x80); if (respond) { //alp->OUTREC(FLAGS) &= ~ALP_FLAG_CF; alp->OUTREC(PLEN) = 2; alp->OUTREC(CMD) |= 0x40; q_writeshort(alp->outq, retval); } return respond; }
void q_writeshort_be(ot_queue* q, ot_uint short_in) { # ifdef __BIG_ENDIAN__ q_writeshort(q, short_in); # else ot_u8* data; data = (ot_u8*)&short_in; q->putcursor[0] = data[0]; q->putcursor[1] = data[1]; q->putcursor += 2; q->length += 2; # endif }
OT_WEAK ot_bool alp_proc_api_query(alp_tmpl* alp, id_tmpl* user_id ) { /// The M2QP API calls follow the rules that future extensions to the API shall /// abide, apart from special cases which *must* be cleared by the developer /// community prior to becoming official. /// The form is: ot_u16 otapi_function(ot_u8*, void*) static const ot_u8 argmap[OTAPI_M2QP_FUNCTIONS] = \ { 6, 9, 10, 11, 12, 13, 14, 14, 15, 0, 0 }; //sub_bdtmpl get_tmpl; ot_u8 dt_buf[24]; // 24 bytes is a safe amount, although less might suffice ot_u16 txq_len; ot_u8 status = alp->inq->getcursor[3]; // record cmd ot_u8 lookup_cmd = (status & ~0x80) - 1; ot_bool respond = (ot_bool)(status & 0x80); alp->inq->getcursor += 4; if ((lookup_cmd < OTAPI_M2QP_FUNCTIONS) && auth_isroot(user_id)) { /// Load template from ALP dir cmd into C datatype bdtmpl_cmd[argmap[lookup_cmd]](alp->inq, (void*)dt_buf); /// Run ALP command, using input template txq_len = m2qp_cmd[lookup_cmd](&status, (void*)dt_buf); /// Response to ALP query command includes three bytes: /// byte 1 - status (0 is error) /// bytes 2 & 3 - 16 bit integer, length of TXQ if (respond) { //alp->outrec.flags &= ~ALP_FLAG_CF; //alp->outrec.cmd |= 0x40; //alp->outrec.plength = 3; alp->OUTREC(FLAGS) &= ~ALP_FLAG_CF; alp->OUTREC(PLEN) = 3; alp->OUTREC(CMD) |= 0x40; q_writebyte(alp->outq, status); q_writeshort(alp->outq, txq_len); } } return True; }
int sub_getdecnum(int* status, FILE* stream, ot_queue* msg) { int digits; char next; char buf[16]; int sign = 1; int force_u = 0; int number = 0; int i = 0; int size = 0; // Buffer until whitespace or ')' delimiter digits = sub_buffernum(status, stream, buf, 15); // Deal with leading minus sign if (buf[i] == '-') { sign = -1; i++; } // Go through the digits & footer // - load in numerical value, one digit at a time // - also look for the type footer: ul, us, uc, u, l, s, c, or none while (i < digits) { if ((buf[i] >= '0') && (buf[i] <= '9')) { number *= 10; number += (buf[i++] - '0'); } else { force_u = (buf[i] == 'u'); i += force_u; if (buf[i] == 'c') size = 1; // c: char (1 byte) else if (buf[i] == 's') size = 2; // s: short (2 bytes) else if (buf[i] == 'l') size = 3; // l: long (4 bytes) break; } } // Determine size in case where footer is not explicitly provided if (size == 0) { int j; int bound[] = {128, 256, 32768, 65536, 0, 0}; int max = number - (sign < 0); for (j=force_u, size=1; ; j+=2, size++) { if ((bound[j]==0) || (bound[j]>=max)) break; } } number *= sign; switch (size & 3) { case 0: case 1: q_writebyte(msg, (ot_u8)number); break; case 2: q_writeshort(msg, (ot_u16)number); break; case 3: size = 4; q_writelong(msg, (ot_u32)number); break; } return size; }
OT_WEAK void alp_stream_advert_tmpl(ot_queue* out_q, void* data_type) { if _PTR_TEST(data_type) { q_writestring(out_q, (ot_u8*)data_type, 4); q_writeshort(out_q, ((advert_tmpl*)data_type)->duration); } }
OT_WEAK void alp_stream_isfcall_tmpl(ot_queue* out_q, void* data_type) { if _PTR_TEST(data_type) { alp_breakdown_isfcomp_tmpl(out_q, data_type); q_writeshort(out_q, ((isfcall_tmpl*)data_type)->max_return); } }
ot_int sub_filedata( alp_tmpl* alp, id_tmpl* user_id, ot_u8 respond, ot_u8 cmd_in, ot_int data_in ) { vlFILE* fp; ot_u16 offset; ot_u16 span; ot_int data_out = 0; ot_bool inc_header = (ot_bool)((cmd_in & 0x0F) == 0x0C); vlBLOCK file_block = (vlBLOCK)((cmd_in >> 4) & 0x07); ot_u8 file_mod = ((cmd_in & 0x02) ? VL_ACCESS_W : VL_ACCESS_R); ot_queue* inq = alp->inq; ot_queue* outq = alp->outq; sub_filedata_TOP: while (data_in > 0) { vaddr header; ot_u8 err_code; ot_u8 file_id; ot_u16 limit; //alp->BOOKMARK_IN = inq->getcursor; //alp->BOOKMARK_OUT = NULL; file_id = q_readbyte(inq); offset = q_readshort(inq); span = q_readshort(inq); limit = offset + span; err_code = vl_getheader_vaddr(&header, file_block, file_id, file_mod, user_id); file_mod = ((file_mod & VL_ACCESS_W) != 0); //fp = NULL; // A. File error catcher Stage // (In this case, gotos make it more readable) /// Make sure file header was retrieved properly, or goto error if (err_code != 0) { goto sub_filedata_senderror; } /// Make sure file opens properly, or goto error fp = vl_open_file(header); if (fp == NULL) { err_code = 0xFF; goto sub_filedata_senderror; } /// Make sure offset is within file bounds, or goto error if (offset >= fp->alloc) { err_code = 0x07; goto sub_filedata_senderror; } if (limit > fp->alloc) { limit = fp->alloc; err_code = 0x08; } // B. File Writing or Reading Stage // Write to file // 1. Process error on bad ALP parameters, but still do partial write // 2. offset, span are adjusted to convey leftover data // 3. miscellaneous write error occurs when vl_write fails if (file_mod) { for (; offset<limit; offset+=2, span-=2, data_in-=2) { if (inq->getcursor >= inq->back) { goto sub_filedata_overrun; } err_code |= vl_write(fp, offset, q_readshort_be(inq)); } } // Read from File // 1. No error for bad read parameter, just fix the limit // 2. If inc_header param is set, include the file header in output // 3. Read out file data else { ot_u8 overhead; //limit = (limit > fp->length) ? fp->length : limit; overhead = 6; overhead += (inc_header != 0) << 2; if ((outq->putcursor+overhead) >= outq->back) { goto sub_filedata_overrun; } q_writeshort_be(outq, vworm_read(header + 4)); // id & mod if (inc_header) { q_writeshort(outq, vworm_read(header + 0)); // length q_writeshort(outq, vworm_read(header + 2)); // alloc data_out += 4; } q_writeshort(outq, offset); q_writeshort(outq, span); data_out += 6; for (; offset<limit; offset+=2, span-=2, data_out+=2) { if ((outq->putcursor+2) >= outq->back) { goto sub_filedata_overrun; } q_writeshort_be(outq, vl_read(fp, offset)); } } // C. Error Sending Stage sub_filedata_senderror: if ((respond != 0) && (err_code | file_mod)) { if ((outq->putcursor+2) >= outq->back) { goto sub_filedata_overrun; } q_writebyte(outq, file_id); q_writebyte(outq, err_code); q_markbyte(inq, span); // go past any leftover input data data_out += 2; } data_in -= 5; // 5 bytes input header vl_close(fp); } // Total Completion: // Set bookmark to NULL, because the record was completely processed //alp->BOOKMARK_IN = NULL; return data_out; // Partial or Non Completion: // Reconfigure last ALP operation, because it was not completely processed ///@todo Bookmarking is obsolete, because the way Chunking is done has /// been revised. Chunked records must be contiguous. ALP-Main will not /// call this app, and thus not call this function, until the message-end /// bit is detected, therefore meaning that all data is received and /// contiguous. This overrun block, thus, should only check the flags for /// chunking, bypass them, and loop back to the top of this function. sub_filedata_overrun: vl_close(fp); ///@todo alp_next_chunk(alp); // { // ot_u8* scratch; // inq->getcursor = (ot_u8*)alp->BOOKMARK_IN; // scratch = inq->getcursor + 1; // *scratch++ = ((ot_u8*)&offset)[UPPER]; // *scratch++ = ((ot_u8*)&offset)[LOWER]; // *scratch++ = ((ot_u8*)&span)[UPPER]; // *scratch = ((ot_u8*)&span)[LOWER]; // } return data_out; }
void sub_build_uhfmsg(ot_int* buffer) { /// This is the routine that builds the DASH7 UDP generic protocol message. /// The protocol has data elements marked by a letter (T, V, R, E, D) that /// signify Temperature, Voltage, RSSI (LF), PaLFi wake Event, and RX Data. /// The elements are fixed/known length. session_tmpl s_tmpl; command_tmpl c_tmpl; ot_u8* data_start; ot_u8 status; // Create a new session: you could change these parameters // Use "CHAN1" for odd events, "CHAN2" for even events s_tmpl.channel = (palfi.wake_event & 1) ? ALERT_CHAN1 : ALERT_CHAN2; s_tmpl.subnetmask = 0; // Use default subnet s_tmpl.flagmask = 0; // Use default app-flags s_tmpl.timeout = 10; // Do CSMA for no more than 10 ticks (~10 ms) otapi_new_session(&s_tmpl); // Broadcast request (takes no 2nd argument) otapi_open_request(ADDR_broadcast, NULL); // Insert Transport-Layer headers c_tmpl.type = CMDTYPE_na2p_request; c_tmpl.opcode = CMD_udp_on_file; c_tmpl.extension= CMDEXT_no_response; otapi_put_command_tmpl(&status, &c_tmpl); otapi_put_dialog_tmpl(&status, NULL); // NULL = defaults // UDP Header q_writebyte(&txq, 255); // Source Port: 255 (custom application port) q_writebyte(&txq, 255); // Destination Port (same value) data_start = txq.putcursor; // Place temperature data q_writebyte(&txq, 'T'); q_writeshort(&txq, buffer[0]); // Place Voltage data q_writebyte(&txq, 'V'); q_writeshort(&txq, buffer[1]); // Place RSSI data q_writebyte(&txq, 'R'); q_writestring(&txq, (ot_u8*)&palfi.rssi1, 3); // Place Action data q_writebyte(&txq, 'E'); q_writebyte(&txq, (ot_int)palfi.wake_event); // Dump some received data if (palfi.wake_event) { q_writebyte(&txq, 'D'); q_writestring(&txq, palfi.rxdata, 8); } // Store this information into the Port 255 file for continuous, automated // reporting by DASH7/OpenTag until it is updated next time. The length of // this information is always 23 bytes. { vlFILE* fp; fp = ISF_open_su(255); if (fp != NULL) { vl_store(fp, 23, data_start); vl_close(fp); } } // Finish Message otapi_close_request(); }