Reading MSP430 LaunchPad temperature with Python
Read temperature samples from an MSP430 LaunchPad over serial, print Celsius readings, and plot a bounded history with Python.
A small Python script turns temperature samples sent by an MSP430 LaunchPad into terminal output and a live graph. This example was recovered from screenshots of an earlier experiment and updated in place for Python 3. The article date records the extraction, not the original experiment.
What the board must send
The default serial connection is /dev/ttyACM0 at 2400 baud. Each byte's numeric value represents a temperature in whole degrees Fahrenheit. The script does not parse decimal text, lines, or ADC register values. For example, byte 75, displayed as K, converts to approximately 23.89 °C; byte 78, displayed as N, converts to approximately 25.56 °C.
This is the protocol assumed by the recovered host code. The firmware, exact LaunchPad model, sensor configuration, and calibration are not available. A connected board must already transmit compatible samples; the historical screenshot does not establish ambient-temperature accuracy.
The script
The dependencies remain pySerial, NumPy, the Gnuplot Python module, and the Gnuplot executable. Use Python 3 with compatible versions of those packages; local checks used Python 3.12. Plotting requires a module exposing an explicit Gnuplot.close() method so the script can request cleanup. --no-plot runs the temperature reader without opening Gnuplot.
#!/usr/bin/env python3
"""Read binary Fahrenheit samples from a LaunchPad and report Celsius.
Dependencies: pySerial, NumPy, and (unless --no-plot) Gnuplot plus its executable.
All 256 byte values are samples; ASCII numbers and delimiters are not parsed.
"""
import argparse
from collections import deque
from contextlib import ExitStack, contextmanager
from dataclasses import dataclass
import sys
import time
from typing import Callable, Iterator, Optional
import numpy as np
MAX_BATCH_BYTES = 65536
MAX_HISTORY_POINTS = 10000
ReadBytes = Callable[[int], bytes]
PlotSample = Callable[[float, float], None]
def bounded_integer(value: int, name: str, maximum: int) -> None:
if type(value) is not int or not 1 <= value <= maximum:
raise ValueError(f'{name} must be an integer between 1 and {maximum}')
def validate_interval(value: float) -> None:
if type(value) not in (int, float):
raise ValueError('interval must be a number')
if not 0.01 <= value <= 3600:
raise ValueError('interval must be finite and between 0.01 and 3600 seconds')
def validate_port(value: str) -> None:
if not isinstance(value, str) or not value.strip() or '\x00' in value:
raise ValueError('port must be a nonempty device name without NUL characters')
@dataclass(frozen=True)
class Settings:
port: str = '/dev/ttyACM0'
baud: int = 2400
interval: float = 3.0
batch_limit: int = 4096
history_limit: int = 1000
plot: bool = True
def __post_init__(self) -> None:
validate_port(self.port)
bounded_integer(self.baud, 'baud', 4_000_000)
validate_interval(self.interval)
bounded_integer(self.batch_limit, 'batch limit', MAX_BATCH_BYTES)
bounded_integer(self.history_limit, 'history limit', MAX_HISTORY_POINTS)
if type(self.plot) is not bool:
raise ValueError('plot must be a boolean')
def parse_arguments(argv=None) -> Settings:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', default='/dev/ttyACM0')
parser.add_argument('--baud', type=int, default=2400)
parser.add_argument('--interval', type=float, default=3.0, help='poll period in seconds')
parser.add_argument('--batch-limit', type=int, default=4096, help='maximum bytes per poll')
parser.add_argument('--history-limit', type=int, default=1000, help='retained graph points')
parser.add_argument('--no-plot', dest='plot', action='store_false', help='print temperatures only')
try:
return Settings(**vars(parser.parse_args(argv)))
except ValueError as error:
parser.error(str(error))
def read_batch(read: ReadBytes, limit: int) -> bytes:
"""Borrow a reader; admit at most limit immutable sample bytes per poll."""
bounded_integer(limit, 'batch limit', MAX_BATCH_BYTES)
raw = read(limit)
if type(raw) is not bytes:
raise TypeError('serial reader must return bytes')
if len(raw) > limit:
raise ValueError('serial reader exceeded the requested batch limit')
return raw
def mean_celsius(raw: bytes) -> Optional[float]:
"""Borrow bounded immutable bytes for this call; retain no NumPy view."""
if type(raw) is not bytes:
raise TypeError('samples must be bytes')
if len(raw) > MAX_BATCH_BYTES:
raise ValueError('sample batch exceeds capacity')
if not raw:
return None
fahrenheit = float(np.frombuffer(raw, dtype=np.uint8).mean())
return (fahrenheit - 32) * 5 / 9
def report_sample(raw: bytes, temperature: float, output=print) -> None:
output(f'seq:{raw!r} temp:{temperature:.6f} C', flush=True)
def monitor(read: ReadBytes, settings: Settings, plot: Optional[PlotSample] = None,
*, clock=time.monotonic, sleep=time.sleep, output=print) -> None:
"""Borrow adapters until return; overrun starts the next poll without delay."""
start = clock()
while True:
deadline = clock() + settings.interval
raw = read_batch(read, settings.batch_limit)
temperature = mean_celsius(raw)
if temperature is not None:
report_sample(raw, temperature, output)
if plot is not None:
plot((clock() - start) / 60, temperature)
sleep(max(0.0, deadline - clock()))
class TemperaturePlot:
"""Own bounded history; borrow the plotter only while its context is open."""
def __init__(self, plot: Callable[[object], None], data_factory, history_limit: int):
bounded_integer(history_limit, 'history limit', MAX_HISTORY_POINTS)
self._points = deque(maxlen=history_limit)
self._plot = plot
self._data_factory = data_factory
def add(self, elapsed: float, temperature: float) -> None:
if self._plot is None:
raise RuntimeError('plot has been closed')
self._points.append((elapsed, temperature))
self._plot(self._data_factory(list(self._points), with_='lines'))
def close(self) -> None:
self._plot = None
self._data_factory = None
self._points.clear()
@contextmanager
def open_plot(history_limit: int) -> Iterator[PlotSample]:
import Gnuplot
if not callable(getattr(Gnuplot.Gnuplot, 'close', None)):
raise RuntimeError('Gnuplot must provide an explicit Gnuplot.close() method')
with ExitStack() as stack:
plot = Gnuplot.Gnuplot()
stack.callback(plot.close)
plot.xlabel('Elapsed time (minutes)')
plot.ylabel('Temperature (C)')
sink = TemperaturePlot(plot.plot, Gnuplot.Data, history_limit)
stack.callback(sink.close)
yield sink.add
@contextmanager
def open_serial(settings: Settings) -> Iterator[ReadBytes]:
import serial
with serial.Serial(settings.port, settings.baud, timeout=0) as port:
yield port.read
def run(settings: Settings) -> None:
with ExitStack() as stack:
read = stack.enter_context(open_serial(settings))
plot = None
if settings.plot:
plot = stack.enter_context(open_plot(settings.history_limit))
monitor(read, settings, plot)
def main(argv=None) -> int:
settings = parse_arguments(argv)
try:
run(settings)
except KeyboardInterrupt:
return 130
except (ImportError, OSError, RuntimeError, ValueError) as error:
print(f'Error: {error}', file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
sys.exit(main())
Running it
Connect the board, identify its serial device, and ensure your user can open it. With the dependencies already installed, run:
python3 launchpad_temperature.py --port /dev/ttyACM0
For terminal output alone:
python3 launchpad_temperature.py --port /dev/ttyACM0 --no-plot
Use --help for connection and capacity settings. The default poll period is three seconds, each read admits at most 4096 bytes, and the graph retains the latest 1000 batch means. The configurable limits are 65536 bytes per read and 10000 graph points; the interval must be between 0.01 and 3600 seconds. Samples left in the device buffer are read on later polls; the host does not control the board's sampling frequency or guarantee against device-buffer overflow.
Reading the output
Each nonempty batch is averaged and converted with (F - 32) * 5 / 9. The raw bytes use an escaped representation so control characters cannot alter the terminal display. A batch of K samples produces:
seq:b'KKKKKKKKK' temp:23.888889 C
The graph adds one point per nonempty batch. Its horizontal axis is elapsed time in minutes; its vertical axis is the batch mean in Celsius.
Timing and cleanup
The reader uses a monotonic clock. If processing exceeds the selected period, the next poll starts without an additional delay. Press Ctrl+C to stop. Cleanup first invalidates the plotting adapter and clears its bounded history, then calls the plotter's close method and closes the serial connection; partial-startup and runtime failures also take the cleanup path.
Automated checks cover byte conversion, invalid inputs, timing overruns, bounded history, and cleanup. The serial adapter was also exercised through a local Linux pseudo-terminal. A physical LaunchPad and the Gnuplot module/executable are unavailable here: board communication, GUI behavior, and native Gnuplot process termination remain unverified.