use async_trait::async_trait;
use endbasic_core::ast::{ArgSep, ExprType};
use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax};
use endbasic_core::exec::{Clearable, Machine, Scope};
use endbasic_core::syms::{
CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, Symbols,
};
use endbasic_core::LineCol;
use std::borrow::Cow;
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
use std::result::Result;
mod fakes;
pub(crate) use fakes::{MockPins, NoopPins};
const CATEGORY: &str = "Hardware interface
EndBASIC provides features to manipulate external hardware. These features are currently limited \
to GPIO interaction on a Raspberry Pi and are only available when EndBASIC has explicitly been \
built with the --features=rpi option. Support for other busses and platforms may come later.";
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Pin(pub u8);
impl Pin {
fn from_i32(i: i32, pos: LineCol) -> Result<Self, CallError> {
if i < 0 {
return Err(CallError::ArgumentError(
pos,
format!("Pin number {} must be positive", i),
));
}
if i > u8::MAX as i32 {
return Err(CallError::ArgumentError(pos, format!("Pin number {} is too large", i)));
}
Ok(Self(i as u8))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PinMode {
In,
InPullDown,
InPullUp,
Out,
}
impl PinMode {
fn parse(s: &str, pos: LineCol) -> Result<PinMode, CallError> {
match s.to_ascii_uppercase().as_ref() {
"IN" => Ok(PinMode::In),
"IN-PULL-UP" => Ok(PinMode::InPullUp),
"IN-PULL-DOWN" => Ok(PinMode::InPullDown),
"OUT" => Ok(PinMode::Out),
s => Err(CallError::ArgumentError(pos, format!("Unknown pin mode {}", s))),
}
}
}
pub trait Pins {
fn setup(&mut self, pin: Pin, mode: PinMode) -> io::Result<()>;
fn clear(&mut self, pin: Pin) -> io::Result<()>;
fn clear_all(&mut self) -> io::Result<()>;
fn read(&mut self, pin: Pin) -> io::Result<bool>;
fn write(&mut self, pin: Pin, v: bool) -> io::Result<()>;
}
pub(crate) struct PinsClearable {
pins: Rc<RefCell<dyn Pins>>,
}
impl PinsClearable {
pub(crate) fn new(pins: Rc<RefCell<dyn Pins>>) -> Box<Self> {
Box::from(Self { pins })
}
}
impl Clearable for PinsClearable {
fn reset_state(&self, syms: &mut Symbols) {
let _ = match MockPins::try_new(syms) {
Some(mut pins) => pins.clear_all(),
None => self.pins.borrow_mut().clear_all(),
};
}
}
pub struct GpioSetupCommand {
metadata: CallableMetadata,
pins: Rc<RefCell<dyn Pins>>,
}
impl GpioSetupCommand {
pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("GPIO_SETUP")
.with_syntax(&[(
&[
SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("pin"),
vtype: ExprType::Integer,
},
ArgSepSyntax::Exactly(ArgSep::Long),
),
SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("mode"),
vtype: ExprType::Text,
},
ArgSepSyntax::End,
),
],
None,
)])
.with_category(CATEGORY)
.with_description(
"Configures a GPIO pin for input or output.
Before a GPIO pin can be used for reads or writes, it must be configured to be an input or \
output pin. Additionally, if pull up or pull down resistors are available and desired, these \
must be configured upfront too.
The mode$ has to be one of \"IN\", \"IN-PULL-DOWN\", \"IN-PULL-UP\", or \"OUT\". These values \
are case-insensitive. The possibility of using the pull-down and pull-up resistors depends on \
whether they are available in the hardware, and selecting these modes will fail if they are not.
It is OK to reconfigure an already configured pin without clearing its state first.",
)
.build(),
pins,
})
}
}
#[async_trait(?Send)]
impl Callable for GpioSetupCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
debug_assert_eq!(2, scope.nargs());
let pin = {
let (i, pos) = scope.pop_integer_with_pos();
Pin::from_i32(i, pos)?
};
let mode = {
let (t, pos) = scope.pop_string_with_pos();
PinMode::parse(&t, pos)?
};
match MockPins::try_new(machine.get_mut_symbols()) {
Some(mut pins) => pins.setup(pin, mode)?,
None => self.pins.borrow_mut().setup(pin, mode)?,
};
Ok(())
}
}
pub struct GpioClearCommand {
metadata: CallableMetadata,
pins: Rc<RefCell<dyn Pins>>,
}
impl GpioClearCommand {
pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("GPIO_CLEAR")
.with_syntax(&[
(&[], None),
(
&[SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("pin"),
vtype: ExprType::Integer,
},
ArgSepSyntax::End,
)],
None,
),
])
.with_category(CATEGORY)
.with_description(
"Resets the GPIO chip or a specific pin.
If no pin% is specified, resets the state of all GPIO pins. \
If a pin% is given, only that pin is reset. It is OK if the given pin has never been configured \
before.",
)
.build(),
pins,
})
}
}
#[async_trait(?Send)]
impl Callable for GpioClearCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
if scope.nargs() == 0 {
match MockPins::try_new(machine.get_mut_symbols()) {
Some(mut pins) => pins.clear_all()?,
None => self.pins.borrow_mut().clear_all()?,
};
} else {
debug_assert_eq!(1, scope.nargs());
let pin = {
let (i, pos) = scope.pop_integer_with_pos();
Pin::from_i32(i, pos)?
};
match MockPins::try_new(machine.get_mut_symbols()) {
Some(mut pins) => pins.clear(pin)?,
None => self.pins.borrow_mut().clear(pin)?,
};
}
Ok(())
}
}
pub struct GpioReadFunction {
metadata: CallableMetadata,
pins: Rc<RefCell<dyn Pins>>,
}
impl GpioReadFunction {
pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("GPIO_READ")
.with_return_type(ExprType::Boolean)
.with_syntax(&[(
&[SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("pin"),
vtype: ExprType::Integer,
},
ArgSepSyntax::End,
)],
None,
)])
.with_category(CATEGORY)
.with_description(
"Reads the state of a GPIO pin.
Returns FALSE to represent a low value, and TRUE to represent a high value.",
)
.build(),
pins,
})
}
}
#[async_trait(?Send)]
impl Callable for GpioReadFunction {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
debug_assert_eq!(1, scope.nargs());
let pin = {
let (i, pos) = scope.pop_integer_with_pos();
Pin::from_i32(i, pos)?
};
let value = match MockPins::try_new(machine.get_mut_symbols()) {
Some(mut pins) => pins.read(pin)?,
None => self.pins.borrow_mut().read(pin)?,
};
scope.return_boolean(value)
}
}
pub struct GpioWriteCommand {
metadata: CallableMetadata,
pins: Rc<RefCell<dyn Pins>>,
}
impl GpioWriteCommand {
pub fn new(pins: Rc<RefCell<dyn Pins>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("GPIO_WRITE")
.with_syntax(&[(
&[
SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("pin"),
vtype: ExprType::Integer,
},
ArgSepSyntax::Exactly(ArgSep::Long),
),
SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("value"),
vtype: ExprType::Boolean,
},
ArgSepSyntax::End,
),
],
None,
)])
.with_category(CATEGORY)
.with_description(
"Sets the state of a GPIO pin.
A FALSE value? sets the pin to low, and a TRUE value? sets the pin to high.",
)
.build(),
pins,
})
}
}
#[async_trait(?Send)]
impl Callable for GpioWriteCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, mut scope: Scope<'_>, machine: &mut Machine) -> CallResult {
debug_assert_eq!(2, scope.nargs());
let pin = {
let (i, pos) = scope.pop_integer_with_pos();
Pin::from_i32(i, pos)?
};
let value = scope.pop_boolean();
match MockPins::try_new(machine.get_mut_symbols()) {
Some(mut pins) => pins.write(pin, value)?,
None => self.pins.borrow_mut().write(pin, value)?,
};
Ok(())
}
}
pub fn add_all(machine: &mut Machine, pins: Rc<RefCell<dyn Pins>>) {
machine.add_clearable(PinsClearable::new(pins.clone()));
machine.add_callable(GpioClearCommand::new(pins.clone()));
machine.add_callable(GpioReadFunction::new(pins.clone()));
machine.add_callable(GpioSetupCommand::new(pins.clone()));
machine.add_callable(GpioWriteCommand::new(pins));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutils::*;
use endbasic_core::ast::Value;
fn check_pin_validation(prefix: &str, fmt: &str) {
check_stmt_compilation_err(
format!(r#"{}BOOLEAN is not a number"#, prefix),
&fmt.replace("_PIN_", "TRUE"),
);
check_stmt_err(
format!(r#"{}Pin number 123456789 is too large"#, prefix),
&fmt.replace("_PIN_", "123456789"),
);
check_stmt_err(
format!(r#"{}Pin number -1 must be positive"#, prefix),
&fmt.replace("_PIN_", "-1"),
);
}
fn do_mock_test_with_vars<S: Into<String>, VS: Into<Vec<(&'static str, Value)>>>(
code: S,
trace: &[i32],
vars: VS,
) {
let code = code.into();
let vars = vars.into();
let mut exp_data = vec![Value::Integer(0); 50];
for (i, d) in trace.iter().enumerate() {
exp_data[i] = Value::Integer(*d);
}
let mut t = Tester::default();
for var in vars.as_slice() {
t = t.set_var(var.0, var.1.clone());
}
let mut c = t
.run(format!(r#"DIM __GPIO_MOCK_DATA(50) AS INTEGER: __GPIO_MOCK_LAST = 0: {}"#, code));
for var in vars.into_iter() {
c = c.expect_var(var.0, var.1.clone());
}
c.expect_var("__GPIO_MOCK_LAST", Value::Integer(trace.len() as i32))
.expect_array_simple("__GPIO_MOCK_DATA", ExprType::Integer, exp_data)
.check();
}
fn do_mock_test<S: Into<String>>(code: S, trace: &[i32]) {
do_mock_test_with_vars(code, trace, [])
}
#[test]
fn test_real_backend() {
check_stmt_err(
"1:1: In call to GPIO_SETUP: GPIO backend not compiled in",
"GPIO_SETUP 0, \"IN\"",
);
check_stmt_err("1:1: In call to GPIO_CLEAR: GPIO backend not compiled in", "GPIO_CLEAR");
check_stmt_err("1:1: In call to GPIO_CLEAR: GPIO backend not compiled in", "GPIO_CLEAR 0");
check_expr_error(
"1:10: In call to GPIO_READ: GPIO backend not compiled in",
"GPIO_READ(0)",
);
check_stmt_err(
"1:1: In call to GPIO_WRITE: GPIO backend not compiled in",
"GPIO_WRITE 0, TRUE",
);
}
#[test]
fn test_gpio_setup_ok() {
for mode in &["in", "IN"] {
do_mock_test(format!(r#"GPIO_SETUP 5, "{}""#, mode), &[501]);
do_mock_test(format!(r#"GPIO_SETUP 5.2, "{}""#, mode), &[501]);
}
for mode in &["in-pull-down", "IN-PULL-DOWN"] {
do_mock_test(format!(r#"GPIO_SETUP 6, "{}""#, mode), &[602]);
do_mock_test(format!(r#"GPIO_SETUP 6.2, "{}""#, mode), &[602]);
}
for mode in &["in-pull-up", "IN-PULL-UP"] {
do_mock_test(format!(r#"GPIO_SETUP 7, "{}""#, mode), &[703]);
do_mock_test(format!(r#"GPIO_SETUP 7.2, "{}""#, mode), &[703]);
}
for mode in &["out", "OUT"] {
do_mock_test(format!(r#"GPIO_SETUP 8, "{}""#, mode), &[804]);
do_mock_test(format!(r#"GPIO_SETUP 8.2, "{}""#, mode), &[804]);
}
}
#[test]
fn test_gpio_setup_multiple() {
do_mock_test(r#"GPIO_SETUP 18, "IN-PULL-UP": GPIO_SETUP 10, "OUT""#, &[1803, 1004]);
}
#[test]
fn test_gpio_setup_errors() {
check_stmt_compilation_err(
"1:1: In call to GPIO_SETUP: expected pin%, mode$",
r#"GPIO_SETUP"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_SETUP: expected pin%, mode$",
r#"GPIO_SETUP 1"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_SETUP: 1:15: INTEGER is not a STRING",
r#"GPIO_SETUP 1; 2"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_SETUP: expected pin%, mode$",
r#"GPIO_SETUP 1, 2, 3"#,
);
check_pin_validation("1:1: In call to GPIO_SETUP: 1:12: ", r#"GPIO_SETUP _PIN_, "IN""#);
check_stmt_err(
r#"1:1: In call to GPIO_SETUP: 1:15: Unknown pin mode IN-OUT"#,
r#"GPIO_SETUP 1, "IN-OUT""#,
);
}
#[test]
fn test_gpio_clear_all() {
do_mock_test("GPIO_CLEAR", &[-1]);
}
#[test]
fn test_gpio_clear_one() {
do_mock_test("GPIO_CLEAR 4", &[405]);
do_mock_test("GPIO_CLEAR 4.1", &[405]);
}
#[test]
fn test_gpio_clear_errors() {
check_stmt_compilation_err(
"1:1: In call to GPIO_CLEAR: expected <> | <pin%>",
r#"GPIO_CLEAR 1,"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_CLEAR: expected <> | <pin%>",
r#"GPIO_CLEAR 1, 2"#,
);
check_pin_validation("1:1: In call to GPIO_CLEAR: 1:12: ", r#"GPIO_CLEAR _PIN_"#);
}
#[test]
fn test_gpio_read_ok() {
do_mock_test_with_vars(
"__GPIO_MOCK_DATA(0) = 310
__GPIO_MOCK_DATA(2) = 311
GPIO_WRITE 5, GPIO_READ(3.1)
GPIO_WRITE 7, GPIO_READ(pin)",
&[310, 520, 311, 721],
[("pin", 3.into())],
);
}
#[test]
fn test_gpio_read_errors() {
check_expr_compilation_error("1:10: In call to GPIO_READ: expected pin%", r#"GPIO_READ()"#);
check_expr_compilation_error(
"1:10: In call to GPIO_READ: expected pin%",
r#"GPIO_READ(1, 2)"#,
);
check_pin_validation("1:5: In call to GPIO_READ: 1:15: ", r#"v = GPIO_READ(_PIN_)"#);
}
#[test]
fn test_gpio_write_ok() {
do_mock_test("GPIO_WRITE 3, TRUE: GPIO_WRITE 3.1, FALSE", &[321, 320]);
}
#[test]
fn test_gpio_write_errors() {
check_stmt_compilation_err(
"1:1: In call to GPIO_WRITE: expected pin%, value?",
r#"GPIO_WRITE"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_WRITE: expected pin%, value?",
r#"GPIO_WRITE 2,"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_WRITE: expected pin%, value?",
r#"GPIO_WRITE 1, TRUE, 2"#,
);
check_stmt_compilation_err(
"1:1: In call to GPIO_WRITE: expected pin%, value?",
r#"GPIO_WRITE 1; TRUE"#,
);
check_pin_validation("1:1: In call to GPIO_WRITE: 1:12: ", r#"GPIO_WRITE _PIN_, TRUE"#);
check_stmt_compilation_err(
"1:1: In call to GPIO_WRITE: 1:15: INTEGER is not a BOOLEAN",
r#"GPIO_WRITE 1, 5"#,
);
}
}