pytec: measure interval

pull/36/head
Astro 2020-12-16 22:14:21 +01:00
parent 93ea46d512
commit b7e6cdbec2
7 changed files with 169 additions and 171 deletions

View File

@ -89,7 +89,10 @@ Set report mode to `on` for a continuous stream of input data.
The scope of this setting is per TCP session. The scope of this setting is per TCP session.
### Commands ### TCP commands
Send commands as simple text string terminated by `\n`. Responses are
formatted as line-delimited JSON.
| Syntax | Function | | Syntax | Function |
| --- | --- | | --- | --- |

View File

@ -2,7 +2,10 @@ from pytec.client import Client
tec = Client() #(host="localhost", port=6667) tec = Client() #(host="localhost", port=6667)
tec.set_param("s-h", 1, "t0", 20) tec.set_param("s-h", 1, "t0", 20)
print(tec.get_pwm())
print(tec.get_pid()) print(tec.get_pid())
print(tec.get_pwm())
print(tec.get_postfilter())
print(tec.get_steinhart_hart()) print(tec.get_steinhart_hart())
for data in tec.report_mode(): for data in tec.report_mode():
print(data) print(data)

View File

@ -39,21 +39,29 @@ series = {
'i_tec': Series(), 'i_tec': Series(),
'tec_i': Series(), 'tec_i': Series(),
'tec_u_meas': Series(), 'tec_u_meas': Series(),
'interval': Series(),
} }
series_lock = Lock() series_lock = Lock()
quit = False quit = False
last_packet_time = None
def recv_data(tec): def recv_data(tec):
global last_packet_time
for data in tec.report_mode(): for data in tec.report_mode():
if data['channel'] == 0: if data['channel'] == 0:
series_lock.acquire() series_lock.acquire()
try: try:
time = data['time'] / 1000.0 time = data['time'] / 1000.0
if last_packet_time:
data['interval'] = time - last_packet_time
last_packet_time = time
for k, s in series.items(): for k, s in series.items():
v = data[k] if k in data:
if k in data and type(v) is float: v = data[k]
s.append(time, v) if type(v) is float:
s.append(time, v)
finally: finally:
series_lock.release() series_lock.release()
@ -108,9 +116,11 @@ def animate(i):
finally: finally:
series_lock.release() series_lock.release()
margin_y = 0.01 * (max_y - min_y) if min_x != max_x:
ax.set_xlim(min_x, max_x) ax.set_xlim(min_x, max_x)
ax.set_ylim(min_y - margin_y, max_y + margin_y) if min_y != max_y:
margin_y = 0.01 * (max_y - min_y)
ax.set_ylim(min_y - margin_y, max_y + margin_y)
global legend global legend
legend.remove() legend.remove()

View File

@ -1,16 +1,14 @@
import socket import socket
import json import json
CHANNELS = 2 class CommandError(Exception):
pass
class Client: class Client:
def __init__(self, host="192.168.1.26", port=23, timeout=None): def __init__(self, host="192.168.1.26", port=23, timeout=None):
self._socket = socket.create_connection((host, port), timeout) self._socket = socket.create_connection((host, port), timeout)
self._lines = [""] self._lines = [""]
def _command(self, *command):
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
def _read_line(self): def _read_line(self):
# read more lines # read more lines
while len(self._lines) <= 1: while len(self._lines) <= 1:
@ -24,13 +22,19 @@ class Client:
self._lines = self._lines[1:] self._lines = self._lines[1:]
return line return line
def _command(self, *command):
self._socket.sendall((" ".join(command) + "\n").encode('utf-8'))
line = self._read_line()
response = json.loads(line)
if "error" in response:
raise CommandError(response["error"])
return response
def _get_conf(self, topic): def _get_conf(self, topic):
self._command(topic) result = [None, None]
result = [] for item in self._command(topic):
for channel in range(0, CHANNELS): result[int(item["channel"])] = item
line = self._read_line()
conf = json.loads(line)
result.append(conf)
return result return result
def get_pwm(self): def get_pwm(self):

View File

