Overview
Latest code is available on GitHub, view the other articles in our load testing series
- Overview
- Installing the module
- Running from the CLI
- Importing and using the module
- Cabling
- Breakdown of the code
- AmmeterRecvSerial.__init__(…)
- AmmeterRecvSerial.ammeter_report()
- (property) AmmeterRecvSerial.ammeter_status()
- (property) AmmeterRecvSerial.ammeter_initialized()
- (property) AmmeterRecvSerial.ammeter_running()
- (property) AmmeterRecvSerial.ammeter_ready()
- (property) AmmeterRecvSerial.ammeter_config()
- (property+setter) AmmeterRecvSerial.ammeter_interval()
- AmmeterRecvSerial.ammeter_init()
- AmmeterRecvSerial.ammeter_start()
- AmmeterRecvSerial.ammeter_stop()
- AmmeterRecvSerial.ammeter_current()
- AmmeterRecvSerial._ammeter_read()
- AmmeterRecvSerial._ammeter_parse_read_line()
This project is part 2 of 2 to capture current data for our load testing. An ESP32 microcontroller will be used as a sender, and this project will cover the SBC device setup to receive. The device will be connected to the ESP32 via USB connected to a CP2102 USB to UART serial converter. At a later point I may add TCP connectivity, but for now we will use a serial interface. The python code will be running on a Raspberry Pi 4B, however since it is basic Python code using a USB serial interface, any computer with Python and a USB port can be used (NOTE: this has been tested on SBC’s running Linux only at this point).
Commands will be sent, and data received via the USB serial interface. Since we are using our Raspberry Pi 4 in this example, we will be using port /dev/ttyUSB0. The code is written as a Python module that can be installed into your Python virtual environment. The code can be run directly from the command line, or the module can be imported into another project. The end goal is to incorporate the current data into the Python SBC load testing tool (stay tuned!).
Installing the module
The package is available on PyPi or can be installed manually using the why/tar.gz file in the dist folder.
pip3 install ammeter_logger
Running from the CLI
After installing the module, from the command line the code can be executed using the following:
(venv) pi@pi:~/dev/ammeter_recv $ python3 -m ammeter_logger --help
usage: __main__.py [-h] [--get-config] [--get-status] [--skip-init] [--force-init] [--init-only]
[--sample-interval SAMPLE_INTERVAL] [--capture-time CAPTURE_TIME] [--baudrate BAUDRATE]
[--log-level LOG_LEVEL]
DEVICE OUTPUT_FILE
Start the ammeter data collector. Requires the sender to be running (provided sender is Micropython for a microcontroller)
positional arguments:
DEVICE Serial device connected to the microcontroller (i.e. /dev/ttyUSB0
OUTPUT_FILE File to save captured data to
optional arguments:
-h, --help show this help message and exit
--get-config (False) Get the configuration from the microcontroller and quit
--get-status (False) Get the current status of the microcontroller and quit
--skip-init (False) Skip initializing the ammeter (not recommended!)
--force-init (False) Force init of the ammeter
--init-only (False) Only initalize the ammeter, print the config and status and quit (implies --force-init)
--sample-interval SAMPLE_INTERVAL
Set the sampling interval, overrides the config on the microcontroller
--capture-time CAPTURE_TIME
Set the max time to capture before stopping, overrides the config on the microcontroller
--baudrate BAUDRATE (115200) Set the baudrate for the serial interface
--log-level LOG_LEVEL
(INFO) Specify the logging level for the console
(venv) pi@pi:~/dev/ammeter_recv $
Most of the options are self-explanatory. You must specify a device (I.e. /dev/ttyUSB0) and a csv file to log your data to. If the sensor has not been initialized, the initialization will run first. This needs to be done with no load to get a solid baseline. After initialization is complete, the sampling can be run for a number of seconds specified. Below are some examples that can be used:
CLI Example: Get Microcontroller Configuration
(venv) pi@pi:~/dev/ammeter_recv $ python3 -m ammeter_logger /dev/ttyUSB0 out.csv --get-config
2022-06-15 16:20:07,410 - root - INFO - AmmeterRecvSerial: /dev/ttyUSB0: 115200: 8N1: Starting backgroup ammeter read.
Current Config: {'interval': 50, 'timeout': 30, 'init_timeout': 30, 'pins': [{'pin': 32, 'name': 'sensor1pin32', 'baseline': 2884}]}
CLI Example: Get Current Microcontroller Status
(venv) pi@pi:~/dev/ammeter_recv $ python3 -m ammeter_logger /dev/ttyUSB0 out.csv --get-status
2022-06-15 16:20:33,899 - root - INFO - AmmeterRecvSerial: /dev/ttyUSB0: 115200: 8N1: Starting backgroup ammeter read.
Current Status: {'status': 'READY', 'timeout': 0, 'noinit_pin': None}
CLI Example: Initialize the microcontroller sensor
(venv) pi@pi:~/dev/ammeter_recv $ python3 -m ammeter_logger /dev/ttyUSB0 out.csv --init-only
2022-06-15 16:22:28,026 - root - INFO - AmmeterRecvSerial: /dev/ttyUSB0: 115200: 8N1: Starting backgroup ammeter read.
Waiting for ammeter to initialize. {'status': 'READY', 'timeout': 0, 'noinit_pin': None}
Waiting for ammeter to initialize. {'status': 'INITIALIZING', 'timeout': '30', 'noinit_pin': None}
Waiting for ammeter to initialize. {'status': 'INITIALIZING', 'timeout': '28', 'noinit_pin': None}
Waiting for ammeter to initialize. {'status': 'INITIALIZING', 'timeout': '26', 'noinit_pin': None}
…
Waiting for ammeter to initialize. {'status': 'INITIALIZING', 'timeout': '4', 'noinit_pin': None}
Waiting for ammeter to initialize. {'status': 'INITIALIZING', 'timeout': '2', 'noinit_pin': None}
Current Config: {'interval': 50, 'timeout': 30, 'init_timeout': 30, 'pins': [{'pin': 32, 'name': 'sensor1pin32', 'baseline': 2782}]}
Current Status: {'status': 'READY', 'timeout': 0, 'noinit_pin': None}
CLI Example: Start the logging
(venv) pi@pi:~/dev/ammeter_recv $ python3 -m ammeter_logger /dev/ttyUSB0 out.csv --capture-time 30
2022-06-15 16:23:30,004 - root - INFO - AmmeterRecvSerial: /dev/ttyUSB0: 115200: 8N1: Starting backgroup ammeter read.
Starting data collection. Collection will run for 30 seconds. You can stop at any point and write the captured data using CTRL+C.
Waiting for logging run to complete. Last amp read: -0.03868132. Current status: {'status': 'RUNNING', 'timeout': '30', 'noinit_pin': None}
Waiting for logging run to complete. Last amp read: -0.03142858. Current status: {'status': 'RUNNING', 'timeout': '29', 'noinit_pin': None}
Waiting for logging run to complete. Last amp read: 0.06285715. Current status: {'status': 'RUNNING', 'timeout': '27', 'noinit_pin': None}
…
Waiting for logging run to complete. Last amp read: -0.0410989. Current status: {'status': 'RUNNING', 'timeout': '5', 'noinit_pin': None}
Waiting for logging run to complete. Last amp read: 0.029011. Current status: {'status': 'RUNNING', 'timeout': '3', 'noinit_pin': None}
Waiting for logging run to complete. Last amp read: 0.003223445. Current status: {'status': 'RUNNING', 'timeout': '1', 'noinit_pin': None}
Writing log to file out.csv...
Writing complete.
Importing and using the module
The code can also be imported into another project. This will be used as a part of our Python SBC load testing. The ammeter_logger module will be used to capture amperage data while other load tests are running, This will allow us to map power usage to each of our tests.
from ammeter_logger import AmmeterRecvSerial
ser = AmmeterRecvSerial(device=’/dev/ttyUSB0’, baudrate=115200)
Since the AmmeterRecvSerial class inherits the serial.Serial class, any paramter available in the serial.Serial class can be use. See the pySerial project for details on supported parameters.
Cabling
Since we used a CP2102 in our ADC Current Sensor Project [LINK], for this project we need only to connect the serial port on the CP2102 to a USB port on the device that will be collecting the data. The CP2102 will appear as a USB serial interface.
For logging amperage data during the boot of an SBC or microcontroller, we will use a Raspberry Pi 4B to collect the data, however any device running a modern version of Linux with a USB port will suffice (I have not tested the Python code on Windows or Mac).
For logging amperage data during the load test (keep an eye on our load testing [LINK] series for more), we will connect the USB port of the CP2102 directly to the device we are load testing. This will allow our load testing package to collect amperage data for each phase of testing in real-time.
Breakdown of the code
The AmmeterRecvSerial extends the serial.Serial class since all of our communication will be using the serial bus. This allows us to add just the functions we need that are specific to the microcontroller current sensor.
AmmeterRecvSerial.__init__(…)
The init function pops the logger parameter before calling the serial.Serial init function using the “super().__init__(*args, **kwargs)” call. This will pass all parameters (other than the logger) to the base class to initialize the serial device. All serial parameters supported by serial.Serial like device, baudrate, stop bits, etc can be passed to the AmmeterRecvSerial class.
AmmeterRecvSerial.ammeter_report()
The report function returns the actual data that was collected. The report will be returned as a dictionary that consists of the following:
{
‘start’: (datetime object representing the start time of the data),
‘stop’: (datetime object representing the stop time of the data),
‘data’: [ (list of data entries that were received)
{
‘received’: (EPOCH time the data was received),
‘name’: (name of the sensor from the microcontroller),
‘ticks’: (number of microseconds from the start reported by the microcontroller),
‘current_amps’: (latest reading, not averaged),
‘last_reads’: [(list of readings that were used for the average)],
‘average’: (average amperage from the list of reads above)
},
… (repeat for all received data points)
]
}
If you would like to export to CSV, you can take a look at the write_log_data function in the __main__.py file. This function is used to export the data when calling the module directly using “python3 -m ammeter_logger …”
(property) AmmeterRecvSerial.ammeter_status()
The status property will clear the currently cached status and query the microcontroller for an updated status. The data will be returned as a dict with the following:
{
‘status’: (string showing the current status: RUNNING, NOINIT, INITIALIZING, READY)
‘timeout’: (number of seconds remaining in initialization or run if applicable else 0),
‘noinit_pin’: (if status is NOINIT, reports the input pin that isn’t initialized, else None)
}
(property) AmmeterRecvSerial.ammeter_initialized()
Returns a True / False after querying the microcontroller for a status.
(property) AmmeterRecvSerial.ammeter_running()
Returns a True / False after querying the microcontroller for a status.
(property) AmmeterRecvSerial.ammeter_ready()
Returns a True / False after querying the microcontroller for a status.
(property) AmmeterRecvSerial.ammeter_config()
Returns the configuration from the microcontroller in the following format:
{
‘interval’: (read interval in milliseconds)
‘timeout’: (timeout for reading data in seconds)
‘init_timeout’: (timeout for initializing the ADC sensor and creating the baseline)
‘pins’: [ (list of pins to read data from)
{
‘pin’: (pin number on the microcontroller)
‘name’: (friendly name to use for the sensor on this pin)
‘baseline’: (baseline ‘0’ calculated after the initialization)
]
}
(property+setter) AmmeterRecvSerial.ammeter_interval()
The property and setter can be used to get the currently configured read interval and to set a new read interval. After setting the interval, the setter will check the interval from the microcontroller to ensure that the configuration was accepted. True is returned from the setter if the value was successfully applied, false if not.
AmmeterRecvSerial.ammeter_init()
Requests the initialization to run on the microcontroller. This is intended to get a zero baseline, so there should be no load on the sensor prior to running the initialization. The initialization will run for the ‘init_timeout’ length in seconds from the configuration (see above).
AmmeterRecvSerial.ammeter_start()
Requests the microcontroller to start the read. Once the read is started, the microcontroller will start returning data at each read interval that the receiver will need to record and store. The microcontroller does not maintain any data beyond the last few reads needed to create an average.
AmmeterRecvSerial.ammeter_stop()
Requests the microcontroller to stop any read currently in progress. The microcontroller may take 1-2 read intervals to stop and return the final stop message.
AmmeterRecvSerial.ammeter_current()
Requests the microcontroller to perform a single read. The single read will still make multiple reads based on the configuration, will discard the highest and lowest value, then average and return the result.
AmmeterRecvSerial._ammeter_read()
This is an internal use function that runs as a thread to check the serial ports input queue every 100ms for received data.
AmmeterRecvSerial._ammeter_parse_read_line()
This is an internal use function that will parse any data that has been received on the serial port. This function will parse the return data to determine what type of message was received, then store the data.