Skip to content

Commit

Permalink
Implementing support for serial input events in the simulator and the…
Browse files Browse the repository at this point in the history
…ir loading in generic tester.
  • Loading branch information
krulis-martin committed Mar 3, 2024
1 parent 4f27af7 commit 69ba8c8
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 50 deletions.
4 changes: 2 additions & 2 deletions GenericTester/GenericTester.vcxproj.user
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LocalDebuggerCommandArguments>--log-buttons --log-leds --log-7seg --raw-7seg ./data/test1.in</LocalDebuggerCommandArguments>
<LocalDebuggerCommandArguments>--log-buttons --log-leds --log-7seg --log-serial ./data/test1.in</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LocalDebuggerCommandArguments>--log-buttons --log-leds --log-7seg --raw-7seg ./data/test1.in</LocalDebuggerCommandArguments>
<LocalDebuggerCommandArguments>--log-buttons --log-leds --log-7seg --log-serial ./data/test1.in</LocalDebuggerCommandArguments>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>
32 changes: 22 additions & 10 deletions GenericTester/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ In ReCodEx, the tester should be compiled with RECODEX macro set. It will make s
- `--simulation-length` - Length of the simulation in ms (overrides value from input file, required if no input file is provided).
- `--loop-delay` - Delay between two loop invocations [us] (default 100).
- `--log-buttons` - Add button events into output log.
- `--log-serial` - Add serial input events into output log.
- `--log-leds` - Add LED events into output log.
- `--log-7seg` - Add events of the 7-segment display into output log.
- `--raw-leds` - Deactivate LEDs event smoothing by demultiplexer and aggregator.
Expand All @@ -33,26 +34,37 @@ Input is a simple text file (similar to CSV, but uses spaces instead of ',' or '

Each line has a fixed format:
```
<timestamp> <button_number> <new_state>
<timestamp> <action_type> <new_state>
```
- `--timestamp` is simulation time (microseconds from the beginning) in decimal format
- `--`button_number` is the index of the button (1-3 with currently supported funshield)
- `--new_state` is a single char `'u'` (up) or `'d'` (down) indicating the new state of the button
- `timestamp` is simulation time (microseconds from the beginning) in decimal format
- `action_type` is the index of the button (1-3 with currently supported funshield) in case of button actions, or `S` for serial input
- `new_state` is a single char `'u'` (up) or `'d'` (down) indicating the new state of the button (if action type is 1-3) or an arbitrary string (all characters till the end of line) that is inserted as serial input (if action type is `S`)

Optionally, the last line of the input file may hold only the timestamp (no button number or state) to denote the end time of the simulation. If the end time is missing, it is set shortly after the last button event (the delay is implementation-defined).
Optionally, the last line of the input file may hold only the timestamp (no action type) to denote the end time of the simulation. If the end time is missing, it is set shortly after the last event (the delay is implementation-defined).

**Example:**
```
100000 1 d
200000 2 d
500000 S Next message to be displayed
700000 2 u
800000 1 u
1000000
```

### Output file format

The output file is a CSV that uses commas as a separator (no quotes are used for value wrapping since the values are trivial). The first column is always a timestamp, remaining columns are defined on the first row based on the configuration arguments (whether particular events are logged). Possible columns are:
The output file is a CSV that uses commas as a separator (double quotes are used for string wrapping). The first column is always a timestamp, remaining columns are defined on the first row based on the configuration arguments (whether particular events are logged). Possible columns are:

- `--timestamp` - event simulation time (microseconds from the beginning) in decimal format
- `--b1`, `b2`, `b3` - buttons, possible values are 0 = button is released, 1 = button is down
- `--leds` - four bit values encoded into single hex digit (0-f), least significant bit is LED #1 (according to `funshield.h`) uses inverted logic (1 = OFF, 0 = ON)
- `--7seg` - output of 7-seg display binary value (4B) encoded in hex (8 digits) holding a digital representation of the display state (first byte is the rightmost position), also uses inverted logic
- `timestamp` - event simulation time (microseconds from the beginning) in decimal format
- `b1`, `b2`, `b3` - buttons, possible values are 0 = button is released, 1 = button is down
- `leds` - four bit values encoded into single hex digit (0-f), least significant bit is LED #1 (according to `funshield.h`) uses inverted logic (1 = OFF, 0 = ON)
- `7seg` - output of 7-seg display binary value (4B) encoded in hex (8 digits) holding a digital representation of the display state (first byte is the rightmost position), also uses inverted logic
- `serial` - string that was added as an input (from host to Arduino) in the input file

All columns besides `timestamp` are filled only when the value is changed at that time (otherwise it is an empty string). Note that multiple changing events may take place at the same time, so multiple different columns may be non-empty on the same row.

The LEDs and 7seg display use smoothening unless the are switched to _raw_* collection (by a particular argument).

In case of error, the first line of the output file contains `ERROR` or `INTERNAL ERROR`. Jhe judge is then expected to just dump the rest of the log as an error message (to stdout in case of regular error, to stderr in case of internal error).

1 change: 1 addition & 0 deletions GenericTester/data/test1.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
100000 1 d
100000 S 6789
200000 1 u
200000 2 d
300000 3 d
Expand Down
79 changes: 49 additions & 30 deletions GenericTester/dataio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@
#include <limits>


/**
* Load input text file (stream) with button events. Fill them into funshield emulator and record them in output time series.
* @param sin input stream (the opened text file)
* @param funshield the emulator being pre-loaded with button events
* @param outputEvents a vector of time series (one for each button), if the vector is empty, no events are recorded
* @return duration of the emulation as loaded from the input stream
*/
logtime_t loadInputData(std::istream& sin, FunshieldSimulationController& funshield, std::vector<std::shared_ptr<TimeSeries<bool>>>& outputEvents)
logtime_t loadInputData(std::istream& sin, FunshieldSimulationController& funshield,
std::vector<std::shared_ptr<TimeSeries<bool>>>& buttonEvents, std::shared_ptr<TimeSeries<std::string>> serialEvents)
{
std::string line;
std::size_t lineCount = 0;
Expand All @@ -23,43 +17,68 @@ logtime_t loadInputData(std::istream& sin, FunshieldSimulationController& funshi

// parse the line
logtime_t time = 0;
int button = -1;
char action = '\0';
std::stringstream(line) >> time >> button >> action;
char actionType = '\0';
char newState = '\0';
std::stringstream ss(line);
ss >> time >> actionType >> newState;

if (time < lastTime) {
throw std::runtime_error("Timestamps are not ordered on line " + std::to_string(lineCount)
+ ". Timestamp " + std::to_string(time) + " is lower than the previous " + std::to_string(lastTime) + ".");
}
lastTime = time;

if (button == -1 && action == '\0') {
if (actionType == '\0') {
return lastTime; // the timestamp had no additional arguments, it must have been the last marker
}

if (button < 1 || button > 3 || (action != 'u' && action != 'd')) {
throw std::runtime_error("Invalid operation (button #" + std::to_string(button) + " action " + action
+ ") found at line " + std::to_string(lineCount));
}
--button; // normalize to zero-based index

bool newButtonState = action == 'd'; // down = true
if (buttonStates[button] == newButtonState) {
continue; // no change in state
}
buttonStates[button] = newButtonState;
if (actionType == 'S') {
// serial input
std::string serialInput;
if (newState != '\0') {
std::getline(ss, serialInput, '\0');
serialInput = newState + serialInput;
}

funshield.getArduino().enqueueSerialInputEvent(serialInput, time);

// enqueue the event into funshield emulator
if (newButtonState) {
funshield.buttonDown(button, time);
if (serialEvents) {
serialEvents->addEvent(time, serialInput);
}
}
else {
funshield.buttonUp(button, time);
}
/*
* Handle button actions.
*/

if (actionType < '1' || actionType > '3' || (newState != 'u' && newState != 'd')) {
throw std::runtime_error("Invalid operation (button #" + std::to_string(actionType) + " action " + newState
+ ") found at line " + std::to_string(lineCount));
}
int button = (int)actionType - (int)'1'; // normalize to 0-based int

bool newButtonState = newState == 'd'; // down = true
if (buttonStates[button] == newButtonState) {
continue; // no change in state
}
buttonStates[button] = newButtonState;

// enqueue the event into funshield emulator
if (newButtonState) {
funshield.buttonDown(button, time);
}
else {
funshield.buttonUp(button, time);
}

// record it for the output events
if (button < (int)buttonEvents.size()) {
buttonEvents[button]->addEvent(time, newButtonState);
}

// record it for the output events
if (button < (int)outputEvents.size()) {
outputEvents[button]->addEvent(time, newButtonState);
}

}

return lastTime + 100000; // add 100ms after last button event
Expand Down
6 changes: 4 additions & 2 deletions GenericTester/dataio.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
* Load input text file (stream) with button events. Fill them into funshield emulator and record them in output time series.
* @param sin input stream (the opened text file)
* @param funshield the emulator being pre-loaded with button events
* @param outputEvents a vector of time series (one for each button), if the vector is empty, no events are recorded
* @param buttonEvents a vector of time series (one for each button), if the vector is empty, no events are recorded
* @param serialEvents a time series holding input events for serial link (data transferred from host to Arduino)
* @return duration of the emulation as loaded from the input stream
*/
logtime_t loadInputData(std::istream& sin, FunshieldSimulationController& funshield, std::vector<std::shared_ptr<TimeSeries<bool>>>& outputEvents);
logtime_t loadInputData(std::istream& sin, FunshieldSimulationController& funshield,
std::vector<std::shared_ptr<TimeSeries<bool>>>& buttonEvents, std::shared_ptr<TimeSeries<std::string>> serialEvents);

/**
* Print out formatted CSV composed of multiple time series (collecting events).
Expand Down
13 changes: 11 additions & 2 deletions GenericTester/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ logtime_t processInput(bpp::ProgramArguments &args, FunshieldSimulationControlle
buttonEvents.emplace_back(std::make_shared<TimeSeries<bool>>());
buttonEvents.emplace_back(std::make_shared<TimeSeries<bool>>());
}
std::shared_ptr<TimeSeries<std::string>> serialEvents;
if (args.getArgBool("log-serial").getValue()) {
serialEvents = std::make_shared<TimeSeries<std::string>>();
}

logtime_t simulationTime = 0;

Expand All @@ -58,11 +62,11 @@ logtime_t processInput(bpp::ProgramArguments &args, FunshieldSimulationControlle
if (!sin.is_open()) {
throw std::runtime_error("Failed to open input file " + args[0]);
}
simulationTime = loadInputData(sin, funshield, buttonEvents);
simulationTime = loadInputData(sin, funshield, buttonEvents, serialEvents);
}
else {
// load events from stdin
simulationTime = loadInputData(std::cin, funshield, buttonEvents);
simulationTime = loadInputData(std::cin, funshield, buttonEvents, serialEvents);
}
}
else {
Expand All @@ -82,6 +86,10 @@ logtime_t processInput(bpp::ProgramArguments &args, FunshieldSimulationControlle
outputEvents["b3"] = buttonEvents[2];
}

if (args.getArgBool("log-serial").getValue()) {
outputEvents["serial"] = serialEvents;
}

return simulationTime;
}

Expand Down Expand Up @@ -111,6 +119,7 @@ int main(int argc, char* argv[])
args.registerArg<bpp::ProgramArguments::ArgInt>("simulation-length", "Length of the simulation in ms (overrides value from input file, required if no input file is provided).", false, 0, 0);
args.registerArg<bpp::ProgramArguments::ArgInt>("loop-delay", "Delay between two loop invocations [us].", false, 100, 1);
args.registerArg<bpp::ProgramArguments::ArgBool>("log-buttons", "Add button events into output log.");
args.registerArg<bpp::ProgramArguments::ArgBool>("log-serial", "Add serial-link input events into output log.");
args.registerArg<bpp::ProgramArguments::ArgBool>("log-leds", "Add LED events into output log.");
args.registerArg<bpp::ProgramArguments::ArgBool>("log-7seg", "Add events of the 7-segment display into output log.");

Expand Down
7 changes: 7 additions & 0 deletions GenericTester/solution.ino
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,11 @@ void loop() {
shiftOut(data_pin, clock_pin, MSBFIRST, 0b11111111);
digitalWrite(latch_pin, HIGH);
}

while (Serial.available()) {
char ch = (char)Serial.read();
if (ch >= '0' && ch <= '9') {
counter = (counter * 10 + (int)(ch - '0')) % 10000;
}
}
}
12 changes: 10 additions & 2 deletions shared/emulator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,11 @@ friend class ArduinoSimulationController;
logtime_t mPinWriteDelay;
logtime_t mPinSetModeDelay;

/**
* Buffer of currently available serial data (which can be read by Arduino).
*/
std::deque<char> mSerialData;

void reset()
{
mCurrentTime = 0;
Expand All @@ -240,13 +243,16 @@ friend class ArduinoSimulationController;
for (auto& [_, arduinoPin] : mPins) {
arduinoPin.reset();
}

mSerialData.clear();
}

/**
* Advances the Arduino emulator time forward by given number of microseconds.
* @param us relative logical time in microseconds
* @return time after update
*/
void advanceCurrentTimeBy(logtime_t us)
logtime_t advanceCurrentTimeBy(logtime_t us)
{
mCurrentTime += us;

Expand All @@ -257,6 +263,7 @@ friend class ArduinoSimulationController;
for (auto& [_, arduinoPin] : mPins) {
arduinoPin.advanceTime(mCurrentTime);
}
return mCurrentTime;
}

/**
Expand Down Expand Up @@ -627,6 +634,7 @@ friend class ArduinoSimulationController;
for (char c : str) {
mSerialData.push_back(c);
}
mSerialData.push_back('\n');
}

/**
Expand Down
39 changes: 37 additions & 2 deletions shared/simulation.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <map>
#include <string>
#include <algorithm>
#include <deque>


/**
Expand All @@ -29,6 +30,11 @@ class ArduinoSimulationController
*/
std::map<pin_t, FutureTimeSeries<ArduinoPinState>> mInputBuffers;

/**
* Registered simulation inputs, strings that will be sent as serial data (at given time)
*/
std::deque<std::pair<logtime_t, std::string>> mSerialInput;

void setMethodEnableFlag(const std::string& name, bool enabled)
{
auto it = mEnableMethodFlags.find(name);
Expand All @@ -38,6 +44,15 @@ class ArduinoSimulationController
*(it->second) = enabled;
}

void advanceCurrentTimeBy(logtime_t time)
{
logtime_t currentTime = mEmulator.advanceCurrentTimeBy(time);
while (!mSerialInput.empty() && mSerialInput.front().first <= currentTime) {
mEmulator.addSerialData(mSerialInput.front().second);
mSerialInput.pop_front();
}
}

public:
ArduinoSimulationController(ArduinoEmulator& emulator) : mEmulator(emulator)
{
Expand Down Expand Up @@ -140,6 +155,18 @@ class ArduinoSimulationController
}
}

void enqueueSerialInputEvent(const std::string& input, logtime_t delay = 0)
{
logtime_t time = mEmulator.mCurrentTime + delay;
if (!mSerialInput.empty() && mSerialInput.back().first > time) {
throw ArduinoEmulatorException("Adding serial input event at " + std::to_string(time)
+ " would violate ordering, since last event is already scheduled at "
+ std::to_string(mSerialInput.back().first) + ".");
}
mSerialInput.emplace_back(std::make_pair(time, input));
}


/**
* Clear all events for pin's queue.
*/
Expand All @@ -149,14 +176,22 @@ class ArduinoSimulationController
arduinoPin.clear();
}

/**
* Remove all scheduled serial events.
*/
void clearSerialInputEvents()
{
mSerialInput.clear();
}

/**
* Invoke the setup function.
* @param setupDelay How much is internal clock advanced after the setup.
*/
void runSetup(logtime_t setupDelay = 1)
{
mEmulator.invokeSetup();
mEmulator.advanceCurrentTimeBy(setupDelay);
advanceCurrentTimeBy(setupDelay);
}

/**
Expand All @@ -166,7 +201,7 @@ class ArduinoSimulationController
void runSingleLoop(logtime_t loopDelay = 1)
{
mEmulator.invokeLoop();
mEmulator.advanceCurrentTimeBy(loopDelay);
advanceCurrentTimeBy(loopDelay);
}

/**
Expand Down
Loading

0 comments on commit 69ba8c8

Please sign in to comment.