// SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2012-2024 Scott Penrose and WII5 Buoy contributors // // This file is part of WII5 Buoy firmware. // See LICENSE for full terms. /** * @file WII5Communications.cpp * @brief High-level communications dispatcher: orchestrates Iridium send/receive flows. */ /* TODO 2024 - This seems a little over complicated, any simplification possible * List of results to send with current SD card??? * Status - what to send what not to? TODO: Full testing, especially: . Receiving multiple messages when only sending one . New sdBlock code for loading and finding records Communications - Really in control of Iridium However, it may become more, startup of Weather and Voltage. Possibly even read/write SD cared for data When is a message sent: * By request - e.g. sendBinModeData * Automatically - e.g. looking for unset results * By time - if start is called and we get to JUMP, and it has been more than 12 hours. (this last one covers the case where something has gotten stuck - e.g. Sparton Failure and no results to post) Template in another Mode / State machine Simple Mode - Just send messages requested, no looking at SD Start sending a message (don't call again until stopped) wii5Communications.setSimpleMode(); wii5Communications.sendBinModeType(1, 0); wii5Communications.start(); while (!wii5Communications.isRunning()) { Wait... Complex Mode: Start the background processing wii5Communications.setAutoMode(); Be very careful of SD changes, power on/off/swap Ideally, once you start - don't turn it off. However, this may not always be practical. Send signalSD when an SD Card is changed. */ #include #include #include void WII5Communications::begin() { step = WII5COMMUNICATIONS_IDLE; lastStep = WII5COMMUNICATIONS_END; wait = 0; autoMode = false; metadataScan = false; testMode = false; mainBinType = 0; metadataBlock = 0; resultsBlock = 0; mainRecordCount = 0; // Clear the request requestBinType = 0; requestMetadataBlock = 0; requestResultsBlock = 0; requestRecordCount = 0; updateStatus = false; countSucces = 0; countFailed = 0; // Find Request - these should happen before other requests or auto searches findBinType = 0; findRecordCount = 0; // When it finds this record it will select this address and the correct Results record // TODO 2024 - Disabled - this is unsafe. Can we make it so there is a maimum - look at other loops disabled = false; } // NOTE: Call start to clear void WII5Communications::setTESTING() { testMode = true; } // TODO 2024 - New code to disable - must timeout !!! bool WII5Communications::setDisabled(bool in) { disabled = in; if (disabled) { step = WII5COMMUNICATIONS_DISABLED; console.log(LOG_INFO, F("Communications: disabled on (do nothing)")); } else { step = WII5COMMUNICATIONS_IDLE; console.log(LOG_INFO, F("Communications: disabled off (move to idle)")); } wait = 0; return disabled; } void WII5Communications::setAutoMode() { autoMode = true; } void WII5Communications::setSimpleMode() { autoMode = false; } bool WII5Communications::getAutoMode() { return autoMode; } bool WII5Communications::getSimpleMode() { return !autoMode; } void WII5Communications::signalSD() { // Reset that we definitely want a metadata scan metadataScan = true; // disable existing scans - set them to 0 - as this is a new SD sdCardId = 0; sdStartBlock = 0; sdCurrentBlock = 0; // Check the mode, and put it in appropriate place. start(); } void WII5Communications::findRecord(uint32_t t, uint32_t rc) { // Set a type and Record Count to find, it will override the previous findRecordCount = rc; findBinType = t; } void WII5Communications::sendText(void *buf = NULL, uint16_t bufSize = 0) { if (buf && bufSize > 0) { strncpy(wii5Commands.lastCommandMessage, buf, bufSize); // < 235 ? bufSize, 235); } uint32_t replyType = wii5BinData.setBit(0, 4); sendBinModeType(replyType, 0, 0, 0); setSimpleMode(); console.log(LOG_INFO, F("Communications: sendText() message=%s"), wii5Commands.lastCommandMessage); start(); } void WII5Communications::sendError() { uint32_t replyType = wii5BinData.setBit(0, 3); wii5Commands.lastCommandResult = WII5RESULTS_ERROR; sendBinModeType(replyType, 0, 0, 0); setSimpleMode(); console.log(LOG_INFO, F("Communications: sendError() message=%s"), wii5Commands.lastCommandMessage); start(); } void WII5Communications::sendBinModeType(uint32_t t, uint32_t recordCount, uint32_t mdBlock, uint32_t resultsBlock) { updateStatus = false; requestWait = 0; requestBinType = t; requestRecordCount = recordCount; // Note - this does not look up the count - uses it to check Block // Leave this at 0 to load currecnt record count requestMetadataBlock = mdBlock; // Leave this at zero to not load a Block requestResultsBlock = resultsBlock; // Leave this at zero to not load a Block } /* How to use this library. setType - The BinData type to be sent setResultsRecord - Pick and SD Card and record to read. - Or could have a setResultsBuf - Not sure how to choose SD card - as it may be locked by maths etc - Not sure how to keep copy of data - 512 bytes too much . readRecord, just before BinData.set ? - How to deal with queues and wrong cards. Advanced use Instead of just a type with record etc, this library should handle things like sending raw data. Simple Mode - Like used in Position. Just send message requested. Queue Mode - Find missing entries, not yet sent */ void WII5Communications::start() { // Note: Test mode is turned to false at each start. So to enable test mode, start then setTESTING testMode = false; sdDone = 0; sdTried = 0; if (step == WII5COMMUNICATIONS_IDLE) { step = WII5COMMUNICATIONS_PREPARE; wait = 0; } } void WII5Communications::stop() { // TODO I think we should consider a shutdown, or just use first? if (step != WII5COMMUNICATIONS_IDLE) { step = WII5COMMUNICATIONS_END; wait = 0; } } bool WII5Communications::isRunning() { return (step != WII5COMMUNICATIONS_IDLE); } void WII5Communications::loop() { bool first = (lastStep != step); lastStep = step; switch (step) { case WII5COMMUNICATIONS_DISABLED: if (first) { console.log(LOG_DEBUG, F("Communications: step disabled")); } break; case WII5COMMUNICATIONS_IDLE: if (first) { if (disabled) { step = WII5COMMUNICATIONS_DISABLED; wait = 0; } console.log(LOG_DEBUG, F("Communications: step idle - manual waiting for start")); wii5Iridium.stop(); countSend = 0; // We use this to count how many in this run } // More than 14 hours since last messsage - send out a ping else if (lastSuccess > ((uint32_t)14*3600000)) { console.log(LOG_DEBUG, F("Communications: Not sent a successful message in 14 hours - send pinkg")); sendBinModeType(1,1,0,0); // TODO Custom text for the error/reason lastSuccess = 0; // To stop repeating every loop } break; case WII5COMMUNICATIONS_PREPARE: step = WII5COMMUNICATIONS_IRIDIUM_START; wait = 0; break; case WII5COMMUNICATIONS_IRIDIUM_START: if (first) { console.log(LOG_DEBUG, F("Communications: step IRIDIUM_START (firmware, imei, signalQuality)")); if (testMode) { console.log(LOG_FATAL, F("FAKE: skipping iridium request Firmware")); step = WII5COMMUNICATIONS_JUMP; wait = 0; } else { wii5Iridium.requestFirmware(); } binNextBit = 0; // Important to start split binCount = 0; } // Waiting - we are ready to send the text else if (wii5Iridium.waiting()) { step = WII5COMMUNICATIONS_JUMP; wait = 0; } // TODO Hard coded else if (wait > 600000) { console.log(LOG_ERROR, F("Communications: Iridium Timed out after 600 seconds")); step = WII5COMMUNICATIONS_JUMP; wait = 0; } break; // Decide what to do case WII5COMMUNICATIONS_JUMP: if (first) { } // Already got one moving on else if (mainBinType > 0) { console.log(LOG_DEBUG, F("Communications: step JUMP - existing message in mainBinType")); step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0; } // New request - set it and move on else if (requestBinType > 0) { console.log(LOG_DEBUG, F("Communications: step JUMP - new message in requestBiType")); // One in the background - must clearit, so not used again mainBinType = requestBinType; metadataBlock = requestMetadataBlock; resultsBlock = requestResultsBlock; mainRecordCount = requestRecordCount; countSucces = 0; countFailed = 0; // Clear the request requestBinType = 0; requestMetadataBlock = 0; requestResultsBlock = 0; requestRecordCount = 0; step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0; } else if (autoMode) { // TODO - autoMode - decide what to do // Only when we have nothing else to do step = WII5COMMUNICATIONS_SD_NEXT; wait = 0; } // else if (Old!) { // TODO Send a minimum packet // } else { step = WII5COMMUNICATIONS_END; wait = 0; } // TODO Decide on loading data from SD // 1. requestBlock or similar manuarl request (Which SD Card?) // 2. Auto Mode - find a record not sent and send it break; // SD Next - find a record case WII5COMMUNICATIONS_SD_NEXT: // Find out the SD Card and Block we started at - if either change, start again // TODO - consider openging the SD Card ourselves // No card is open.... quit if (!sdBlock.cardIsOpen()) { // TODO - consider opening one? - Nah - Capture mode only console.log(LOG_DEBUG, F("Communications: card is not open - finishing")); sdCardId = 0; sdStartBlock = 0; sdCurrentBlock = 0; step = WII5COMMUNICATIONS_END; wait = 0; } // Nothing processed - lets get that data from the SD card else if ( (sdCardId == 0) || (sdStartBlock == 0) ) { console.log(LOG_DEBUG, F("Communications: new card... getting locations")); sdCardId = sdBlock.getCardId(); sdStartBlock = sdBlock.getMetadataNext(); // 1 record past where to read sdCurrentBlock = sdBlock.getMetadataNext(); // Current record read, starts past because never reads. Sub one first } // Card has changed, blank these and move on else if ( (sdBlock.getCardId() != sdCardId) || (sdBlock.getMetadataNext() != sdStartBlock)) { console.log(LOG_DEBUG, F("Communications: card changed ! - trying to read new card")); console.log(LOG_DEBUG, F("Communications: OldSD=%d, NewCard=%d"), int(sdCardId), int(sdBlock.getCardId())); console.log(LOG_DEBUG, F("Communications: OldBlock=%lu, NewBlock=%lu"), sdCurrentBlock, sdBlock.getMetadataNext()); sdCardId = 0; sdStartBlock = 0; sdCurrentBlock = 0; } // Is the next block we have to read before the start? else if ((sdCurrentBlock - 1) < sdBlock.getMetadataFirst()) { console.log(LOG_DEBUG, F("Communications: Read all metadata records to start")); // Finsihed readin as far as we can. sdCardId = 0; sdStartBlock = 0; sdCurrentBlock = 0; step = WII5COMMUNICATIONS_END; wait = 0; } // TODO - check if we have gone back far enough - e.g. 100 records. // TODO - FIND ! This is where we can do the find. // Do 100 max tests else if (sdTried > 100) { console.log(LOG_DEBUG, F("Communications: Tried 100 records, Finishing")); // Finsihed readin as far as we can. sdCardId = 0; sdStartBlock = 0; sdCurrentBlock = 0; step = WII5COMMUNICATIONS_END; wait = 0; } // Do 10 transmitts else if (sdDone > 10) { console.log(LOG_DEBUG, F("Communications: Done 10 transmitts. Finsihing")); // Finsihed readin as far as we can. sdCardId = 0; sdStartBlock = 0; sdCurrentBlock = 0; step = WII5COMMUNICATIONS_END; wait = 0; } // Read record and use now - cos might loose it else { sdTried++; sdCurrentBlock--; console.log(LOG_DEBUG, F("Communications: reading block: %lu"), sdCurrentBlock); if (!sdBlock.read(sdCurrentBlock)) { console.log(LOG_FATAL, F("Communications: Error reading this block. Skipping to next")); // TODO what next afer error? } else { WII5MetaDataObject* local_metadata = (WII5MetaDataObject*)(sdBlock.metadata->data); // TODO Print this out to test ? if ( // Status is blank, not sent or tried (sdBlock.metadata->status == 0) && (sdBlock.metadata->resultsBlockStart != 0) && (sdBlock.metadata->recordId != 0) && (sdBlock.metadata->recordId == local_metadata->recordCount) ) { console.log(LOG_DEBUG, F("Communications: Metadata readyy, but is Result?")); // Copy the values across, but zero if next record fails - because we loose the data mainBinType = wii5Config.getCaptureBinaryType(); metadataBlock = sdCurrentBlock; resultsBlock = sdBlock.metadata->resultsBlockStart; mainRecordCount = sdBlock.metadata->recordId; updateStatus = true; bool badResults = false; // Read the block if (!sdBlock.read(resultsBlock)) { badResults = true; console.log(LOG_ERROR, F("Communications: Failed to read results for testing")); } // All records should match else if (sdBlock.block->recordId != mainRecordCount) { badResults = true; console.log(LOG_ERROR, F("Communications: Results bock record ID does not match, skipping")); } // This is a way I know the RESULTS block has been written to else if (sdBlock.block->status != mainRecordCount) { badResults = true; console.log(LOG_ERROR, F("Communications: Results status is not the record count - it isn't ready to send")); } if (badResults) { // Abandon ship console.log(LOG_FATAL, F("Communications: Error reading this block. Skipping to next")); mainBinType = 0; metadataBlock = 0; resultsBlock = 0; mainRecordCount = 0; updateStatus = false; } else { console.log(LOG_FATAL, F("Communications: Block ready to send = %lu"), resultsBlock); step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0; } } else { console.log(LOG_DEBUG, F("Communications: Skipping - Status = %lu"), sdBlock.metadata->status); } } } break; // SD Load Block case WII5COMMUNICATIONS_SD_BLOCK: step = WII5COMMUNICATIONS_IRIDIUM_SEND; wait = 0; break; // SEND a message case WII5COMMUNICATIONS_IRIDIUM_SEND: if (first) { console.log(LOG_DEBUG, F("Communications: step IRIDIUM_SEND")); } // Safety else if (binCount > 25) { console.log(LOG_FATAL, F("Communications: More than 25 data splits - cancelling Iridium")); step = WII5COMMUNICATIONS_END; wait = 0; } // if (wii5BinData.getSplit(wii5Config.getCommunicationsBinaryType(), 300, &binNextBit, &binType)) { else { // NOTE: Stil hard coded iridium number if (wii5BinData.getSplit(mainBinType, 340, &binNextBit, &binType)) { console.log(LOG_INFO, F("Communications: Sending BinData Splitting #=%d, original type/size=%lu/%d, new type/size=%lu/%d, nextBit=%d"), binCount, mainBinType, wii5BinData.getSize(mainBinType), binType, wii5BinData.getSize(binType), int(binNextBit) ); binCount++; // NOTE: setData will not set data from Blocks, call the necessary after steps. // see BinData for more information. wii5BinData.createData(binType, wii5BinaryIridium, 340, mainRecordCount); wii5BinData.setData(binType, wii5BinaryIridium, 340); // TODO Hard coded // NOTE: Performance. This should be split up into steps in this state machine, card open, read metadata, etc... This will do for now if (metadataBlock) { if (sdBlock.read(metadataBlock)) { console.log(LOG_INFO, F("Communications: Loaded metadata=%lu and setting Iridium data"), metadataBlock); // TODO Consider checking metadta block wth mainRecordCount wii5BinData.setBlockMetadata(binType, wii5BinaryIridium, 340, (WII5MetaDataObject*)sdBlock.metadata->data); } else { console.log(LOG_FATAL, F("Communications: FAILED TO LOAD Load metadata=%lu"), metadataBlock); } } if (resultsBlock) { if (sdBlock.read(resultsBlock)) { console.log(LOG_INFO, F("Communications: Loaded results=%lu"), resultsBlock); wii5BinData.setBlockResults(binType, wii5BinaryIridium, 340, (WII5Processed*)sdBlock.block->data); } else { console.log(LOG_FATAL, F("Communications: FAILED TO LOAD Load results=%lu"), resultsBlock); } } wii5Iridium.setBinSize( wii5BinData.getSize(binType) ); // Send it ! if (testMode) console.log(LOG_FATAL, F("FAKE: Not requestSendBinary")); else { wii5Iridium.requestSendBinary(); countSend++; } waitStatus = true; // We are waiting for the status, unlike command replies step = WII5COMMUNICATIONS_IRIDIUM_SEND_WAIT; wait = 0; } // Clear them and move on else { console.log(LOG_DEBUG, F("Communications: Iridium send complete, moving to end")); if ( updateStatus && metadataBlock && (countSucces > 0) // && (countFailed < 1) - TODO add this test ) { // TODO how about some better meaning to the number? sdDone++; console.log(LOG_DEBUG, F("Communications: Updating status block=%lu, record=%lu, status=1"), metadataBlock, mainRecordCount); sdBlock.metadataUpdateStatus(metadataBlock, mainRecordCount, 1); sdBlock.read(metadataBlock); console.log(LOG_INFO, F("Communications: TODO Updated status and read for block = %lu, record = %lu and status = %lu"), metadataBlock, mainRecordCount, sdBlock.metadata->status ); } else { console.log(LOG_INFO, F("Communications: Message sent but status not updated - metadataBlock=%lu,countSucces=%d,update=%d"), metadataBlock, int(countSucces), int(updateStatus)); } updateStatus = false; binNextBit = 0; // Important to start split binCount = 0; mainBinType = 0; metadataBlock = 0; resultsBlock = 0; step = WII5COMMUNICATIONS_JUMP; wait = 0; } } break; // Wait for send and check reply case WII5COMMUNICATIONS_IRIDIUM_SEND_WAIT: if (first) { console.log(LOG_DEBUG, F("Communications: step IRIDIUM_SEND_WAIT")); if (testMode) { console.log(LOG_FATAL, F("FAKE: Not waiting for Iridium")); updateStatus = true; countSucces++; step = WII5COMMUNICATIONS_JUMP; wait = 0; first = true; } } else if (wii5Iridium.waiting()) { // Wait for status success/fail for counts. This allos us to ignore command messages if (waitStatus) { if (wii5Iridium.lastSend == WII5IRIDIUMSENDRESULT_OK) { countSucces++; lastSuccess = 0; // To stop repeating every loop } else countFailed++; waitStatus = false; } // TODO - check this isn't a race condition. // Check incoming messages if (wii5Iridium.lastReceive == WII5IRIDIUMRECEIVERESULT_OK) { if (wii5Iridium.lastSend == WII5IRIDIUMSENDRESULT_OK) console.log(LOG_INFO, F("Communications: (Send success) Received Iridium Data length=%d"), wii5Iridium.lastReceiveLen); else console.log(LOG_ERROR, F("Communications: (Send failed) Received Iridium Data length=%d"), wii5Iridium.lastReceiveLen); wii5Commands.processBinData(); if ( (wii5Commands.lastCommandCmd != WII5COMMAND_NONE) && (wii5Commands.lastCommandResult != WII5RESULTS_UNKNOWN) ) { console.log(LOG_ERROR, F("Communications: Sending reply command=%d, result=%d"), int(wii5Commands.lastCommandCmd), int(wii5Commands.lastCommandResult)); // Always send a result, allowing for next message too - 3 or 4 depending on lenght reply uint32_t replyType = wii5BinData.setBit(0, 3); wii5BinData.createData(replyType, wii5BinaryIridium, 340, 1); wii5BinData.setData(replyType, wii5BinaryIridium, 340); // TODO Hard coded wii5Iridium.setBinSize(wii5BinData.getSize(replyType)); wii5Iridium.requestSendBinary(); // Blank last command wii5Commands.lastCommandCmd = WII5COMMAND_NONE; wii5Commands.lastCommandResult = WII5RESULTS_UNKNOWN; memset(wii5Commands.lastCommandMessage, 0, sizeof(wii5Commands.lastCommandMessage)); } else { // TODO Send error ! console.log(LOG_ERROR, F("Communications: Unable to reply command=%d, result=%d"), int(wii5Commands.lastCommandCmd), int(wii5Commands.lastCommandResult)); step = WII5COMMUNICATIONS_JUMP; wait = 0; first = true; } // No change of step - as we are sending again } else { if (wii5Iridium.lastSend == WII5IRIDIUMSENDRESULT_OK) console.log(LOG_INFO, F("Communications: (Send success) nothing received")); else console.log(LOG_ERROR, F("Communications: (Send failed) nothing received")); // Check any further waiting messages - send same message again. step = WII5COMMUNICATIONS_JUMP; wait = 0; first = true; } } // TODO Configurables else if (wait > 300000) { console.log(LOG_ERROR, F("Communications: Iridium Timed out after 5 minutes")); step = WII5COMMUNICATIONS_END; wait = 0; } break; case WII5COMMUNICATIONS_END: if (first) { console.log(LOG_INFO, F("Communications: step END - checking")); } else if (metadataScan && (countSend < 1)) { // Turn it off, so we don't get in here twice on no send. metadataScan = false; console.log(LOG_INFO, F("Communications: NO TRANSMISSIONS - Sending a default")); uint32_t replyType = wii5BinData.setBit(1, 5); sendBinModeType(replyType, 0, 0, 0); setSimpleMode(); step = WII5COMMUNICATIONS_JUMP; wait = 0; } else { metadataScan = false; console.log(LOG_INFO, F("Communications: Sent %d messages"), countSend); // Noithing left to do - lets shut it down. wii5Iridium.stop(); step = WII5COMMUNICATIONS_IDLE; wait = 0; } break; default: console.log(LOG_FATAL, F("COMMUNICATIONS: Default step... step=%d"), step); step = WII5COMMUNICATIONS_IDLE; wait = 0; } } WII5Communications wii5Communications;