Merge branch 'uniffi2'

This commit is contained in:
Vinzenz Schroeter 2024-11-23 22:57:46 +01:00
commit 93657c9f85
66 changed files with 7298 additions and 2469 deletions

2
.gitignore vendored
View file

@ -1,8 +1,6 @@
target
.idea
out
bin
obj
.direnv
.envrc
result

902
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,12 @@ resolver = "2"
members = [
"crates/servicepoint",
"crates/servicepoint_binding_c",
"crates/servicepoint_binding_cs",
"crates/servicepoint_binding_c/examples/lang_c"
"crates/servicepoint_binding_c/examples/lang_c",
"crates/servicepoint_binding_uniffi"
]
[workspace.package]
version = "0.11.0"
version = "0.12.0"
[workspace.lints.rust]
missing-docs = "warn"

View file

@ -1,5 +1,10 @@
# servicepoint
[![crates.io](https://img.shields.io/crates/v/servicepoint.svg)](https://crates.io/crates/servicepoint)
[![Crates.io Total Downloads](https://img.shields.io/crates/d/servicepoint)](https://crates.io/crates/servicepoint)
[![docs.rs](https://img.shields.io/docsrs/servicepoint)](https://docs.rs/servicepoint/latest/servicepoint/)
[![GPLv3 licensed](https://img.shields.io/crates/l/servicepoint)](../../LICENSE)
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point
Display" or "Airport Display".
This repository contains a library for parsing, encoding and sending packets to this display via UDP in multiple
@ -7,11 +12,11 @@ programming languages.
Take a look at the contained crates for language specific information:
| Language | Readme |
|-----------|---------------------------------------------------------------------|
| Rust | [servicepoint](crates/servicepoint/README.md) |
| C / C++ | [servicepoint_binding_c](crates/servicepoint_binding_c/README.md) |
| .NET (C#) | [servicepoint_binding_cs](crates/servicepoint_binding_cs/README.md) |
| Crate | Languages | Readme |
|-----------------------------|-----------------------------------|-------------------------------------------------------------------------|
| servicepoint | Rust | [servicepoint](crates/servicepoint/README.md) |
| servicepoint_binding_c | C / C++ | [servicepoint_binding_c](crates/servicepoint_binding_c/README.md) |
| servicepoint_binding_uniffi | C# / Python / Go / Kotlin / Swift | [servicepoint_binding_cs](crates/servicepoint_binding_uniffi/README.md) |
## Projects using the library

View file

@ -17,7 +17,7 @@ cargo add servicepoint
or
```toml
[dependencies]
servicepoint = "0.11.0"
servicepoint = "0.12.0"
```
## Examples

View file

@ -17,7 +17,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
cbindgen = "0.27.0"
[dependencies.servicepoint]
version = "0.11.0"
version = "0.12.0"
path = "../servicepoint"
features = ["all_compressions"]

View file

@ -1201,6 +1201,20 @@ SPCommand *sp_command_hard_reset(void);
*/
SPCommand *sp_command_try_from_packet(SPPacket *packet);
/**
* Creates a new instance of [SPConnection] for testing that does not actually send anything.
*
* returns: a new instance. Will never return NULL.
*
* # Safety
*
* The caller has to make sure that:
*
* - the returned instance is freed in some way, either by using a consuming function or
* by explicitly calling `sp_connection_free`.
*/
SPConnection *sp_connection_fake(void);
/**
* Closes and deallocates a [SPConnection].
*

View file

@ -3,7 +3,7 @@
//! prefix `sp_connection_`
use std::ffi::{c_char, CStr};
use std::ptr::null_mut;
use std::ptr::{null_mut, NonNull};
use crate::{SPCommand, SPPacket};
@ -46,6 +46,22 @@ pub unsafe extern "C" fn sp_connection_open(
Box::into_raw(Box::new(SPConnection(connection)))
}
/// Creates a new instance of [SPConnection] for testing that does not actually send anything.
///
/// returns: a new instance. Will never return NULL.
///
/// # Safety
///
/// The caller has to make sure that:
///
/// - the returned instance is freed in some way, either by using a consuming function or
/// by explicitly calling `sp_connection_free`.
#[no_mangle]
pub unsafe extern "C" fn sp_connection_fake() -> NonNull<SPConnection> {
let result = Box::new(SPConnection(servicepoint::Connection::Fake));
NonNull::from(Box::leak(result))
}
/// Sends a [SPPacket] to the display using the [SPConnection].
///
/// The passed `packet` gets consumed.

View file

@ -1,20 +0,0 @@
[package]
name = "servicepoint_binding_cs"
version.workspace = true
edition = "2021"
publish = false
readme = "README.md"
[lib]
crate-type = ["cdylib"]
test = false
[build-dependencies]
csbindgen = "1.9.3"
[dependencies]
servicepoint_binding_c = { version = "0.11.0", path = "../servicepoint_binding_c" }
servicepoint = { version = "0.11.0", path = "../servicepoint" }
[lints]
workspace = true

View file

@ -1,65 +0,0 @@
# ServicePoint
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point
Display" or "Airport Display".
This crate contains C# bindings for the `servicepoint` library, enabling users to parse, encode and send packets to this display via UDP.
## Examples
```csharp
using ServicePoint;
// using statement calls Dispose() on scope exit, which frees unmanaged instances
using var connection = Connection.Open("127.0.0.1:2342");
using var pixels = Bitmap.New(Constants.PixelWidth, Constants.PixelHeight);
while (true)
{
pixels.Fill(true);
connection.Send(Command.BitmapLinearWin(0, 0, pixels.Clone()));
Thread.Sleep(5000);
pixels.Fill(false);
connection.Send(Command.BitmapLinearWin(0, 0, pixels.Clone()));
Thread.Sleep(5000);
}
```
A full example including project files is available as part of this crate.
## Note on stability
This library is still in early development.
You can absolutely use it, and it works, but expect minor breaking changes with every version bump.
## Installation
NuGet packages are not a good way to distribute native projects ([relevant issue](https://github.com/dotnet/sdk/issues/33845)).
Because of that, there is no NuGet package you can use directly.
Including this repository as a submodule and building from source is the recommended way of using the library.
```bash
git submodule add https://github.com/cccb/servicepoint.git
git commit -m "add servicepoint submodule"
```
You can now reference `servicepoint-bindings-cs/src/ServicePoint.csproj` in your project.
The rust library will automatically be built.
Please provide more information in the form of an issue if you need the build to copy a different library file for your platform.
## Notes on differences to rust library
Uses C bindings internally to provide a similar API to rust. Things to keep in mind:
- You will get a `NullPointerException` when trying to call a method where the native instance has been consumed already (e.g. when `Send`ing a command instance twice). Send a clone instead of the original if you want to keep using it.
- Some lower-level APIs _will_ panic in native code when used improperly.
Example: manipulating the `Span<byte>` of an object after freeing the instance.
- C# specifics are documented in the library. Use the rust documentation for everything else. Naming and semantics are the same apart from CamelCase instead of kebab_case.
- You will only get rust backtraces in debug builds of the native code.
- F# is not explicitly tested. If there are usability or functionality problems, please open an issue.
- Reading and writing to instances concurrently is not safe. Only reading concurrently is safe.
## Everything else
Look at the main project [README](https://github.com/cccb/servicepoint/blob/main/README.md) for further information.

View file

@ -1,28 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint", "ServicePoint/ServicePoint.csproj", "{70EFFA3F-012A-4518-9627-466BEAE4252E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "lang-cs", "examples/lang_cs/lang_cs.csproj", "{DA3B8B6E-993A-47DA-844B-F92AF520FF59}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{C2F8EC4A-2426-4DC3-990F-C43810B183F5}"
ProjectSection(SolutionItems) = preProject
..\..\shell.nix = ..\..\shell.nix
..\..\.envrc = ..\..\.envrc
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{70EFFA3F-012A-4518-9627-466BEAE4252E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70EFFA3F-012A-4518-9627-466BEAE4252E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70EFFA3F-012A-4518-9627-466BEAE4252E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70EFFA3F-012A-4518-9627-466BEAE4252E}.Release|Any CPU.Build.0 = Release|Any CPU
{DA3B8B6E-993A-47DA-844B-F92AF520FF59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA3B8B6E-993A-47DA-844B-F92AF520FF59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA3B8B6E-993A-47DA-844B-F92AF520FF59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA3B8B6E-993A-47DA-844B-F92AF520FF59}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,88 +0,0 @@
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class BitVec : SpNativeInstance<BindGen.BitVec>
{
public static BitVec New(int size)
{
unsafe
{
return new BitVec(NativeMethods.sp_bitvec_new((nuint)size));
}
}
public static BitVec Load(Span<byte> bytes)
{
unsafe
{
fixed (byte* bytesPtr = bytes)
{
return new BitVec(NativeMethods.sp_bitvec_load(bytesPtr, (nuint)bytes.Length));
}
}
}
public BitVec Clone()
{
unsafe
{
return new BitVec(NativeMethods.sp_bitvec_clone(Instance));
}
}
public bool this[int index]
{
get
{
unsafe
{
return NativeMethods.sp_bitvec_get(Instance, (nuint)index);
}
}
set
{
unsafe
{
NativeMethods.sp_bitvec_set(Instance, (nuint)index, value);
}
}
}
public void Fill(bool value)
{
unsafe
{
NativeMethods.sp_bitvec_fill(Instance, value);
}
}
public int Length
{
get
{
unsafe
{
return (int)NativeMethods.sp_bitvec_len(Instance);
}
}
}
public Span<byte> Data
{
get
{
unsafe
{
var slice = NativeMethods.sp_bitvec_unsafe_data_ref(Instance);
return new Span<byte>(slice.start, (int)slice.length);
}
}
}
private unsafe BitVec(BindGen.BitVec* instance) : base(instance)
{
}
private protected override unsafe void Free() => NativeMethods.sp_bitvec_free(Instance);
}

View file

@ -1,100 +0,0 @@
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class Bitmap : SpNativeInstance<BindGen.Bitmap>
{
public static Bitmap New(int width, int height)
{
unsafe
{
return new Bitmap(NativeMethods.sp_bitmap_new((nuint)width, (nuint)height));
}
}
public static Bitmap Load(int width, int height, Span<byte> bytes)
{
unsafe
{
fixed (byte* bytesPtr = bytes)
{
return new Bitmap(NativeMethods.sp_bitmap_load((nuint)width, (nuint)height, bytesPtr,
(nuint)bytes.Length));
}
}
}
public Bitmap Clone()
{
unsafe
{
return new Bitmap(NativeMethods.sp_bitmap_clone(Instance));
}
}
public bool this[int x, int y]
{
get
{
unsafe
{
return NativeMethods.sp_bitmap_get(Instance, (nuint)x, (nuint)y);
}
}
set
{
unsafe
{
NativeMethods.sp_bitmap_set(Instance, (nuint)x, (nuint)y, value);
}
}
}
public void Fill(bool value)
{
unsafe
{
NativeMethods.sp_bitmap_fill(Instance, value);
}
}
public int Width
{
get
{
unsafe
{
return (int)NativeMethods.sp_bitmap_width(Instance);
}
}
}
public int Height
{
get
{
unsafe
{
return (int)NativeMethods.sp_bitmap_height(Instance);
}
}
}
public Span<byte> Data
{
get
{
unsafe
{
var slice = NativeMethods.sp_bitmap_unsafe_data_ref(Instance);
return new Span<byte>(slice.start, (int)slice.length);
}
}
}
private unsafe Bitmap(BindGen.Bitmap* instance) : base(instance)
{
}
private protected override unsafe void Free() => NativeMethods.sp_bitmap_free(Instance);
}

View file

@ -1,100 +0,0 @@
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class BrightnessGrid : SpNativeInstance<BindGen.BrightnessGrid>
{
public static BrightnessGrid New(int width, int height)
{
unsafe
{
return new BrightnessGrid(NativeMethods.sp_brightness_grid_new((nuint)width, (nuint)height));
}
}
public static BrightnessGrid Load(int width, int height, Span<byte> bytes)
{
unsafe
{
fixed (byte* bytesPtr = bytes)
{
return new BrightnessGrid(NativeMethods.sp_brightness_grid_load((nuint)width, (nuint)height, bytesPtr,
(nuint)bytes.Length));
}
}
}
public BrightnessGrid Clone()
{
unsafe
{
return new BrightnessGrid(NativeMethods.sp_brightness_grid_clone(Instance));
}
}
public byte this[int x, int y]
{
get
{
unsafe
{
return NativeMethods.sp_brightness_grid_get(Instance, (nuint)x, (nuint)y);
}
}
set
{
unsafe
{
NativeMethods.sp_brightness_grid_set(Instance, (nuint)x, (nuint)y, value);
}
}
}
public void Fill(byte value)
{
unsafe
{
NativeMethods.sp_brightness_grid_fill(Instance, value);
}
}
public int Width
{
get
{
unsafe
{
return (int)NativeMethods.sp_brightness_grid_width(Instance);
}
}
}
public int Height
{
get
{
unsafe
{
return (int)NativeMethods.sp_brightness_grid_height(Instance);
}
}
}
public Span<byte> Data
{
get
{
unsafe
{
var slice = NativeMethods.sp_brightness_grid_unsafe_data_ref(Instance);
return new Span<byte>(slice.start, (int)slice.length);
}
}
}
private unsafe BrightnessGrid(BindGen.BrightnessGrid* instance) : base(instance)
{
}
private protected override unsafe void Free() => NativeMethods.sp_brightness_grid_free(Instance);
}

View file

@ -1,129 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class Command : SpNativeInstance<BindGen.Command>
{
public static bool TryFromPacket(Packet packet, [MaybeNullWhen(false)] out Command command)
{
unsafe
{
var result = NativeMethods.sp_command_try_from_packet(packet.Into());
if (result == null)
{
command = null;
return false;
}
command = new Command(result);
return true;
}
}
public Command Clone()
{
unsafe
{
return new Command(NativeMethods.sp_command_clone(Instance));
}
}
public static Command Clear()
{
unsafe
{
return new Command(NativeMethods.sp_command_clear());
}
}
public static Command HardReset()
{
unsafe
{
return new Command(NativeMethods.sp_command_hard_reset());
}
}
public static Command FadeOut()
{
unsafe
{
return new Command(NativeMethods.sp_command_fade_out());
}
}
public static Command Brightness(byte brightness)
{
unsafe
{
return new Command(NativeMethods.sp_command_brightness(brightness));
}
}
public static Command CharBrightness(int x, int y, BrightnessGrid grid)
{
unsafe
{
return new Command(NativeMethods.sp_command_char_brightness((ushort)x, (ushort)y, grid.Into()));
}
}
public static Command BitmapLinear(int offset, BitVec bitVec, CompressionCode compressionCode)
{
unsafe
{
return new Command(
NativeMethods.sp_command_bitmap_linear((ushort)offset, bitVec.Into(), compressionCode));
}
}
public static Command BitmapLinearAnd(int offset, BitVec bitVec, CompressionCode compressionCode)
{
unsafe
{
return new Command(
NativeMethods.sp_command_bitmap_linear_and((ushort)offset, bitVec.Into(), compressionCode));
}
}
public static Command BitmapLinearOr(int offset, BitVec bitVec, CompressionCode compressionCode)
{
unsafe
{
return new Command(
NativeMethods.sp_command_bitmap_linear_or((ushort)offset, bitVec.Into(), compressionCode));
}
}
public static Command BitmapLinearXor(int offset, BitVec bitVec, CompressionCode compressionCode)
{
unsafe
{
return new Command(
NativeMethods.sp_command_bitmap_linear_xor((ushort)offset, bitVec.Into(), compressionCode));
}
}
public static Command BitmapLinearWin(int x, int y, Bitmap bitmap, CompressionCode compression)
{
unsafe
{
return new Command(NativeMethods.sp_command_bitmap_linear_win((ushort)x, (ushort)y, bitmap.Into(), compression));
}
}
public static Command Cp437Data(int x, int y, Cp437Grid byteGrid)
{
unsafe
{
return new Command(NativeMethods.sp_command_cp437_data((ushort)x, (ushort)y, byteGrid.Into()));
}
}
private unsafe Command(BindGen.Command* instance) : base(instance)
{
}
private protected override unsafe void Free() => NativeMethods.sp_command_free(Instance);
}

View file

@ -1,40 +0,0 @@
using System.Text;
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class Connection : SpNativeInstance<BindGen.Connection>
{
public static Connection Open(string host)
{
unsafe
{
fixed (byte* bytePtr = Encoding.UTF8.GetBytes(host))
{
return new Connection(NativeMethods.sp_connection_open(bytePtr));
}
}
}
public bool Send(Packet packet)
{
unsafe
{
return NativeMethods.sp_connection_send_packet(Instance, packet.Into());
}
}
public bool Send(Command command)
{
unsafe
{
return NativeMethods.sp_connection_send_command(Instance, command.Into());
}
}
private protected override unsafe void Free() => NativeMethods.sp_connection_free(Instance);
private unsafe Connection(BindGen.Connection* instance) : base(instance)
{
}
}

View file

@ -1,24 +0,0 @@
using ServicePoint.BindGen;
namespace ServicePoint;
public static class Constants
{
/// size of a single tile in one dimension
public const nuint TileSize = NativeMethods.SP_TILE_SIZE;
/// tile count in the x-direction
public const nuint TileWidth = NativeMethods.SP_TILE_WIDTH;
/// tile count in the y-direction
public const nuint TileHeight = NativeMethods.SP_TILE_SIZE;
/// screen width in pixels
public const nuint PixelWidth = TileWidth * TileSize;
/// screen height in pixels
public const nuint PixelHeight = TileHeight * TileSize;
/// pixel count on whole screen
public const nuint PixelCount = PixelWidth * PixelHeight;
}

View file

@ -1,131 +0,0 @@
using System.Text;
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class Cp437Grid : SpNativeInstance<BindGen.Cp437Grid>
{
public static Cp437Grid New(int width, int height)
{
unsafe
{
return new Cp437Grid(NativeMethods.sp_cp437_grid_new((nuint)width, (nuint)height));
}
}
public static Cp437Grid Load(int width, int height, Span<byte> bytes)
{
unsafe
{
fixed (byte* bytesPtr = bytes)
{
return new Cp437Grid(NativeMethods.sp_cp437_grid_load((nuint)width, (nuint)height, bytesPtr,
(nuint)bytes.Length));
}
}
}
public Cp437Grid Clone()
{
unsafe
{
return new Cp437Grid(NativeMethods.sp_cp437_grid_clone(Instance));
}
}
public byte this[int x, int y]
{
get
{
unsafe
{
return NativeMethods.sp_cp437_grid_get(Instance, (nuint)x, (nuint)y);
}
}
set
{
unsafe
{
NativeMethods.sp_cp437_grid_set(Instance, (nuint)x, (nuint)y, value);
}
}
}
public string this[int y]
{
set
{
var width = Width;
ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, width);
var x = 0;
for (; x < value.Length; x++)
this[x, y] = (byte)value[x];
for (; x < width; x++)
this[x, y] = 0;
}
get
{
var sb = new StringBuilder();
for (int x = 0; x < Width; x++)
{
var val = this[x, y];
if (val == 0)
break;
sb.Append((char)val);
}
return sb.ToString();
}
}
public void Fill(byte value)
{
unsafe
{
NativeMethods.sp_cp437_grid_fill(Instance, value);
}
}
public int Width
{
get
{
unsafe
{
return (int)NativeMethods.sp_cp437_grid_width(Instance);
}
}
}
public int Height
{
get
{
unsafe
{
return (int)NativeMethods.sp_cp437_grid_height(Instance);
}
}
}
public Span<byte> Data
{
get
{
unsafe
{
var slice = NativeMethods.sp_cp437_grid_unsafe_data_ref(Instance);
return new Span<byte>(slice.start, (int)slice.length);
}
}
}
private unsafe Cp437Grid(BindGen.Cp437Grid* instance) : base(instance)
{
}
private protected override unsafe void Free() => NativeMethods.sp_cp437_grid_free(Instance);
}

View file

@ -1 +0,0 @@
global using System;

View file

@ -1,36 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using ServicePoint.BindGen;
namespace ServicePoint;
public sealed class Packet : SpNativeInstance<BindGen.Packet>
{
public static Packet FromCommand(Command command)
{
unsafe
{
return new Packet(NativeMethods.sp_packet_from_command(command.Into()));
}
}
public static bool TryFromBytes(Span<byte> bytes, [MaybeNullWhen(false)] out Packet packet)
{
unsafe
{
fixed (byte* bytesPtr = bytes)
{
var instance = NativeMethods.sp_packet_try_load(bytesPtr, (nuint)bytes.Length);
packet = instance == null
? null
: new Packet(instance);
return packet != null;
}
}
}
private unsafe Packet(BindGen.Packet* instance) : base(instance)
{
}
private protected override unsafe void Free() => NativeMethods.sp_packet_free(Instance);
}

View file

@ -1,16 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace ServicePoint;
public static class ServicePointExtensions
{
public static Packet IntoPacket(this Command command)
{
return Packet.FromCommand(command);
}
public static bool TryIntoCommand(this Packet packet, [MaybeNullWhen(false)] out Command command)
{
return Command.TryFromPacket(packet, out command);
}
}

View file

@ -1,51 +0,0 @@
namespace ServicePoint;
public abstract class SpNativeInstance<T>
: IDisposable
where T : unmanaged
{
private unsafe T* _instance;
internal unsafe T* Instance
{
get
{
if (_instance == null)
throw new NullReferenceException("instance is null");
return _instance;
}
}
private protected unsafe SpNativeInstance(T* instance)
{
ArgumentNullException.ThrowIfNull(instance);
_instance = instance;
}
private protected abstract void Free();
internal unsafe T* Into()
{
var instance = _instance;
_instance = null;
return instance;
}
private unsafe void ReleaseUnmanagedResources()
{
if (_instance != null)
Free();
_instance = null;
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
~SpNativeInstance()
{
ReleaseUnmanagedResources();
}
}

View file

@ -1,39 +0,0 @@
//! Build script generating the C# code needed to call methods from the `servicepoint` C library.
use std::fs;
fn main() {
println!("cargo::rerun-if-changed=../servicepoint_binding_c/src");
println!("cargo::rerun-if-changed=build.rs");
let mut builder = csbindgen::Builder::default();
let mut paths = fs::read_dir("../servicepoint_binding_c/src").unwrap()
.map(|x| x.unwrap().path())
.collect::<Vec<_>>();
paths.sort();
for path in paths {
println!("cargo:rerun-if-changed={}", path.display());
builder = builder.input_extern_file(path);
}
builder
.csharp_dll_name("servicepoint_binding_c")
.csharp_namespace("ServicePoint.BindGen")
.csharp_use_nint_types(true)
.csharp_class_accessibility("public")
.csharp_generate_const_filter(|_| true)
.csharp_type_rename(move |name| {
if name.len() > 2
&& name.starts_with("SP")
&& name.chars().nth(2).unwrap().is_uppercase()
{
name[2..].to_string()
} else {
name
}
})
.generate_csharp_file("ServicePoint/BindGen/ServicePoint.g.cs")
.unwrap();
}

View file

@ -1,20 +0,0 @@
using ServicePoint;
using CompressionCode = ServicePoint.BindGen.CompressionCode;
using var connection = Connection.Open("127.0.0.1:2342");
connection.Send(Command.Clear().IntoPacket());
connection.Send(Command.Brightness(128).IntoPacket());
using var pixels = Bitmap.New(Constants.PixelWidth, Constants.PixelHeight);
for (var offset = 0; offset < int.MaxValue; offset++)
{
pixels.Fill(false);
for (var y = 0; y < pixels.Height; y++)
pixels[(y + offset) % Constants.PixelWidth, y] = true;
connection.Send(Command.BitmapLinearWin(0, 0, pixels.Clone(), CompressionCode.Lzma).IntoPacket());
Thread.Sleep(14);
}

View file

@ -1 +0,0 @@
//! This crate is intentionally left empty. Only the build script is relevant here.

View file

@ -0,0 +1,60 @@
[package]
name = "servicepoint_binding_uniffi"
version.workspace = true
publish = false
edition = "2021"
license = "GPL-3.0-or-later"
description = "C bindings for the servicepoint crate."
homepage = "https://docs.rs/crate/servicepoint_binding_c"
repository = "https://github.com/cccb/servicepoint"
#readme = "README.md"
[lib]
crate-type = ["cdylib"]
[build-dependencies]
uniffi = { version = "0.25.3", features = ["build"] }
[dependencies]
uniffi = { version = "0.25.3" }
thiserror.workspace = true
[dependencies.servicepoint]
version = "0.12.0"
path = "../servicepoint"
features = ["all_compressions"]
[dependencies.uniffi-bindgen-cs]
git = "https://github.com/NordSecurity/uniffi-bindgen-cs"
# tag="v0.8.3+v0.25.0"
rev = "f68639fbc720b50ebe561ba75c66c84dc456bdce"
optional = true
[dependencies.uniffi-bindgen-go]
git = "https://github.com/NordSecurity/uniffi-bindgen-go.git"
# tag = "0.2.1+v0.25.0"
rev = "a77dc0462dc18d53846c758155ab4e0a42e5b240"
optional = true
[lints]
#workspace = true
[package.metadata.docs.rs]
all-features = true
[[bin]]
name = "uniffi-bindgen"
required-features = ["uniffi/cli"]
[[bin]]
name = "uniffi-bindgen-cs"
required-features = ["cs"]
[[bin]]
name = "uniffi-bindgen-go"
required-features = ["go"]
[features]
default = []
cs = ["dep:uniffi-bindgen-cs"]
go = ["dep:uniffi-bindgen-go"]

View file

@ -0,0 +1,90 @@
# ServicePoint
In [CCCB](https://berlin.ccc.de/), there is a big pixel matrix hanging on the wall. It is called "Service Point
Display" or "Airport Display".
This crate contains bindings for multiple programming languages, enabling non-rust-developers to use the library.
Also take a look at the main project [README](https://github.com/cccb/servicepoint/blob/main/README.md) for more
information.
## Note on stability
This library is still in early development.
You can absolutely use it, and it works, but expect minor breaking changes with every version bump.
## Notes on differences to rust library
- Performance will not be as good as the rust version:
- most objects are reference counted.
- objects with mutating methods will also have a MRSW lock
- You will not get rust backtraces in release builds of the native code
- Panic messages will work (PanicException)
## Supported languages
| Language | Support level | Notes |
|-----------|---------------|-------------------------------------------------------------------------------------------------|
| .NET (C#) | Full | see dedicated section |
| Ruby | Working | LD_LIBRARY_PATH has to be set, see example project |
| Python | Tested once | Required project file not included. The shared library will be loaded from the script location. |
| Go | untested | |
| Kotlin | untested | |
| Swift | untested | |
## Installation
Including this repository as a submodule and building from source is the recommended way of using the library.
```bash
git submodule add https://github.com/cccb/servicepoint.git
git commit -m "add servicepoint submodule"
```
Run `generate-bindings.sh` to regenerate all bindings. This will also build `libservicepoint.so` (or equivalent on your
platform).
For languages not fully supported, there will be no project file for the library, just the naked source file(s).
If you successfully use a language, please open an issue or PR to add the missing ones.
## .NET (C#)
This is the best supported language.
F# is not tested. If there are usability or functionality problems, please open an issue.
Currently, the project file is hard-coded for Linux and will need tweaks for other platforms (e.g. `.dylib` instead of `.so`).
You do not have to compile or copy the rust crate manually, as building `ServicePoint.csproj` also builds it.
### Example
```csharp
using System.Threading;
using ServicePoint;
var connection = new Connection("127.0.0.1:2342");
connection.Send(Command.Clear());
connection.Send(Command.Brightness(5));
var pixels = Bitmap.NewMaxSized();
for (ulong offset = 0; offset < ulong.MaxValue; offset++)
{
pixels.Fill(false);
for (ulong y = 0; y < pixels.Height(); y++)
pixels.Set((y + offset) % pixels.Width(), y, true);
connection.Send(Command.BitmapLinearWin(0, 0, pixels));
Thread.Sleep(14);
}
```
A full example including project files is available as part of this crate.
### Why is there no NuGet-Package?
NuGet packages are not a good way to distribute native
binaries ([relevant issue](https://github.com/dotnet/sdk/issues/33845)).
Because of that, there is no NuGet package you can use directly.

View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -e
cargo build --release
SCRIPT_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
TARGET_PATH="$(realpath "$SCRIPT_PATH"/../../target/release)"
SERVICEPOINT_SO="$TARGET_PATH/libservicepoint_binding_uniffi.so"
LIBRARIES_PATH="$SCRIPT_PATH/libraries"
echo "Source: $SERVICEPOINT_SO"
echo "Output: $LIBRARIES_PATH"
BINDGEN="cargo run --features=uniffi/cli --bin uniffi-bindgen -- "
BINDGEN_CS="cargo run --features=cs --bin uniffi-bindgen-cs -- "
BINDGEN_GO="cargo run --features=go --bin uniffi-bindgen-go -- "
COMMON_ARGS="--library $SERVICEPOINT_SO"
${BINDGEN} generate $COMMON_ARGS --language python --out-dir "$LIBRARIES_PATH/python"
${BINDGEN} generate $COMMON_ARGS --language kotlin --out-dir "$LIBRARIES_PATH/kotlin"
${BINDGEN} generate $COMMON_ARGS --language swift --out-dir "$LIBRARIES_PATH/swift"
${BINDGEN} generate $COMMON_ARGS --language ruby --out-dir "$LIBRARIES_PATH/ruby/lib"
${BINDGEN_CS} $COMMON_ARGS --out-dir "$LIBRARIES_PATH/csharp/ServicePoint"
${BINDGEN_GO} $COMMON_ARGS --out-dir "$LIBRARIES_PATH/go/"

View file

@ -0,0 +1,4 @@
go
kotlin
python
swift

View file

@ -0,0 +1,2 @@
bin
obj

View file

@ -0,0 +1,2 @@
bin
obj

View file

@ -0,0 +1,19 @@
using System.Threading;
using ServicePoint;
var connection = new Connection("127.0.0.1:2342");
connection.Send(Command.Clear());
connection.Send(Command.Brightness(5));
var pixels = Bitmap.NewMaxSized();
for (ulong offset = 0; offset < ulong.MaxValue; offset++)
{
pixels.Fill(false);
for (ulong y = 0; y < pixels.Height(); y++)
pixels.Set((y + offset) % pixels.Width(), y, true);
connection.Send(Command.BitmapLinearWin(0, 0, pixels));
Thread.Sleep(14);
}

View file

@ -3,13 +3,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>lang_cs</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ServicePoint.Example</RootNamespace>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../ServicePoint/ServicePoint.csproj"/>
<ProjectReference Include="../ServicePoint/ServicePoint.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,16 @@
namespace ServicePoint.Tests;
public class BitmapTests
{
[Fact]
public void BasicFunctions()
{
var bitmap = new Bitmap(8, 2);
Assert.False(bitmap.Get(0, 0));
Assert.False(bitmap.Get(bitmap.Width() - 1, bitmap.Height() - 1));
bitmap.Fill(true);
Assert.True(bitmap.Get(1, 1));
bitmap.Set(1, 1, false);
Assert.False(bitmap.Get(1, 1));
}
}

View file

@ -0,0 +1,31 @@
namespace ServicePoint.Tests;
public class CharGridTests
{
[Fact]
public void BasicFunctions()
{
var grid = new CharGrid(8, 2);
Assert.Equal("\0", grid.Get(0, 0));
Assert.Equal("\0", grid.Get(grid.Width() - 1, grid.Height() - 1));
grid.Fill(" ");
Assert.Equal(" ", grid.Get(1, 1));
grid.Set(1, 1, "-");
Assert.Equal("-", grid.Get(1, 1));
Assert.Throws<PanicException>(() => grid.Get(8, 2));
}
[Fact]
public void RowAndCol()
{
var grid = new CharGrid(3, 2);
Assert.Equal("\0\0\0", grid.GetRow(0));
grid.Fill(" ");
Assert.Equal(" ", grid.GetCol(1));
Assert.Throws<CharGridException.OutOfBounds>(() => grid.GetCol(3));
Assert.Throws<CharGridException.InvalidSeriesLength>(() => grid.SetRow(1, "Text"));
grid.SetRow(1, "Foo");
Assert.Equal("Foo", grid.GetRow(1));
Assert.Equal(" o", grid.GetCol(2));
}
}

View file

@ -0,0 +1,42 @@
namespace ServicePoint.Tests;
public class CommandTests
{
private Connection _connection = Connection.NewFake();
[Fact]
public void ClearSendable()
{
_connection.Send(Command.Clear());
}
[Fact]
public void BrightnessSendable()
{
_connection.Send(Command.Brightness(5));
}
[Fact]
public void InvalidBrightnessThrows()
{
Assert.Throws<ServicePointException.InvalidBrightness>(() => Command.Brightness(42));
}
[Fact]
public void FadeOutSendable()
{
_connection.Send(Command.FadeOut());
}
[Fact]
public void HardResetSendable()
{
_connection.Send(Command.HardReset());
}
[Fact]
public void BitmapLinearWinSendable()
{
_connection.Send(Command.BitmapLinearWin(0, 0, Bitmap.NewMaxSized(), CompressionCode.Uncompressed));
}
}

View file

@ -0,0 +1,11 @@
namespace ServicePoint.Tests;
public class ConnectionTests
{
[Fact]
public void InvalidHostnameThrows()
{
Assert.Throws<ServicePointException.IoException>(() => new Connection(""));
Assert.Throws<ServicePointException.IoException>(() => new Connection("-%6$§"));
}
}

View file

@ -0,0 +1,2 @@
global using Xunit;
global using ServicePoint;

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../ServicePoint/ServicePoint.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,14 @@
using ServicePoint;
public static class ServicePointConstants
{
private static readonly Constants _instance = ServicepointBindingUniffiMethods.GetConstants();
public static readonly ulong PixelWidth = _instance.pixelWidth;
public static readonly ulong PixelHeight = _instance.pixelHeight;
public static readonly ulong PixelCount = _instance.pixelCount;
public static readonly ulong TileWidth = _instance.tileWidth;
public static readonly ulong TileHeight = _instance.tileHeight;
public static readonly ulong TileSize = _instance.tileSize;
}

View file

@ -5,13 +5,11 @@
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
</PropertyGroup>
<PropertyGroup>
<PackageId>ServicePoint</PackageId>
<Version>0.11.0</Version>
<Version>0.12.0</Version>
<Authors>Repository Authors</Authors>
<Company>None</Company>
<Product>ServicePoint</Product>
@ -27,30 +25,28 @@
<!-- generate C# bindings -->
<Target Name="BuildBindings" Condition="'$(Configuration)'=='Release'" BeforeTargets="PrepareForBuild">
<Exec Command="cargo build --release"/>
<Exec Command="cargo build --manifest-path ../../../crates/servicepoint_binding_c/Cargo.toml --release"/>
<Exec Command="cargo build -p servicepoint_binding_uniffi --release"/>
</Target>
<Target Name="BuildBindings" Condition="'$(Configuration)'=='Debug'" BeforeTargets="PrepareForBuild">
<Exec Command="cargo build"/>
<Exec Command="cargo build --manifest-path ../../../crates/servicepoint_binding_c/Cargo.toml"/>
<Exec Command="cargo build -p servicepoint_binding_uniffi"/>
</Target>
<!-- include native binary in output -->
<ItemGroup Condition="'$(Configuration)'=='Debug'">
<Content Include="../../../target/debug/libservicepoint_binding_c.so" CopyToOutputDirectory="Always">
<Link>libservicepoint_binding_c.so</Link>
<Content Include="../../../../../target/debug/libservicepoint_binding_uniffi.so" CopyToOutputDirectory="Always">
<Link>libservicepoint_binding_uniffi.so</Link>
</Content>
</ItemGroup>
<ItemGroup Condition="'$(Configuration)'=='Release'">
<Content Include="../../../target/release/libservicepoint_binding_c.so" CopyToOutputDirectory="Always">
<Link>libservicepoint_binding_c.so</Link>
<Content Include="../../../../../target/release/libservicepoint_binding_uniffi.so" CopyToOutputDirectory="Always">
<Link>libservicepoint_binding_uniffi.so</Link>
</Content>
</ItemGroup>
<ItemGroup>
<!-- include link to source code at revision -->
<None Include="../README.md" Pack="true" PackagePath="\"/>
<!-- add README.md to package -->
<None Include="../README.md" Pack="true" PackagePath="\"/>
<!-- include link to source code at revision -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
</ItemGroup>

View file

@ -0,0 +1,34 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint", "ServicePoint\ServicePoint.csproj", "{53576D3C-E32E-49BF-BF10-2DB504E50CE1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint.Example", "ServicePoint.Example\ServicePoint.Example.csproj", "{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServicePoint.Tests", "ServicePoint.Tests\ServicePoint.Tests.csproj", "{9DC15508-A980-4135-9FC6-659FF54B4E5C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53576D3C-E32E-49BF-BF10-2DB504E50CE1}.Release|Any CPU.Build.0 = Release|Any CPU
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FEF24227-090E-46C2-B8F6-ACB5AA1A4309}.Release|Any CPU.Build.0 = Release|Any CPU
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DC15508-A980-4135-9FC6-659FF54B4E5C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'servicepoint', path: '..'

View file

@ -0,0 +1,19 @@
PATH
remote: ..
specs:
servicepoint (0.0.0)
ffi
GEM
remote: https://rubygems.org/
specs:
ffi (1.17.0-x86_64-linux-gnu)
PLATFORMS
x86_64-linux
DEPENDENCIES
servicepoint!
BUNDLED WITH
2.3.27

View file

@ -0,0 +1,25 @@
require_relative "../lib/servicepoint_binding_uniffi"
include ServicepointBindingUniffi
connection = Connection.new("172.23.42.29:2342")
pixels = Bitmap.new_max_sized
x_offset = 0
loop do
pixels.fill(false)
(0..((pixels.height) -1)).each do |y|
pixels.set((y + x_offset) % pixels.width, y, true);
end
command = Command.bitmap_linear_win(0, 0, pixels, CompressionCode::UNCOMPRESSED)
connection.send(command)
sleep 0.0005
x_offset += 1
end

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
LD_LIBRARY_PATH="../../../../../target/release:$LD_LIBRARY_PATH" ruby example.rb

View file

@ -0,0 +1,13 @@
Gem::Specification.new do |s|
s.name = "servicepoint"
s.version = "0.12.0"
s.summary = ""
s.description = ""
s.authors = ["kaesaecracker"]
s.email = ""
s.files = ["lib/servicepoint_binding_uniffi.rb"]
s.homepage =
"https://rubygems.org/gems/hola"
s.license = "MIT"
s.add_dependency 'ffi'
end

View file

@ -0,0 +1,3 @@
fn main() {
uniffi_bindgen_cs::main().unwrap();
}

View file

@ -0,0 +1,3 @@
fn main() {
uniffi_bindgen_go::main().unwrap();
}

View file

@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}

View file

@ -0,0 +1,77 @@
use servicepoint::{DataRef, Grid};
use std::sync::{Arc, RwLock};
#[derive(uniffi::Object)]
pub struct Bitmap {
pub(crate) actual: RwLock<servicepoint::Bitmap>,
}
impl Bitmap {
fn internal_new(actual: servicepoint::Bitmap) -> Arc<Self> {
Arc::new(Self {
actual: RwLock::new(actual),
})
}
}
#[uniffi::export]
impl Bitmap {
#[uniffi::constructor]
pub fn new(width: u64, height: u64) -> Arc<Self> {
Self::internal_new(servicepoint::Bitmap::new(
width as usize,
height as usize,
))
}
#[uniffi::constructor]
pub fn new_max_sized() -> Arc<Self> {
Self::internal_new(servicepoint::Bitmap::max_sized())
}
#[uniffi::constructor]
pub fn load(width: u64, height: u64, data: Vec<u8>) -> Arc<Self> {
Self::internal_new(servicepoint::Bitmap::load(
width as usize,
height as usize,
&data,
))
}
#[uniffi::constructor]
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.read().unwrap().clone())
}
pub fn set(&self, x: u64, y: u64, value: bool) {
self.actual
.write()
.unwrap()
.set(x as usize, y as usize, value)
}
pub fn get(&self, x: u64, y: u64) -> bool {
self.actual.read().unwrap().get(x as usize, y as usize)
}
pub fn fill(&self, value: bool) {
self.actual.write().unwrap().fill(value)
}
pub fn width(&self) -> u64 {
self.actual.read().unwrap().width() as u64
}
pub fn height(&self) -> u64 {
self.actual.read().unwrap().height() as u64
}
pub fn equals(&self, other: &Bitmap) -> bool {
let a = self.actual.read().unwrap();
let b = other.actual.read().unwrap();
*a == *b
}
pub fn copy_raw(&self) -> Vec<u8> {
self.actual.read().unwrap().data_ref().to_vec()
}
}

View file

@ -0,0 +1,61 @@
use std::sync::{Arc, RwLock};
#[derive(uniffi::Object)]
pub struct BitVec {
pub(crate) actual: RwLock<servicepoint::BitVec>,
}
impl BitVec {
fn internal_new(actual: servicepoint::BitVec) -> Arc<Self> {
Arc::new(Self {
actual: RwLock::new(actual),
})
}
}
#[uniffi::export]
impl BitVec {
#[uniffi::constructor]
pub fn new(size: u64) -> Arc<Self> {
Self::internal_new(servicepoint::BitVec::repeat(false, size as usize))
}
#[uniffi::constructor]
pub fn load(data: Vec<u8>) -> Arc<Self> {
Self::internal_new(servicepoint::BitVec::from_slice(&data))
}
#[uniffi::constructor]
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.read().unwrap().clone())
}
pub fn set(&self, index: u64, value: bool) {
self.actual.write().unwrap().set(index as usize, value)
}
pub fn get(&self, index: u64) -> bool {
self.actual
.read()
.unwrap()
.get(index as usize)
.is_some_and(move |bit| *bit)
}
pub fn fill(&self, value: bool) {
self.actual.write().unwrap().fill(value)
}
pub fn len(&self) -> u64 {
self.actual.read().unwrap().len() as u64
}
pub fn equals(&self, other: &BitVec) -> bool {
let a = self.actual.read().unwrap();
let b = other.actual.read().unwrap();
*a == *b
}
pub fn copy_raw(&self) -> Vec<u8> {
self.actual.read().unwrap().clone().into_vec()
}
}

View file

@ -0,0 +1,86 @@
use servicepoint::{Brightness, DataRef, Grid};
use std::sync::{Arc, RwLock};
#[derive(uniffi::Object)]
pub struct BrightnessGrid {
pub(crate) actual: RwLock<servicepoint::BrightnessGrid>,
}
impl BrightnessGrid {
fn internal_new(actual: servicepoint::BrightnessGrid) -> Arc<Self> {
Arc::new(Self {
actual: RwLock::new(actual),
})
}
}
#[uniffi::export]
impl BrightnessGrid {
#[uniffi::constructor]
pub fn new(width: u64, height: u64) -> Arc<Self> {
Self::internal_new(servicepoint::BrightnessGrid::new(
width as usize,
height as usize,
))
}
#[uniffi::constructor]
pub fn load(width: u64, height: u64, data: Vec<u8>) -> Arc<Self> {
Self::internal_new(servicepoint::BrightnessGrid::saturating_load(
width as usize,
height as usize,
&data,
))
}
#[uniffi::constructor]
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.read().unwrap().clone())
}
pub fn set(&self, x: u64, y: u64, value: u8) {
self.actual.write().unwrap().set(
x as usize,
y as usize,
Brightness::saturating_from(value),
)
}
pub fn get(&self, x: u64, y: u64) -> u8 {
self.actual
.read()
.unwrap()
.get(x as usize, y as usize)
.into()
}
pub fn fill(&self, value: u8) {
self.actual
.write()
.unwrap()
.fill(Brightness::saturating_from(value))
}
pub fn width(&self) -> u64 {
self.actual.read().unwrap().width() as u64
}
pub fn height(&self) -> u64 {
self.actual.read().unwrap().height() as u64
}
pub fn equals(&self, other: &BrightnessGrid) -> bool {
let a = self.actual.read().unwrap();
let b = other.actual.read().unwrap();
*a == *b
}
pub fn copy_raw(&self) -> Vec<u8> {
self.actual
.read()
.unwrap()
.data_ref()
.iter()
.map(u8::from)
.collect()
}
}

View file

@ -0,0 +1,163 @@
use servicepoint::{Grid, SeriesError};
use std::convert::Into;
use std::sync::{Arc, RwLock};
use crate::cp437_grid::Cp437Grid;
#[derive(uniffi::Object)]
pub struct CharGrid {
pub(crate) actual: RwLock<servicepoint::CharGrid>,
}
#[derive(uniffi::Error, thiserror::Error, Debug)]
pub enum CharGridError {
#[error("Exactly one character was expected, but {value:?} was provided")]
StringNotOneChar { value: String },
#[error("The provided series was expected to have a length of {expected}, but was {actual}")]
InvalidSeriesLength { actual: u64, expected: u64 },
#[error("The index {index} was out of bounds for size {size}")]
OutOfBounds { index: u64, size: u64 },
}
#[uniffi::export]
impl CharGrid {
#[uniffi::constructor]
pub fn new(width: u64, height: u64) -> Arc<Self> {
Self::internal_new(servicepoint::CharGrid::new(
width as usize,
height as usize,
))
}
#[uniffi::constructor]
pub fn load(data: String) -> Arc<Self> {
Self::internal_new(servicepoint::CharGrid::from(&*data))
}
#[uniffi::constructor]
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.read().unwrap().clone())
}
pub fn set(
&self,
x: u64,
y: u64,
value: String,
) -> Result<(), CharGridError> {
let value = Self::str_to_char(value)?;
self.actual
.write()
.unwrap()
.set(x as usize, y as usize, value);
Ok(())
}
pub fn get(&self, x: u64, y: u64) -> String {
self.actual
.read()
.unwrap()
.get(x as usize, y as usize)
.into()
}
pub fn fill(&self, value: String) -> Result<(), CharGridError> {
let value = Self::str_to_char(value)?;
self.actual.write().unwrap().fill(value);
Ok(())
}
pub fn width(&self) -> u64 {
self.actual.read().unwrap().width() as u64
}
pub fn height(&self) -> u64 {
self.actual.read().unwrap().height() as u64
}
pub fn equals(&self, other: &CharGrid) -> bool {
let a = self.actual.read().unwrap();
let b = other.actual.read().unwrap();
*a == *b
}
pub fn as_string(&self) -> String {
let grid = self.actual.read().unwrap();
String::from(&*grid)
}
pub fn set_row(&self, y: u64, row: String) -> Result<(), CharGridError> {
self.actual
.write()
.unwrap()
.set_row(y as usize, &row.chars().collect::<Vec<_>>())
.map_err(CharGridError::from)
}
pub fn set_col(&self, x: u64, col: String) -> Result<(), CharGridError> {
self.actual
.write()
.unwrap()
.set_row(x as usize, &col.chars().collect::<Vec<_>>())
.map_err(CharGridError::from)
}
pub fn get_row(&self, y: u64) -> Result<String, CharGridError> {
self.actual
.read()
.unwrap()
.get_row(y as usize)
.map(String::from_iter)
.ok_or(CharGridError::OutOfBounds {index: y, size: self.height()})
}
pub fn get_col(&self, x: u64) -> Result<String, CharGridError> {
self.actual
.read()
.unwrap()
.get_col(x as usize)
.map(String::from_iter)
.ok_or(CharGridError::OutOfBounds {index: x, size: self.width()})
}
pub fn to_cp437(&self) -> Arc<Cp437Grid> {
Cp437Grid::internal_new(servicepoint::Cp437Grid::from(&*self.actual.read().unwrap()))
}
}
impl CharGrid {
pub(crate) fn internal_new(actual: servicepoint::CharGrid) -> Arc<Self> {
Arc::new(Self {
actual: RwLock::new(actual),
})
}
fn str_to_char(value: String) -> Result<char, CharGridError> {
if value.len() != 1 {
return Err(CharGridError::StringNotOneChar {
value,
});
}
let value = value.chars().nth(0).unwrap();
Ok(value)
}
}
impl From<SeriesError> for CharGridError {
fn from(e: SeriesError) -> Self {
match e {
SeriesError::OutOfBounds { index, size } => {
CharGridError::OutOfBounds {
index: index as u64,
size: size as u64,
}
}
SeriesError::InvalidLength { actual, expected } => {
CharGridError::InvalidSeriesLength {
actual: actual as u64,
expected: expected as u64,
}
}
}
}
}

View file

@ -0,0 +1,162 @@
use crate::bitmap::Bitmap;
use crate::bitvec::BitVec;
use crate::brightness_grid::BrightnessGrid;
use crate::compression_code::CompressionCode;
use crate::cp437_grid::Cp437Grid;
use crate::errors::ServicePointError;
use servicepoint::Origin;
use std::sync::Arc;
#[derive(uniffi::Object)]
pub struct Command {
pub(crate) actual: servicepoint::Command,
}
impl Command {
fn internal_new(actual: servicepoint::Command) -> Arc<Command> {
Arc::new(Command { actual })
}
}
#[uniffi::export]
impl Command {
#[uniffi::constructor]
pub fn clear() -> Arc<Self> {
Self::internal_new(servicepoint::Command::Clear)
}
#[uniffi::constructor]
pub fn brightness(brightness: u8) -> Result<Arc<Self>, ServicePointError> {
servicepoint::Brightness::try_from(brightness)
.map_err(move |value| ServicePointError::InvalidBrightness {
value,
})
.map(servicepoint::Command::Brightness)
.map(Self::internal_new)
}
#[uniffi::constructor]
pub fn fade_out() -> Arc<Self> {
Self::internal_new(servicepoint::Command::FadeOut)
}
#[uniffi::constructor]
pub fn hard_reset() -> Arc<Self> {
Self::internal_new(servicepoint::Command::HardReset)
}
#[uniffi::constructor]
pub fn bitmap_linear_win(
offset_x: u64,
offset_y: u64,
bitmap: &Arc<Bitmap>,
compression: CompressionCode,
) -> Arc<Self> {
let origin = Origin::new(offset_x as usize, offset_y as usize);
let bitmap = bitmap.actual.read().unwrap().clone();
let actual = servicepoint::Command::BitmapLinearWin(
origin,
bitmap,
servicepoint::CompressionCode::try_from(compression as u16)
.unwrap(),
);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn char_brightness(
offset_x: u64,
offset_y: u64,
grid: &Arc<BrightnessGrid>,
) -> Arc<Self> {
let origin = Origin::new(offset_x as usize, offset_y as usize);
let grid = grid.actual.read().unwrap().clone();
let actual = servicepoint::Command::CharBrightness(origin, grid);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn bitmap_linear(
offset: u64,
bitmap: &Arc<BitVec>,
compression: CompressionCode,
) -> Arc<Self> {
let bitmap = bitmap.actual.read().unwrap().clone();
let actual = servicepoint::Command::BitmapLinear(
offset as usize,
bitmap,
servicepoint::CompressionCode::try_from(compression as u16)
.unwrap(),
);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn bitmap_linear_and(
offset: u64,
bitmap: &Arc<BitVec>,
compression: CompressionCode,
) -> Arc<Self> {
let bitmap = bitmap.actual.read().unwrap().clone();
let actual = servicepoint::Command::BitmapLinearAnd(
offset as usize,
bitmap,
servicepoint::CompressionCode::try_from(compression as u16)
.unwrap(),
);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn bitmap_linear_or(
offset: u64,
bitmap: &Arc<BitVec>,
compression: CompressionCode,
) -> Arc<Self> {
let bitmap = bitmap.actual.read().unwrap().clone();
let actual = servicepoint::Command::BitmapLinearOr(
offset as usize,
bitmap,
servicepoint::CompressionCode::try_from(compression as u16)
.unwrap(),
);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn bitmap_linear_xor(
offset: u64,
bitmap: &Arc<BitVec>,
compression: CompressionCode,
) -> Arc<Self> {
let bitmap = bitmap.actual.read().unwrap().clone();
let actual = servicepoint::Command::BitmapLinearXor(
offset as usize,
bitmap,
servicepoint::CompressionCode::try_from(compression as u16)
.unwrap(),
);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn cp437_data(
offset_x: u64,
offset_y: u64,
grid: &Arc<Cp437Grid>,
) -> Arc<Self> {
let origin = Origin::new(offset_x as usize, offset_y as usize);
let grid = grid.actual.read().unwrap().clone();
let actual = servicepoint::Command::Cp437Data(origin, grid);
Self::internal_new(actual)
}
#[uniffi::constructor]
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.clone())
}
pub fn equals(&self, other: &Command) -> bool {
self.actual == other.actual
}
}

View file

@ -0,0 +1,14 @@
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, uniffi::Enum)]
pub enum CompressionCode {
/// no compression
Uncompressed = 0x0,
/// compress using flate2 with zlib header
Zlib = 0x677a,
/// compress using bzip2
Bzip2 = 0x627a,
/// compress using lzma
Lzma = 0x6c7a,
/// compress using Zstandard
Zstd = 0x7a73,
}

View file

@ -0,0 +1,36 @@
use std::sync::Arc;
use crate::command::Command;
use crate::errors::ServicePointError;
#[derive(uniffi::Object)]
pub struct Connection {
actual: servicepoint::Connection,
}
#[uniffi::export]
impl Connection {
#[uniffi::constructor]
pub fn new(host: String) -> Result<Arc<Self>, ServicePointError> {
servicepoint::Connection::open(host)
.map(|actual| Arc::new(Connection { actual }))
.map_err(|err| ServicePointError::IoError {
error: err.to_string(),
})
}
#[uniffi::constructor]
pub fn new_fake() -> Arc<Self> {
Arc::new(Self {
actual: servicepoint::Connection::Fake,
})
}
pub fn send(&self, command: Arc<Command>) -> Result<(), ServicePointError> {
self.actual.send(command.actual.clone()).map_err(|err| {
ServicePointError::IoError {
error: format!("{err:?}"),
}
})
}
}

View file

@ -0,0 +1,21 @@
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, uniffi::Record)]
pub struct Constants {
pub tile_size: u64,
pub tile_width: u64,
pub tile_height: u64,
pub pixel_width: u64,
pub pixel_height: u64,
pub pixel_count: u64,
}
#[uniffi::export]
fn get_constants() -> Constants {
Constants {
tile_size: servicepoint::TILE_SIZE as u64,
tile_width: servicepoint::TILE_WIDTH as u64,
tile_height: servicepoint::TILE_HEIGHT as u64,
pixel_width: servicepoint::PIXEL_WIDTH as u64,
pixel_height: servicepoint::PIXEL_HEIGHT as u64,
pixel_count: servicepoint::PIXEL_COUNT as u64,
}
}

View file

@ -0,0 +1,77 @@
use servicepoint::{DataRef, Grid};
use std::sync::{Arc, RwLock};
use crate::char_grid::CharGrid;
#[derive(uniffi::Object)]
pub struct Cp437Grid {
pub(crate) actual: RwLock<servicepoint::Cp437Grid>,
}
impl Cp437Grid {
pub(crate) fn internal_new(actual: servicepoint::Cp437Grid) -> Arc<Self> {
Arc::new(Self {
actual: RwLock::new(actual),
})
}
}
#[uniffi::export]
impl Cp437Grid {
#[uniffi::constructor]
pub fn new(width: u64, height: u64) -> Arc<Self> {
Self::internal_new(servicepoint::Cp437Grid::new(
width as usize,
height as usize,
))
}
#[uniffi::constructor]
pub fn load(width: u64, height: u64, data: Vec<u8>) -> Arc<Self> {
Self::internal_new(servicepoint::Cp437Grid::load(
width as usize,
height as usize,
&data,
))
}
#[uniffi::constructor]
pub fn clone(other: &Arc<Self>) -> Arc<Self> {
Self::internal_new(other.actual.read().unwrap().clone())
}
pub fn set(&self, x: u64, y: u64, value: u8) {
self.actual
.write()
.unwrap()
.set(x as usize, y as usize, value)
}
pub fn get(&self, x: u64, y: u64) -> u8 {
self.actual.read().unwrap().get(x as usize, y as usize)
}
pub fn fill(&self, value: u8) {
self.actual.write().unwrap().fill(value)
}
pub fn width(&self) -> u64 {
self.actual.read().unwrap().width() as u64
}
pub fn height(&self) -> u64 {
self.actual.read().unwrap().height() as u64
}
pub fn equals(&self, other: &Cp437Grid) -> bool {
let a = self.actual.read().unwrap();
let b = other.actual.read().unwrap();
*a == *b
}
pub fn copy_raw(&self) -> Vec<u8> {
self.actual.read().unwrap().data_ref().to_vec()
}
pub fn to_utf8(&self) -> Arc<CharGrid> {
CharGrid::internal_new(servicepoint::CharGrid::from(&*self.actual.read().unwrap()))
}
}

View file

@ -0,0 +1,7 @@
#[derive(uniffi::Error, thiserror::Error, Debug)]
pub enum ServicePointError {
#[error("An IO error occurred: {error}")]
IoError { error: String },
#[error("The specified brightness value {value} is out of range")]
InvalidBrightness { value: u8 },
}

View file

@ -0,0 +1,12 @@
uniffi::setup_scaffolding!();
mod bitmap;
mod bitvec;
mod brightness_grid;
mod char_grid;
mod command;
mod compression_code;
mod connection;
mod cp437_grid;
mod errors;
mod constants;

View file

@ -0,0 +1,3 @@
[bindings.csharp]
namespace = "ServicePoint"
access_modifier = "public"

View file

@ -54,7 +54,11 @@
lzma
];
makeExample =
package: example:
{
package,
example,
features ? "",
}:
naersk'.buildPackage {
pname = example;
cargoBuildOptions =
@ -68,9 +72,12 @@
nativeBuildInputs = nativeBuildInputs;
strictDeps = true;
buildInputs = buildInputs;
gitSubmodules = true;
overrideMain = old: {
preConfigure = ''
cargo_build_options="$cargo_build_options --example ${example}"
cargo_build_options="$cargo_build_options --example ${example} ${
if features == "" then "" else "--features " + features
}"
'';
};
};
@ -95,11 +102,28 @@
in
rec {
servicepoint = makePackage "servicepoint";
announce = makeExample "servicepoint" "announce";
game-of-life = makeExample "servicepoint" "game_of_life";
moving-line = makeExample "servicepoint" "moving_line";
random-brightness = makeExample "servicepoint" "random_brightness";
wiping-clear = makeExample "servicepoint" "wiping_clear";
announce = makeExample {
package = "servicepoint";
example = "announce";
};
game-of-life = makeExample {
package = "servicepoint";
example = "game_of_life";
features = "rand";
};
moving-line = makeExample {
package = "servicepoint";
example = "moving_line";
};
random-brightness = makeExample {
package = "servicepoint";
example = "random_brightness";
features = "rand";
};
wiping-clear = makeExample {
package = "servicepoint";
example = "wiping_clear";
};
}
);
@ -117,16 +141,19 @@
clippy
cargo-expand
cargo-tarpaulin
gcc
gnumake
dotnet-sdk_8
];
};
in
{
default = pkgs.mkShell rec {
inputsFrom = [ self.packages.${system}.servicepoint ];
packages = [ rust-toolchain ];
packages = with pkgs; [
rust-toolchain
ruby
dotnet-sdk_8
gcc
gnumake
];
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath (builtins.concatMap (d: d.buildInputs) inputsFrom)}";
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
};