Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
# build
build/
build-*/
out/
_codeql_build_dir/
_codeql_detected_source_root

Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ set(HEADERS
include/peripherals/peripheral_interface.h
include/console_emulator.h
include/backend.h
include/authorization_snapshot.h
include/types.h
include/url_utils.h
include/hardware/aht10.h
Expand Down Expand Up @@ -593,6 +594,8 @@ if(ENABLE_TESTING)
tests/cache_manager_test.cpp
tests/controller_test.cpp
tests/message_storage_test.cpp
tests/report_delivery_test.cpp
tests/foreground_backend_test.cpp
tests/user_cache_test.cpp
tests/backlog_worker_test.cpp
tests/bounded_executor_test.cpp
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ If you use MSVC toolchain instead of Ninja/gcc, change the `-G "Ninja"` to your
## Documentation

- [State Machine](docs/state_machine.md) - Complete state machine workflow and transitions
- [Foreground Backend Waits](docs/foreground_backend_waits.md) - Configurable waits, cache continuation, and durable background reporting
- [Hardware Integration](docs/hardware.md) - Display and NFC wiring/configuration
- [Logging System](docs/logging.md) - Detailed logging configuration and usage
- [Installation Guide](docs/installation.md) - Complete installation instructions
Expand Down
131 changes: 131 additions & 0 deletions docs/foreground_backend_waits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Foreground backend waits and durable reporting

Set `fuelflux::timing::kForegroundBackendWaitTimeout` in
[`include/timing_config.h`](../include/timing_config.h) and rebuild. The default
is `std::chrono::seconds{5}`; a non-positive value fails compilation. This one
setting controls authorization cache fallback, the slow-connection warning,
and release of both refuel and intake transmission screens on every connection
type. HTTP connection/total deadlines and DNS deadlines remain independent.

## Authorization

Each attempt captures saved user and tank values, pending-card protection, and
writable local report-storage availability before starting its independent
backend session. The general user/tank snapshot comes from one cache generation.
The captured values stay unchanged for that attempt even if synchronization or
report delivery finishes meanwhile.

The display initially shows `Проверка...` / `Ожидайте`. An online result received
before the foreground deadline follows the existing allowance, role, and tank
selection checks. A network failure can use the captured snapshot; a definitive
authorization denial cannot.

When the foreground deadline expires, usable saved data automatically continues
through the existing cache authorization checks. Otherwise the display shows
`Медленное соединение`, `Ожидайте или`, and the keyboard profile's Cancel prompt
(`Нажмите ОТМЕНА (B)` for legacy, `Нажмите ОТМЕНА` for VID). The warning uses small
text lines so it fits the ST7565 as well as the larger ILI9488. Cancel immediately
returns to the welcome screen. Cancel before the warning retains its former
behavior.

Attempts have independent IDs and sessions. Abandonment signals transport
cancellation, including cancellable c-ares DNS waits on the GSM build. The
controller accepts an attempt's result only while that attempt is current.
A late successful unused session is closed asynchronously; it cannot update
the active session or cache.

## Reports and subsequent operations

Before enabling another operation, the controller stores the completed payload
and a protected per-card user/tank snapshot in one SQLite transaction, deducting
the refuel volume exactly once. Intake preserves the allowance. A persistence
failure shows `Ошибка записи` and enters the error procedure instead of showing
successful completion with an unrecorded transaction.

The transmission screen remains until delivery resolves or the foreground wait
expires. The existing completion screen retains the final volume and accepts a
new card or PIN. Screen release does not send the report again. The delivery
coordinator owns the reporting backend session, so the new operation cannot
reuse or close it.

A card with queued or in-flight reports immediately uses its protected snapshot
and locally reduced allowance, even when the general cache lacks its tanks.
Different cards authorize normally. Late delivery changes only its matching
report and attempt IDs. Network failures schedule retries; definitive rejections
move the unchanged payload to `dead_messages` and resolve that report. Once no
pending reports remain for the card, its next operation attempts online
authorization normally.

Authorization has a bounded worker and queue. A separate single delivery worker
coordinates both new reports and retries, with one active sender and ordered
delivery within each card. At most one additional original session is retained;
other queued reports obtain an independent session when selected. Controller
state and display transitions remain on the controller loop. Shutdown cancels
active network work and joins the owned workers. At process exit the shared
token-cleanup worker is also cancelled and joined before DNS/logging teardown.
The same cleanup order applies when the application enters permanent failure.

Controller startup and shutdown synchronize event-loop ownership, including a
thread that has been launched but has not entered `run()` yet. Exceptions release
that ownership too. `shutdown()` returns `false` if an active controller action
does not exit within `kShutdownDeadline`; it leaves live state and peripherals
intact so the caller can retry after the action exits. Destruction while the loop
is still live terminates the process. The application also exits with failure on
a shutdown timeout, allowing the service supervisor to restart it instead of
freeing state under a hung peripheral call. Independent/cancellable backend
sessions are required by `Controller` and checked at construction; synchronous
backend adapters may still be used directly by synchronization/delivery callers.

## Persistence and compatibility

The `backlog` schema now has stable increasing report IDs, delivery attempt IDs,
queued/in-flight status, retry eligibility, and a payload interpretation flag.
Startup migrates legacy implicit rowids in order without changing payloads.
Legacy payloads retain their existing tank-mapping interpretation. New payloads
capture the backend tank ID and transaction timestamp once; retries do not
remap tanks or replace timestamps.

`card_report_state` stores the reduced allowance and saved user/tanks separately
from cache generations. Report acknowledgements and retries never deduct again.
Interrupted deliveries are recovered for retry at coordinator startup. Protected
card data and pending-card restrictions therefore survive restart.
If startup recovery cannot write to SQLite, the worker retries recovery before
sending any report. Recovery clears the in-flight marker; only the next delivery
claim increments the attempt ID. Old completions are rejected both before that
claim and after it.

Canonical delivery checks the method's user role and payload shape. It does not
compare an already-recorded transaction with a later server allowance or remap
its tank: those values may have changed since dispensing, and doing so would
incorrectly suppress or alter a retry.

Synchronization records resolved snapshot revisions before fetching server
data and may release protection only if those revisions remain unchanged and
the card still has no pending reports after the cache commit. A server fetch
started while reports were pending cannot restore an older allowance. A fresh
successful online authorization refreshes resolved protection and its revision,
so an older synchronization fetch cannot undo that authorization either.

Backend deduplication remains unverified. Network-failure retries are preserved.
A timeout can occur after the server accounted for a report but before its reply
arrived; stable local IDs and exclusive sending cannot prevent duplicate server
accounting in that case. Exactly-once accounting requires backend support for
an idempotency key or equivalent reconciliation protocol. The local report ID
is not sent as an undocumented backend field.

## Verification

`foreground_backend_test.cpp` exercises configurable waits, early/late results,
cache fallback, missing-cache cancellation, same/different-card continuation,
refuel/intake delivery, rejection, storage failures, and shutdown.
`report_delivery_test.cpp` covers atomic deductions, snapshot protection, stable
payloads, attempt matching, per-card ordering, migration, and restart recovery.
Backend tests include cancellation of an already-sent HTTP authorization;
c-ares cancellation tests are enabled in `USE_CARES` builds. Display tests check
the Russian warning and both Cancel labels against the small display's width.

Run `ctest --test-dir <build-directory> --output-on-failure`. The Windows console
suite and Linux container build with `TARGET_SIM800C=ON` have been exercised.
The Linux checks include cancellation while waiting for a deliberately withheld
DNS reply and while waiting for another resolver call. Physical displays and
the modem still require validation on the target hardware.
25 changes: 18 additions & 7 deletions docs/state_machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

The FuelFlux controller uses a Mealy state machine to manage the fuel dispensing workflow. The state machine ensures proper sequencing of operations and handles various user interactions, timeouts, and error conditions.

Authorization and report delivery run on separate workers. The shared
`kForegroundBackendWaitTimeout` (default 5 seconds, rebuild to change) controls
cache fallback, the slow-connection warning, and release of transmission screens.
See [Foreground Backend Waits](foreground_backend_waits.md) for persistence,
pending-card continuation, late responses, and retry behavior.

## States

### SystemState Enumeration
Expand Down Expand Up @@ -42,12 +48,14 @@ The FuelFlux controller uses a Mealy state machine to manage the fuel dispensing
| `PinEntered` | User completed PIN entry (pressed 'A') |
| `AuthorizationSuccess` | Backend authorization succeeded |
| `AuthorizationFailed` | Backend authorization failed |
| `AuthorizationCancelled` | User abandoned a pending attempt from the slow-connection screen |
| `TankSelected` | User selected a valid tank |
| `VolumeEntered` | User entered a valid volume |
| `AmountEntered` | User entered a payment amount (future use) |
| `RefuelingStarted` | Pump started dispensing fuel |
| `RefuelingStopped` | Pump stopped (manually or target reached) |
| `DataTransmissionComplete` | Backend transaction transmission completed |
| `DataTransmissionComplete` | Report resolved or foreground wait expired after durable storage |
| `FlowDisplayRefresh` | Refresh the meter display without treating it as keyboard input |
| `IntakeSelected` | Operator selected intake operation |
| `IntakeDirectionSelected` | Operator selected intake direction (In/Out) |
| `IntakeVolumeEntered` | Operator entered intake volume |
Expand Down Expand Up @@ -128,9 +136,10 @@ reporting, offline backlog storage, and allowance deduction.
- Both trigger: Event: `RefuelingStopped` → State: `RefuelDataTransmission`

6. **Refuel Data Transmission**
- Transaction is logged to backend asynchronously
- Report and reduced local allowance are committed together before another operation is enabled
- The delivery coordinator sends the report using its own session
- Display shows: "Data transmission in progress"
- User cannot interact during transmission
- At delivery completion or the configured foreground deadline, the screen is released; delivery/retries can continue in the background
- Event: `DataTransmissionComplete` → State: `RefuelingComplete`

7. **Refueling Complete**
Expand Down Expand Up @@ -200,9 +209,9 @@ reporting, offline backlog storage, and allowance deduction.
- Volume to transfer
- Direction (In: 1, Out: 2)
- Timestamp
- Transaction is logged to backend asynchronously
- Transaction and saved session data are persisted locally, then sent by the delivery coordinator
- Display shows: "Data transmission in progress"
- Operator cannot interact during transmission (all keys disabled)
- At delivery completion or the configured foreground deadline, the screen is released; delivery/retries can continue in the background
- Event: `DataTransmissionComplete` → State: `IntakeComplete`

7. **Intake Complete**
Expand Down Expand Up @@ -266,7 +275,9 @@ When timeout occurs during intake entry: session ends, returns to `Waiting`, ope
|----------|-------|--------|--------|
| Invalid direction (e.g., '3') | `IntakeDirectionSelection` | Shows error, clears input | Stay in `IntakeDirectionSelection` |
| Zero or negative volume | `IntakeVolumeEntry` | Shows error, clears input | Stay in `IntakeVolumeEntry` |
| Backend transmission fails | `IntakeDataTransmission` | Transaction not logged | Stay in `IntakeDataTransmission`, can retry with cancel+restart |
| Network transmission fails | `IntakeDataTransmission` | Durable report queued for retry | Completion screen after foreground deadline; same card uses saved data |
| Backend definitively rejects report | `IntakeDataTransmission` | Move report to failed reports | Report resolved; next online authorization allowed when card has no pending reports |
| Local report storage fails | `IntakeDataTransmission` | Show `Ошибка записи` | Enter `Error`; do not release an unrecorded transaction |
| Invalid tank number | `TankSelection` | Shows error, clears input | Stay in `TankSelection` |
| Timeout during intake | Any intake state | Session cleared | Return to `Waiting` |

Expand Down Expand Up @@ -317,7 +328,7 @@ The 'B' key (Stop/Cancel) is active in most states and handles different operati
|------------|--------|
| `Waiting` | No effect |
| `PinEntry` | Clear PIN, return to `Waiting` |
| `Authorization` | No effect (cannot cancel during auth) |
| `Authorization` | No effect before foreground deadline; on the slow-connection screen, abandon attempt and return to `Waiting` |
| `NotAuthorized` | Return to `Waiting` |
| `TankSelection` | Cancel selection, return to `Waiting` |
| `VolumeEntry` | Cancel entry, return to `Waiting` |
Expand Down
25 changes: 25 additions & 0 deletions include/authorization_snapshot.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "backend.h"
#include <optional>

namespace fuelflux {

// A value copy: it must not change when synchronization flips cache tables.
struct AuthorizationSnapshot {
UserInfo user;
std::vector<BackendTankInfo> tanks;
};

struct ProtectedCardSnapshot {
AuthorizationSnapshot authorization;
bool pending = false;
};

struct SavedAuthorizationState {
std::optional<AuthorizationSnapshot> saved;
bool reportStorageAvailable = false;
bool pendingReports = true; // Fail closed if storage cannot be read.
};

} // namespace fuelflux
17 changes: 17 additions & 0 deletions include/backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ struct FuelTank {
class IBackend {
public:
virtual ~IBackend() = default;
// Return an independent cancellable session for each foreground operation.
// Empty is allowed for synchronous-only adapters. Controller requires this
// capability and rejects unsupported adapters at construction, before use.
virtual std::shared_ptr<IBackend> CreateIndependentSession() const { return {}; }
virtual void CancelPendingRequests() = 0;
virtual bool SendReportPayload(const std::string& payload, bool intake, bool canonicalTankId) {
if (!canonicalTankId) return intake ? IntakePayload(payload) : RefuelPayload(payload);
return false;
Comment thread
Copilot marked this conversation as resolved.
Outdated
}
virtual bool Authorize(const std::string& uid) = 0;
virtual bool Deauthorize() = 0;
virtual bool Refuel(TankNumber tankNumber, Volume volume) = 0;
Expand Down Expand Up @@ -86,13 +95,18 @@ class IBackend {
class BackendBase : public IBackend, public std::enable_shared_from_this<BackendBase> {
public:
~BackendBase() override = default;
// Process shutdown only: cancel and join the shared token-cleanup worker
// before tearing down DNS and logging. Independent controller workers stop first.
static void ShutdownAsyncRequests();

bool Authorize(const std::string& uid) override;
bool Deauthorize() override;
bool Refuel(TankNumber tankNumber, Volume volume) override;
bool Intake(TankNumber tankNumber, Volume volume, IntakeDirection direction) override;
bool RefuelPayload(const std::string& payload) override;
bool IntakePayload(const std::string& payload) override;
bool SendReportPayload(const std::string& payload, bool intake, bool canonicalTankId) override;
void CancelPendingRequests() override { cancelled_.store(true); }

bool IsAuthorized() const override { return session_.IsAuthorized(); }
std::string GetToken() const override { return session_.GetToken(); }
Expand Down Expand Up @@ -126,6 +140,7 @@ class BackendBase : public IBackend, public std::enable_shared_from_this<Backend
// Get the bounded executor for async deauthorization requests
// Uses Meyer's singleton pattern for thread-safe lazy initialization
static BoundedExecutor& GetDeauthorizeExecutor();
static std::atomic<bool>& DeauthorizeCancellation();

// Map TankNumber in a payload from visualNumberTank to idTank.
// Returns true if a match was found and the mapping was applied.
Expand All @@ -140,6 +155,7 @@ class BackendBase : public IBackend, public std::enable_shared_from_this<Backend
std::vector<BackendTankInfo> fuelTanks_;
std::string lastError_;
std::atomic<bool> networkError_{false};
std::atomic<bool> cancelled_{false};
std::shared_ptr<MessageStorage> storage_;
};

Expand All @@ -153,6 +169,7 @@ class Backend : public BackendBase {
Backend(const std::string& baseAPI, const std::string& controllerUid, std::shared_ptr<MessageStorage> storage = nullptr);

~Backend() override;
std::shared_ptr<IBackend> CreateIndependentSession() const override;

private:
// Private method for common parsing of responses from the backend
Expand Down
12 changes: 12 additions & 0 deletions include/backlog_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <functional>

#include "backend.h"
#include "message_storage.h"
Expand All @@ -33,11 +35,16 @@ class BacklogWorker {
void SetInterval(std::chrono::milliseconds interval);

bool ProcessOnce();
void Wake();
// Atomically submit a report and reserve its original session for delivery.
std::optional<long long> Submit(MessageMethod method, const std::string& payload,
AuthorizationSnapshot snapshot, double deduction, std::shared_ptr<IBackend> session = nullptr);

private:
void RunLoop();
bool ProcessMessage(const StoredMessage& message);
bool HandleFailure(const StoredMessage& message);
bool FinishDelivery(const StoredMessage& message, MessageStorage::DeliveryResult result);

std::shared_ptr<MessageStorage> storage_;
std::shared_ptr<IBackend> backend_;
Expand All @@ -46,6 +53,11 @@ class BacklogWorker {
std::thread workerThread_;
mutable std::mutex mutex_;
std::condition_variable cv_;
bool wake_ = false;
std::mutex deliveryMutex_;
std::unordered_map<long long, std::shared_ptr<IBackend>> sessions_;
std::shared_ptr<IBackend> activeBackend_;
bool activeSessionSupplied_ = false;
};

} // namespace fuelflux
5 changes: 5 additions & 0 deletions include/cache_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "user_cache.h"
#include "backend.h"
#include "timing_config.h"
#include "message_storage.h"
#include <memory>
#include <thread>
#include <atomic>
Expand Down Expand Up @@ -52,6 +53,9 @@ class CacheManager {

// Deduct allowance from cache (called after refuel for RoleId==1)
bool DeductAllowance(const std::string& uid, double amount);
// Configure before Start(). Only a fresh, complete population may release
// resolved local snapshots; requests begun before delivery must not do so.
void SetReportStorage(std::shared_ptr<MessageStorage> storage) { reportStorage_ = std::move(storage); }

private:
void WorkerThread();
Expand All @@ -60,6 +64,7 @@ class CacheManager {

std::shared_ptr<UserCache> cache_;
std::shared_ptr<IBackend> backend_;
std::shared_ptr<MessageStorage> reportStorage_;

std::thread workerThread_;
std::atomic<bool> running_;
Expand Down
Loading