@ -1,3 +1,4 @@
use heapless::{consts::{U2, U1024}, Vec};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use smoltcp::time::Instant; use smoltcp::time::Instant;
use stm32f4xx_hal::hal; use stm32f4xx_hal::hal;
@ -412,7 +413,7 @@ impl Channels {
(duty * max, max) (duty * max, max)
} }
pub fn report(&mut self, channel: usize) -> Report { fn report(&mut self, channel: usize) -> Report {
let vref = self.channel_state(channel).vref; let vref = self.channel_state(channel).vref;
let (i_set, _) = self.get_i(channel); let (i_set, _) = self.get_i(channel);
let i_tec = self.read_itec(channel); let i_tec = self.read_itec(channel);
@ -441,7 +442,23 @@ impl Channels {
} }
} }
pub fn pwm_summary(&mut self, channel: usize) -> PwmSummary { pub fn reports_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut reports = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = reports.push(self.report(channel));
}
serde_json_core::to_vec(&reports)
}
pub fn pid_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.channel_state(channel).pid.summary(channel));
}
serde_json_core::to_vec(&summaries)
}
fn pwm_summary(&mut self, channel: usize) -> PwmSummary {
PwmSummary { PwmSummary {
channel, channel,
center: CenterPointJson(self.channel_state(channel).center.clone()), center: CenterPointJson(self.channel_state(channel).center.clone()),
@ -452,19 +469,43 @@ impl Channels {
} }
} }
pub fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary { pub fn pwm_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.pwm_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
fn postfilter_summary(&mut self, channel: usize) -> PostFilterSummary {
let rate = self.adc.get_postfilter(channel as u8).unwrap() let rate = self.adc.get_postfilter(channel as u8).unwrap()
.and_then(|filter| filter.output_rate()); .and_then(|filter| filter.output_rate());
PostFilterSummary { channel, rate } PostFilterSummary { channel, rate }
} }
pub fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary { pub fn postfilter_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.postfilter_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
fn steinhart_hart_summary(&mut self, channel: usize) -> SteinhartHartSummary {
let params = self.channel_state(channel).sh.clone(); let params = self.channel_state(channel).sh.clone();
SteinhartHartSummary { channel, params } SteinhartHartSummary { channel, params }
} }
pub fn steinhart_hart_summaries_json(&mut self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
let mut summaries = Vec::<_, U2>::new();
for channel in 0..CHANNELS {
let _ = summaries.push(self.steinhart_hart_summary(channel));
}
serde_json_core::to_vec(&summaries)
}
} }
type JsonBuffer = heapless::Vec<u8, heapless::consts::U512>; type JsonBuffer = Vec<u8, U1024>;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Report { pub struct Report {
@ -484,12 +525,6 @@ pub struct Report {
pid_output: Option<ElectricCurrent>, pid_output: Option<ElectricCurrent>,
} }
impl Report {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
pub struct CenterPointJson(CenterPoint); pub struct CenterPointJson(CenterPoint);
// used in JSON encoding, not for config // used in JSON encoding, not for config
@ -529,91 +564,14 @@ pub struct PwmSummary {
max_i_neg: PwmSummaryField<ElectricCurrent>, max_i_neg: PwmSummaryField<ElectricCurrent>,
} }
impl PwmSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct PostFilterSummary { pub struct PostFilterSummary {
channel: usize, channel: usize,
rate: Option<f32>, rate: Option<f32>,
} }
impl PostFilterSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct SteinhartHartSummary { pub struct SteinhartHartSummary {
channel: usize, channel: usize,
params: steinhart_hart::Parameters, params: steinhart_hart::Parameters,
} }
impl SteinhartHartSummary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn report_to_json() {
// `/ 1.1` results in values with a really long serialization
let report = Report {
channel: 0,
time: 3200,
adc: Some(ElectricPotential::new::<volt>(0.65 / 1.1)),
sens: Some(ElectricalResistance::new::<ohm>(10000.0 / 1.1)),
temperature: Some(30.0 / 1.1),
pid_engaged: false,
i_set: ElectricCurrent::new::<ampere>(0.5 / 1.1),
vref: ElectricPotential::new::<volt>(1.5 / 1.1),
dac_value: ElectricPotential::new::<volt>(2.0 / 1.1),
dac_feedback: ElectricPotential::new::<volt>(2.0 / 1.1),
i_tec: ElectricPotential::new::<volt>(2.0 / 1.1),
tec_i: ElectricCurrent::new::<ampere>(0.2 / 1.1),
tec_u_meas: ElectricPotential::new::<volt>(2.0 / 1.1),
pid_output: Some(ElectricCurrent::new::<ampere>(0.5 / 1.1)),
};
let buf = report.to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
#[test]
fn pwm_summary_to_json() {
let value = 1.0 / 1.1;
let max = 5.0 / 1.1;
let pwm_summary = PwmSummary {
channel: 0,
center: CenterPointJson(CenterPoint::Vref),
i_set: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
max_v: PwmSummaryField {
value: ElectricPotential::new::<volt>(value),
max: ElectricPotential::new::<volt>(max),
},
max_i_pos: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
max_i_neg: PwmSummaryField {
value: ElectricCurrent::new::<ampere>(value),
max: ElectricCurrent::new::<ampere>(max),
},
};
let buf = pwm_summary.to_json().unwrap();
assert_eq!(buf[0], b'{');
assert_eq!(buf[buf.len() - 1], b'}');
}
}

View File

@ -81,7 +81,8 @@ const TCP_PORT: u16 = 23;
fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool { fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
let send_free = socket.send_capacity() - socket.send_queue(); let send_free = socket.send_capacity() - socket.send_queue();
if data.len() > send_free + 1 { if data.len() > send_free + 1 {
// Not enough buffer space, skip report for now // Not enough buffer space, skip report for now,
// instead of sending incomplete line
warn!( warn!(
"TCP socket has only {}/{} needed {}", "TCP socket has only {}/{} needed {}",
send_free + 1, socket.send_capacity(), data.len(), send_free + 1, socket.send_capacity(), data.len(),
@ -103,17 +104,6 @@ fn send_line(socket: &mut TcpSocket, data: &[u8]) -> bool {
false false
} }
fn report_to(channel: usize, channels: &mut Channels, socket: &mut TcpSocket) -> bool {
match channels.report(channel).to_json() {
Ok(buf) =>
send_line(socket, &buf[..]),
Err(e) => {
error!("unable to serialize report: {:?}", e);
false
}
}
}
/// Initialization and main loop /// Initialization and main loop
#[cfg(not(test))] #[cfg(not(test))]
#[entry] #[entry]
@ -221,68 +211,79 @@ fn main() -> ! {
socket.close() socket.close()
} else if socket.can_send() && socket.can_recv() { } else if socket.can_send() && socket.can_recv() {
match socket.recv(|buf| session.feed(buf)) { match socket.recv(|buf| session.feed(buf)) {
Ok(SessionInput::Nothing) => {} Ok(SessionInput::Nothing) => {
send_line(&mut socket, b"{}");
}
Ok(SessionInput::Command(command)) => match command { Ok(SessionInput::Command(command)) => match command {
Command::Quit => Command::Quit =>
socket.close(), socket.close(),
Command::Reporting(_reporting) => { Command::Reporting(_reporting) => {
// handled by session // handled by session
send_line(&mut socket, b"{}");
} }
Command::Show(ShowCommand::Reporting) => { Command::Show(ShowCommand::Reporting) => {
let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting()); let _ = writeln!(socket, "{{ \"report\": {:?} }}", session.reporting());
} }
Command::Show(ShowCommand::Input) => { Command::Show(ShowCommand::Input) => {
for channel in 0..CHANNELS { match channels.reports_json() {
report_to(channel, &mut channels, &mut socket); Ok(buf) => {
send_line(&mut socket, &buf[..]);
}
Err(e) => {
error!("unable to serialize report: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
}
} }
} }
Command::Show(ShowCommand::Pid) => { Command::Show(ShowCommand::Pid) => {
for channel in 0..CHANNELS { match channels.pid_summaries_json() {
match channels.channel_state(channel).pid.summary(channel).to_json() { Ok(buf) => {
Ok(buf) => { send_line(&mut socket, &buf);
send_line(&mut socket, &buf); }
} Err(e) => {
Err(e) => error!("unable to serialize pid summary: {:?}", e);
error!("unable to serialize pid summary: {:?}", e), let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
} }
} }
} }
Command::Show(ShowCommand::Pwm) => { Command::Show(ShowCommand::Pwm) => {
for channel in 0..CHANNELS { match channels.pwm_summaries_json() {
match channels.pwm_summary(channel).to_json() { Ok(buf) => {
Ok(buf) => { send_line(&mut socket, &buf);
send_line(&mut socket, &buf); }
} Err(e) => {
Err(e) => error!("unable to serialize pwm summary: {:?}", e);
error!("unable to serialize pwm summary: {:?}", e), let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
} }
} }
} }
Command::Show(ShowCommand::SteinhartHart) => { Command::Show(ShowCommand::SteinhartHart) => {
for channel in 0..CHANNELS { match channels.steinhart_hart_summaries_json() {
match channels.steinhart_hart_summary(channel).to_json() { Ok(buf) => {
Ok(buf) => { send_line(&mut socket, &buf);
send_line(&mut socket, &buf); }
} Err(e) => {
Err(e) => error!("unable to serialize steinhart-hart summaries: {:?}", e);
error!("unable to serialize steinhart-hart summary: {:?}", e), let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
} }
} }
} }
Command::Show(ShowCommand::PostFilter) => { Command::Show(ShowCommand::PostFilter) => {
for channel in 0..CHANNELS { match channels.postfilter_summaries_json() {
match channels.postfilter_summary(channel).to_json() { Ok(buf) => {
Ok(buf) => { send_line(&mut socket, &buf);
send_line(&mut socket, &buf); }
} Err(e) => {
Err(e) => error!("unable to serialize postfilter summary: {:?}", e);
error!("unable to serialize postfilter summary: {:?}", e), let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
} }
} }
} }
Command::PwmPid { channel } => { Command::PwmPid { channel } => {
channels.channel_state(channel).pid_engaged = true; channels.channel_state(channel).pid_engaged = true;
leds.g3.on(); leds.g3.on();
send_line(&mut socket, b"{}");
} }
Command::Pwm { channel, pin, value } => { Command::Pwm { channel, pin, value } => {
match pin { match pin {
@ -306,6 +307,7 @@ fn main() -> ! {
channels.set_max_i_neg(channel, current); channels.set_max_i_neg(channel, current);
} }
} }
send_line(&mut socket, b"{}");
} }
Command::CenterPoint { channel, center } => { Command::CenterPoint { channel, center } => {
let (i_tec, _) = channels.get_i(channel); let (i_tec, _) = channels.get_i(channel);
@ -314,6 +316,7 @@ fn main() -> ! {
if !state.pid_engaged { if !state.pid_engaged {
channels.set_i(channel, i_tec); channels.set_i(channel, i_tec);
} }
send_line(&mut socket, b"{}");
} }
Command::Pid { channel, parameter, value } => { Command::Pid { channel, parameter, value } => {
let pid = &mut channels.channel_state(channel).pid; let pid = &mut channels.channel_state(channel).pid;
@ -336,6 +339,7 @@ fn main() -> ! {
IntegralMax => IntegralMax =>
pid.parameters.integral_max = value as f32, pid.parameters.integral_max = value as f32,
} }
send_line(&mut socket, b"{}");
} }
Command::SteinhartHart { channel, parameter, value } => { Command::SteinhartHart { channel, parameter, value } => {
let sh = &mut channels.channel_state(channel).sh; let sh = &mut channels.channel_state(channel).sh;
@ -345,29 +349,41 @@ fn main() -> ! {
B => sh.b = value, B => sh.b = value,
R0 => sh.r0 = ElectricalResistance::new::<ohm>(value), R0 => sh.r0 = ElectricalResistance::new::<ohm>(value),
} }
send_line(&mut socket, b"{}");
} }
Command::PostFilter { channel, rate: None } => { Command::PostFilter { channel, rate: None } => {
channels.adc.set_postfilter(channel as u8, None).unwrap(); channels.adc.set_postfilter(channel as u8, None).unwrap();
send_line(&mut socket, b"{}");
} }
Command::PostFilter { channel, rate: Some(rate) } => { Command::PostFilter { channel, rate: Some(rate) } => {
let filter = ad7172::PostFilter::closest(rate); let filter = ad7172::PostFilter::closest(rate);
match filter { match filter {
Some(filter) => Some(filter) => {
channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap(), channels.adc.set_postfilter(channel as u8, Some(filter)).unwrap();
None => send_line(&mut socket, b"{}");
error!("unable to choose postfilter for rate {:.3}", rate), }
None => {
error!("unable to choose postfilter for rate {:.3}", rate);
send_line(&mut socket, b"{{\"error\": \"unable to choose postfilter rate\"}}");
}
} }
} }
Command::Load { channel } => { Command::Load { channel } => {
for c in 0..CHANNELS { for c in 0..CHANNELS {
if channel.is_none() || channel == Some(c) { if channel.is_none() || channel == Some(c) {
match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) { match store.read_value::<ChannelConfig>(CHANNEL_CONFIG_KEY[c]) {
Ok(Some(config)) => Ok(Some(config)) => {
config.apply(&mut channels, c), config.apply(&mut channels, c);
Ok(None) => send_line(&mut socket, b"{}");
error!("flash config not found"), }
Err(e) => Ok(None) => {
error!("unable to load config from flash: {:?}", e), error!("flash config not found");
send_line(&mut socket, b"{{\"error\": \"flash config not found\"}}");
}
Err(e) => {
error!("unable to load config from flash: {:?}", e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
}
} }
} }
} }
@ -376,9 +392,13 @@ fn main() -> ! {
for c in 0..CHANNELS { for c in 0..CHANNELS {
if channel.is_none() || channel == Some(c) { if channel.is_none() || channel == Some(c) {
let config = ChannelConfig::new(&mut channels, c); let config = ChannelConfig::new(&mut channels, c);
let _ = store match store.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) {
.write_value(CHANNEL_CONFIG_KEY[c], &config, &mut store_value_buf) Ok(()) => {}
.map_err(|e| error!("unable to save channel {} config to flash: {:?}", c, e)); Err(e) => {
error!("unable to save channel {} config to flash: {:?}", c, e);
let _ = writeln!(socket, "{{\"error\":\"{:?}\"}}", e);
}
}
} }
} }
} }
@ -387,6 +407,7 @@ fn main() -> ! {
.write_value("ipv4", &config, [0; 16]) .write_value("ipv4", &config, [0; 16])
.map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e)); .map_err(|e| error!("unable to save ipv4 config to flash: {:?}", e));
new_ipv4_config = Some(config); new_ipv4_config = Some(config);
send_line(&mut socket, b"{}");
} }
Command::Reset => { Command::Reset => {
for i in 0..CHANNELS { for i in 0..CHANNELS {
@ -405,8 +426,15 @@ fn main() -> ! {
} }
} else if socket.can_send() { } else if socket.can_send() {
if let Some(channel) = session.is_report_pending() { if let Some(channel) = session.is_report_pending() {
if report_to(channel, &mut channels, &mut socket) { match channels.reports_json() {
session.mark_report_sent(channel); Ok(buf) => {
send_line(&mut socket, &buf[..]);
session.mark_report_sent(channel);
}
Err(e) => {
error!("unable to serialize report: {:?}", e);
}
} }
} }
} }

View File

@ -91,8 +91,6 @@ impl Controller {
} }
} }
type JsonBuffer = heapless::Vec<u8, heapless::consts::U360>;
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Summary { pub struct Summary {
channel: usize, channel: usize,
@ -101,12 +99,6 @@ pub struct Summary {
integral: f64, integral: f64,
} }
impl Summary {
pub fn to_json(&self) -> Result<JsonBuffer, serde_json_core::ser::Error> {
serde_json_core::to_vec(self)
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;